Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closes #8585: Support generic templates for plugins #8586

Merged
merged 6 commits into from
Feb 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 195 additions & 0 deletions docs/plugins/development/templates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
# Templates

## Base Templates

The following template blocks are available on all templates.

| Name | Required | Description |
|--------------|----------|---------------------------------------------------------------------|
| `title` | Yes | Page title |
| `content` | Yes | Page content |
| `head` | - | Content to include in the HTML `<head>` element |
| `javascript` | - | Javascript content included at the end of the HTML `<body>` element |

!!! note
For more information on how template blocks work, consult the [Django documentation](https://docs.djangoproject.com/en/stable/ref/templates/builtins/#block).

### layout.html

Path: `base/layout.html`

NetBox provides a base template to ensure a consistent user experience, which plugins can extend with their own content. This is a general-purpose template that can be used when none of the function-specific templates below are suitable.

#### Blocks

| Name | Required | Description |
|-----------|----------|----------------------------|
| `header` | - | Page header |
| `tabs` | - | Horizontal navigation tabs |
| `modals` | - | Bootstrap 5 modal elements |

#### Example

An example of a plugin template which extends `layout.html` is included below.

```jinja2
{% extends 'base/layout.html' %}

{% block header %}
<h1>My Custom Header</h1>
{% endblock header %}

{% block content %}
<p>{{ some_plugin_context_var }}</p>
{% endblock content %}
```

The first line of the template instructs Django to extend the NetBox base template and inject our custom content within its `content` block.

!!! note
Django renders templates with its own custom [template language](https://docs.djangoproject.com/en/stable/topics/templates/#the-django-template-language). This is very similar to Jinja2, however there are some important distinctions of which authors should be aware. Be sure to familiarize yourself with Django's template language before attempting to create new templates.

## Generic View Templates

### object.html

Path: `generic/object.html`

This template is used by the `ObjectView` generic view to display a single object.

#### Blocks

| Name | Required | Description |
|---------------------|----------|----------------------------------------------|
| `breadcrumbs` | - | Breadcrumb list items (HTML `<li>` elements) |
| `object_identifier` | - | A unique identifier (string) for the object |
| `extra_controls` | - | Additional action buttons to display |
| `extra_tabs` | - | Additional tabs to include |

#### Context

| Name | Required | Description |
|----------|----------|----------------------------------|
| `object` | Yes | The object instance being viewed |

### object_edit.html

Path: `generic/object_edit.html`

This template is used by the `ObjectEditView` generic view to create or modify a single object.

#### Blocks

| Name | Required | Description |
|------------------|----------|-------------------------------------------------------|
| `form` | - | Custom form content (within the HTML `<form>` element |
| `buttons` | - | Form submission buttons |

#### Context

| Name | Required | Description |
|--------------|----------|-----------------------------------------------------------------|
| `object` | Yes | The object instance being modified (or none, if creating) |
| `form` | Yes | The form class for creating/modifying the object |
| `return_url` | Yes | The URL to which the user is redirect after submitting the form |

### object_delete.html

Path: `generic/object_delete.html`

This template is used by the `ObjectDeleteView` generic view to delete a single object.

#### Blocks

None

#### Context

| Name | Required | Description |
|--------------|----------|-----------------------------------------------------------------|
| `object` | Yes | The object instance being deleted |
| `form` | Yes | The form class for confirming the object's deletion |
| `return_url` | Yes | The URL to which the user is redirect after submitting the form |

### object_list.html

Path: `generic/object_list.html`

This template is used by the `ObjectListView` generic view to display a filterable list of multiple objects.

#### Blocks

| Name | Required | Description |
|------------------|----------|--------------------------------------------------------------------|
| `extra_controls` | - | Additional action buttons |
| `bulk_buttons` | - | Additional bulk action buttons to display beneath the objects list |

#### Context

| Name | Required | Description |
|------------------|----------|-----------------------------------------------------------------------|
| `model` | Yes | The object class |
| `table` | Yes | The table class used for rendering the list of objects |
| `permissions` | Yes | A mapping of add, change, and delete permissions for the current user |
| `action_buttons` | Yes | A list of buttons to display (options are `add`, `import`, `export`) |
| `filter_form` | - | The bound filterset form for filtering the objects list |
| `return_url` | - | The return URL to pass when submitting a bulk operation form |

### bulk_import.html

Path: `generic/bulk_import.html`

This template is used by the `BulkImportView` generic view to import multiple objects at once from CSV data.

#### Blocks

None

#### Context

| Name | Required | Description |
|--------------|----------|--------------------------------------------------------------|
| `model` | Yes | The object class |
| `form` | Yes | The CSV import form class |
| `return_url` | - | The return URL to pass when submitting a bulk operation form |
| `fields` | - | A dictionary of form fields, to display import options |

### bulk_edit.html

Path: `generic/bulk_edit.html`

This template is used by the `BulkEditView` generic view to modify multiple objects simultaneously.

#### Blocks

None

#### Context

| Name | Required | Description |
|--------------|----------|-----------------------------------------------------------------|
| `model` | Yes | The object class |
| `form` | Yes | The bulk edit form class |
| `table` | Yes | The table class used for rendering the list of objects |
| `return_url` | Yes | The URL to which the user is redirect after submitting the form |

### bulk_delete.html

Path: `generic/bulk_delete.html`

This template is used by the `BulkDeleteView` generic view to delete multiple objects simultaneously.

#### Blocks

| Name | Required | Description |
|-----------------|----------|---------------------------------------|
| `message_extra` | - | Supplementary warning message content |

#### Context

| Name | Required | Description |
|--------------|----------|-----------------------------------------------------------------|
| `model` | Yes | The object class |
| `form` | Yes | The bulk delete form class |
| `table` | Yes | The table class used for rendering the list of objects |
| `return_url` | Yes | The URL to which the user is redirect after submitting the form |
42 changes: 1 addition & 41 deletions docs/plugins/development/views.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,47 +71,7 @@ A URL pattern has three components:

This makes our view accessible at the URL `/plugins/animal-sounds/random/`. (Remember, our `AnimalSoundsConfig` class sets our plugin's base URL to `animal-sounds`.) Viewing this URL should show the base NetBox template with our custom content inside it.

## Templates

### Plugin Views

NetBox provides a base template to ensure a consistent user experience, which plugins can extend with their own content. This template includes four content blocks:

* `title` - The page title
* `header` - The upper portion of the page
* `content` - The main page body
* `javascript` - A section at the end of the page for including Javascript code

For more information on how template blocks work, consult the [Django documentation](https://docs.djangoproject.com/en/stable/ref/templates/builtins/#block).

```jinja2
{% extends 'base/layout.html' %}

{% block content %}
{% with config=settings.PLUGINS_CONFIG.netbox_animal_sounds %}
<h2 class="text-center" style="margin-top: 200px">
{% if animal %}
The {{ animal.name|lower }} says
{% if config.loud %}
{{ animal.sound|upper }}!
{% else %}
{{ animal.sound }}
{% endif %}
{% else %}
No animals have been created yet!
{% endif %}
</h2>
{% endwith %}
{% endblock %}

```

The first line of the template instructs Django to extend the NetBox base template and inject our custom content within its `content` block.

!!! note
Django renders templates with its own custom [template language](https://docs.djangoproject.com/en/stable/topics/templates/#the-django-template-language). This is very similar to Jinja2, however there are some important differences to be aware of.

### Extending Core Views
## Extending Core Views

Plugins can inject custom content into certain areas of the detail views of applicable models. This is accomplished by subclassing `PluginTemplateExtension`, designating a particular NetBox model, and defining the desired methods to render custom content. Four methods are available:

Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ nav:
- Getting Started: 'plugins/development/index.md'
- Models: 'plugins/development/models.md'
- Views: 'plugins/development/views.md'
- Templates: 'plugins/development/templates.md'
- Tables: 'plugins/development/tables.md'
- Forms: 'plugins/development/forms.md'
- Filter Sets: 'plugins/development/filtersets.md'
Expand Down
7 changes: 2 additions & 5 deletions netbox/dcim/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -803,7 +803,6 @@ def get_extra_context(self, request, instance):

return {
'instance_count': instance_count,
'active_tab': 'devicetype',
}


Expand Down Expand Up @@ -953,11 +952,10 @@ class ModuleTypeView(generic.ObjectView):
queryset = ModuleType.objects.prefetch_related('manufacturer')

def get_extra_context(self, request, instance):
# instance_count = Module.objects.restrict(request.user).filter(device_type=instance).count()
instance_count = Module.objects.restrict(request.user).filter(module_type=instance).count()

return {
# 'instance_count': instance_count,
'active_tab': 'moduletype',
'instance_count': instance_count,
}


Expand Down Expand Up @@ -1570,7 +1568,6 @@ def get_extra_context(self, request, instance):
return {
'services': services,
'vc_members': vc_members,
'active_tab': 'device',
}


Expand Down
22 changes: 11 additions & 11 deletions netbox/netbox/views/generic/bulk_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,21 @@
from django.core.exceptions import FieldDoesNotExist, ValidationError
from django.db import transaction, IntegrityError
from django.db.models import ManyToManyField, ProtectedError
from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea
from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django_tables2.export import TableExport

from extras.models import ExportTemplate
from extras.signals import clear_webhooks
from netbox.tables import configure_table
from utilities.error_handlers import handle_protectederror
from utilities.exceptions import PermissionsViolation
from utilities.forms import (
BootstrapMixin, BulkRenameForm, ConfirmationForm, CSVDataField, CSVFileField, restrict_form_fields,
)
from utilities.htmx import is_htmx
from utilities.permissions import get_permission_for_model
from netbox.tables import configure_table
from utilities.views import GetReturnURLMixin
from .base import BaseMultiObjectView

Expand Down Expand Up @@ -178,7 +178,7 @@ def get(self, request):
})

context = {
'content_type': content_type,
'model': model,
'table': table,
'permissions': permissions,
'action_buttons': self.action_buttons,
Expand Down Expand Up @@ -304,7 +304,7 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
Attributes:
model_form: The form used to create each imported object
"""
template_name = 'generic/object_bulk_import.html'
template_name = 'generic/bulk_import.html'
model_form = None

def _import_form(self, *args, **kwargs):
Expand Down Expand Up @@ -369,9 +369,9 @@ def get_required_permission(self):
def get(self, request):

return render(request, self.template_name, {
'model': self.model_form._meta.model,
'form': self._import_form(),
'fields': self.model_form().fields,
'obj_type': self.model_form._meta.model._meta.verbose_name,
'return_url': self.get_return_url(request),
**self.get_extra_context(request),
})
Expand Down Expand Up @@ -418,9 +418,9 @@ def post(self, request):
logger.debug("Form validation failed")

return render(request, self.template_name, {
'model': self.model_form._meta.model,
'form': form,
'fields': self.model_form().fields,
'obj_type': self.model_form._meta.model._meta.verbose_name,
'return_url': self.get_return_url(request),
**self.get_extra_context(request),
})
Expand All @@ -434,7 +434,7 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
filterset: FilterSet to apply when deleting by QuerySet
form: The form class used to edit objects in bulk
"""
template_name = 'generic/object_bulk_edit.html'
template_name = 'generic/bulk_edit.html'
filterset = None
form = None

Expand Down Expand Up @@ -590,7 +590,7 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
"""
An extendable view for renaming objects in bulk.
"""
template_name = 'generic/object_bulk_rename.html'
template_name = 'generic/bulk_rename.html'

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand Down Expand Up @@ -681,7 +681,7 @@ class BulkDeleteView(GetReturnURLMixin, BaseMultiObjectView):
filterset: FilterSet to apply when deleting by QuerySet
table: The table used to display devices being deleted
"""
template_name = 'generic/object_bulk_delete.html'
template_name = 'generic/bulk_delete.html'
filterset = None
table = None

Expand Down Expand Up @@ -759,8 +759,8 @@ def post(self, request, **kwargs):
return redirect(self.get_return_url(request))

return render(request, self.template_name, {
'model': model,
'form': form,
'obj_type_plural': model._meta.verbose_name_plural,
'table': table,
'return_url': self.get_return_url(request),
**self.get_extra_context(request),
Expand All @@ -775,7 +775,7 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView):
"""
Add one or more components (e.g. interfaces, console ports, etc.) to a set of Devices or VirtualMachines.
"""
template_name = 'generic/object_bulk_add_component.html'
template_name = 'generic/bulk_add_component.html'
parent_model = None
parent_field = None
form = None
Expand Down
Loading