diff --git a/docs/plugins/development/templates.md b/docs/plugins/development/templates.md new file mode 100644 index 000000000..70228c623 --- /dev/null +++ b/docs/plugins/development/templates.md @@ -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 `` element | +| `javascript` | - | Javascript content included at the end of the HTML `` 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 %} +

My Custom Header

+{% endblock header %} + +{% block content %} +

{{ some_plugin_context_var }}

+{% 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 `
  • ` 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 `
    ` 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 | diff --git a/docs/plugins/development/views.md b/docs/plugins/development/views.md index 9c44e18ed..5408cef19 100644 --- a/docs/plugins/development/views.md +++ b/docs/plugins/development/views.md @@ -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 %} -

    - {% 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 %} -

    - {% 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: diff --git a/mkdocs.yml b/mkdocs.yml index bc582cb2f..fb64ca44a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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' diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index c70f33074..1df1eb1ac 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -803,7 +803,6 @@ class DeviceTypeView(generic.ObjectView): return { 'instance_count': instance_count, - 'active_tab': 'devicetype', } @@ -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, } @@ -1570,7 +1568,6 @@ class DeviceView(generic.ObjectView): return { 'services': services, 'vc_members': vc_members, - 'active_tab': 'device', } diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index 8a6d1e49f..912fceb11 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -7,13 +7,14 @@ from django.contrib.contenttypes.models import ContentType 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 ( @@ -21,7 +22,6 @@ from utilities.forms import ( ) 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 @@ -178,7 +178,7 @@ class ObjectListView(BaseMultiObjectView): }) context = { - 'content_type': content_type, + 'model': model, 'table': table, 'permissions': permissions, 'action_buttons': self.action_buttons, @@ -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): @@ -369,9 +369,9 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView): 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), }) @@ -418,9 +418,9 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView): 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), }) @@ -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 @@ -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) @@ -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 @@ -759,8 +759,8 @@ class BulkDeleteView(GetReturnURLMixin, BaseMultiObjectView): 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), @@ -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 diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index a078f565c..e5e90e0f1 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -344,8 +344,7 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): restrict_form_fields(form, request.user) return render(request, self.template_name, { - 'obj': obj, - 'obj_type': self.queryset.model._meta.verbose_name, + 'object': obj, 'form': form, 'return_url': self.get_return_url(request, obj), **self.get_extra_context(request, obj), @@ -423,8 +422,7 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): logger.debug("Form validation failed") return render(request, self.template_name, { - 'obj': obj, - 'obj_type': self.queryset.model._meta.verbose_name, + 'object': obj, 'form': form, 'return_url': self.get_return_url(request, obj), **self.get_extra_context(request, obj), @@ -468,7 +466,6 @@ class ObjectDeleteView(GetReturnURLMixin, BaseObjectView): return render(request, self.template_name, { 'object': obj, - 'object_type': self.queryset.model._meta.verbose_name, 'form': form, 'return_url': self.get_return_url(request, obj), **self.get_extra_context(request, obj), @@ -513,7 +510,6 @@ class ObjectDeleteView(GetReturnURLMixin, BaseObjectView): return render(request, self.template_name, { 'object': obj, - 'object_type': self.queryset.model._meta.verbose_name, 'form': form, 'return_url': self.get_return_url(request, obj), **self.get_extra_context(request, obj), @@ -557,8 +553,7 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView): instance = self.alter_object(self.queryset.model, request) return render(request, self.template_name, { - 'obj': instance, - 'obj_type': self.queryset.model._meta.verbose_name, + 'object': instance, 'replication_form': form, 'form': model_form, 'return_url': self.get_return_url(request), @@ -577,8 +572,7 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView): return redirect(self.get_return_url(request)) return render(request, self.template_name, { - 'obj': instance, - 'obj_type': self.queryset.model._meta.verbose_name, + 'object': instance, 'replication_form': form, 'form': model_form, 'return_url': self.get_return_url(request), diff --git a/netbox/templates/base/layout.html b/netbox/templates/base/layout.html index cf3841dd2..083d0347f 100644 --- a/netbox/templates/base/layout.html +++ b/netbox/templates/base/layout.html @@ -4,6 +4,14 @@ {% load search %} {% load static %} +{% comment %} +Blocks: + header: Page header + tabs: Horizontal navigation tabs + content: Page content + modals: Bootstrap 5 modal components +{% endcomment %} + {% block layout %}
    diff --git a/netbox/templates/circuits/circuittermination_edit.html b/netbox/templates/circuits/circuittermination_edit.html index 9ba41216d..3f3fad812 100644 --- a/netbox/templates/circuits/circuittermination_edit.html +++ b/netbox/templates/circuits/circuittermination_edit.html @@ -2,7 +2,7 @@ {% load static %} {% load form_helpers %} -{% block title %}{{ obj.circuit.provider }} {{ obj.circuit }} - Side {{ form.term_side.value }}{% endblock %} +{% block title %}{{ object.circuit.provider }} {{ object.circuit }} - Side {{ form.term_side.value }}{% endblock %} {% block form %}
    @@ -12,13 +12,13 @@
    - +
    - +
    @@ -69,7 +69,7 @@ {# Override buttons block, 'Create & Add Another'/'_addanother' is not needed on a circuit. #} {% block buttons %} Cancel - {% if obj.pk %} + {% if object.pk %} diff --git a/netbox/templates/dcim/device/base.html b/netbox/templates/dcim/device/base.html index 4d24717cd..ff139ee4a 100644 --- a/netbox/templates/dcim/device/base.html +++ b/netbox/templates/dcim/device/base.html @@ -95,13 +95,7 @@ {% endif %} {% endblock %} -{% block tab_items %} - - +{% block extra_tabs %} {% with tab_name='device-bays' devicebay_count=object.devicebays.count %} {% if active_tab == tab_name or devicebay_count %} - +{% block extra_tabs %} {% with tab_name='device-bay-templates' devicebay_count=object.devicebaytemplates.count %} {% if active_tab == tab_name or devicebay_count %}
    diff --git a/netbox/templates/dcim/moduletype/base.html b/netbox/templates/dcim/moduletype/base.html index 70a447499..f5713efc3 100644 --- a/netbox/templates/dcim/moduletype/base.html +++ b/netbox/templates/dcim/moduletype/base.html @@ -43,13 +43,7 @@ {% endif %} {% endblock %} -{% block tab_items %} - - +{% block extra_tabs %} {% with interface_count=object.interfacetemplates.count %} {% if interface_count %}
  • elements) + object_identifier: Unique identifier for the object + extra_controls: Additional action buttons to display + extra_tabs: Additional tabs to include + content: Page content + +Context: + object: The object instance being viewed +{% endcomment %} + {% block header %}
    {# Breadcrumbs #} @@ -66,11 +78,15 @@ {% block tabs %}