Templates are responsible for rendering HTML content for NetBox views. Each template exists as a file with a mix of HTML and template code. Generally speaking, each model in a NetBox plugin must have its own template. Templates may also be created or customized for other views, but the default templates NetBox provides are suitable in most cases.
NetBox's rendering backend uses the Django Template Language (DTL). It will immediately look very familiar if you've used Jinja2, but be aware that there are some important differences between the two. Generally, DTL is much more limited in the types of logic it can execute: Directly executing Python code, for instance, is not possible. Be sure to study the Django documentation before attempting to create any complex templates.
🟦 Note: If you skipped the previous step, run git checkout step05-views
.
NetBox looks for templates within the templates/
directory (if it exists) within the plugin root. Within this directory, create a subdirectory bearing the name of the plugin:
$ cd netbox_access_lists/
$ mkdir -p templates/netbox_access_lists/
The template files will reside in this directory. Default templates are provided for all generic views except for ObjectView
, so we'll need to create templates for our AccessListView
and AccessListRuleView
views.
By default, each ObjectView
subclass will look for a template bearing the name of its associated model. For instance, AccessListView
will look for accesslist.html
. This can be overriden by setting template_name
on the view, but this behavior is suitable for our purposes.
Begin by creating the file accesslist.html
in the plugin's template directory.
$ edit templates/netbox_access_lists/accesslist.html
Although we need to create our own template, NetBox has done much of the work for us, and provides a generic template that we can easily extend. At the top of the file, add an extends
tag:
{% extends 'generic/object.html' %}
This tells the rendering engine to first load the NetBox template at generic/object.html
and populate only the content we provide within block
tags.
Let's extend the generic template's content
block with some information about the access list.
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">Access List</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Name</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">Default Action</th>
<td>{{ object.get_default_action_display }}</td>
</tr>
<tr>
<th scope="row">Rules</th>
<td>{{ object.rules.count }}</td>
</tr>
</table>
</div>
</div>
{% include 'inc/panels/custom_fields.html' %}
</div>
<div class="col col-md-6">
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/comments.html' %}
</div>
</div>
{% endblock content %}
Here we've created a Boostrap 5 row and two column elements. In the first column, we have a simple card to display the access list's name and default action, as well as the number of rules assigned to it. And below it, you'll see an include
tag which pulls in an additional template to render any custom fields associated with the model. In the second column, we've included two more templates to render tags and comments.
🟢 Tip: If you're not sure how best to construct the page's layout, there are plenty of examples to reference within NetBox's core templates.
Let's take a look at our new template! Navigate to the list view again (at http://localhost:8000/plugins/access-lists/access-lists/), and follow the link through to a particular access list. You should see something like the image below.
🟦 Note: If NetBox complains that the template still does not exist, you may need to manually restart the development server (manage.py runserver
).
This is nice, but it would be handy to include the access list's assigned rules on the page as well.
To include the access list rules, we'll need to provide additional context data under the view. Open views.py
and find the AccessListView
class. (It should be the first class defined.) Add a get_extra_context()
method to this class per the code below.
class AccessListView(generic.ObjectView):
queryset = models.AccessList.objects.all()
def get_extra_context(self, request, instance):
table = tables.AccessListRuleTable(instance.rules.all())
table.configure(request)
return {
'rules_table': table,
}
This method does three things:
- Instantiate
AccessListRuleTable
with a queryset matching all rules assigned to this access list - Configure the table instance according to the current request (to honor user preferences)
- Return a dictionary of context data referencing the table instance
This makes the table available to our template as the rules_table
context variable. Let's add it to our template.
First, we need to import the render_table
tag from the django-tables2
library, so that we can render the table as HTML. Add this at the top of the template, immediately below the {% extends 'generic/object.html' %}
line:
{% load render_table from django_tables2 %}
Then, immediately above the {% endblock content %}
line at the end of the file, insert the following template code:
<div class="row">
<div class="col col-md-12">
<div class="card">
<h5 class="card-header">Rules</h5>
<div class="card-body table-responsive">
{% render_table rules_table %}
</div>
</div>
</div>
</div>
After refreshing the access list view in the browser, you should now see the rules table at the bottom of the page.
Speaking of rules, let's not forget about our AccessListRule
model: It needs a template too. Create accesslistrule.html
alongside our first template:
$ edit templates/netbox_access_lists/accesslistrule.html
And copy the content below:
{% extends 'generic/object.html' %}
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">Access List Rule</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Access List</th>
<td>
<a href="{{ object.access_list.get_absolute_url }}">{{ object.access_list }}</a>
</td>
</tr>
<tr>
<th scope="row">Index</th>
<td>{{ object.index }}</td>
</tr>
<tr>
<th scope="row">Description</th>
<td>{{ object.description|placeholder }}</td>
</tr>
</table>
</div>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
</div>
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">Details</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Protocol</th>
<td>{{ object.get_protocol_display }}</td>
</tr>
<tr>
<th scope="row">Source Prefix</th>
<td>
{% if object.source_prefix %}
<a href="{{ object.source_prefix.get_absolute_url }}">{{ object.source_prefix }}</a>
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Source Ports</th>
<td>{{ object.source_ports|join:", "|placeholder }}</td>
</tr>
<tr>
<th scope="row">Destination Prefix</th>
<td>
{% if object.destination_prefix %}
<a href="{{ object.destination_prefix.get_absolute_url }}">{{ object.destination_prefix }}</a>
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Destination Ports</th>
<td>{{ object.destination_ports|join:", "|placeholder }}</td>
</tr>
<tr>
<th scope="row">Action</th>
<td>{{ object.get_action_display }}</td>
</tr>
</table>
</div>
</div>
</div>
</div>
{% endblock content %}
You'll probably be able to tell at this point what most of the above template code does, but here are a few details worth mentioning:
- The URL for the rule's parent access list is retrieved by calling
object.access_list.get_absolute_url()
(the method we added in step five), without the parentheses (a distinction of DTL). This method is used for related prefixes as well. - NetBox's
placeholder
filter is applied to the rule's description. (This renders a — for empty fields.) - The
protocol
andaction
attributes are rendered by calling e.g.object.get_protocol_display()
(again without the parentheses). This is a Django convention for static choice fields to return the human-friendly label rather than the raw value.
Feel free to experiment with different layouts and content before proceeding with the next step.
⬅️ Step 5: Views | Step 7: Navigation ➡️