Skip to content

Nautobot Apps - Lab 06: Setting the Plugin Settings and Extending the Core Templates

So far, we've built a maintenance notice model and views for retrieving, creating, updating, and deleting instances of that model. Our plugin is really coming along! However, it's still not very tightly integrated with the rest of Nautobot. In this lab, we'll explore how to configure plugin settings as well as extend core templates.


Task 1: Set the Plugin Settings

Plugin settings allows the consumer of our plugin to customize the plugin behavior based on their needs. These custom settings are defined in the nautobot_config.py file using the PLUGINS_CONFIG variable. We can also define the default settings that will be used in the event that the plugin consumer does not customize the settings.

Step 1-0 - Prepare for the Lab

For this and all remaining labs, it is advisable to open two terminal windows, one for running the Nautobot development server, and another for executing various commands.

The steps in this lab require that you have a terminal open and have switched to the nautobot user. Verify your username with the whoami command and switch user to nautobot if needed.

ntc@nautobot:~$ whoami
ntc
ntc@nautobot:~$ sudo -iu nautobot
nautobot@nautobot:~$ whoami
nautobot
nautobot@nautobot:~$

If you have the Nautobot development server running from a previous lab, please continue to the next step. Otherwise we need to start the server with the command nautobot-server runserver 0.0.0.0:8080 --insecure.

nautobot@ntc-nautobot-apps:~$ nautobot-server runserver 0.0.0.0:8080 --insecure
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
April 24, 2022 - 05:19:10
Django version 3.1.14, using settings 'nautobot_config'
Starting development server at http://0.0.0.0:8080/
Quit the server with CONTROL-C.

Step 1-1 - Configure Default Plugin Settings

We will use the plugin settings to specify a default duration time for our Maintenance Notices. Edit the __init__.py file located in the plugin/maintenance_notices/ folder. This is where we previously defined our MaintenanceNoticesConfig class. Locate the line default_settings = {} and make the following change:

class MaintenanceNoticesConfig(PluginConfig):
    ...
    default_settings = {
        "default_duration": 120,
    }

Step 1-2 - Access the Plugin Settings

Let's make use of this now! We can reference this plugin configuration parameter to provide a default value for the maintenance notice form's duration field. Edit the views.py file.

First, import Django's settings module. We can put this above any other imports that we have in the file. We also want to assign the plugin specific settings to the constant SETTINGS. The second step will make our code a little cleaner when we reference the settings.

"""Views for Maintenance Notices."""

from django.conf import settings

...

SETTINGS = settings.PLUGINS_CONFIG['maintenance_notices']


class MaintenanceNoticeListView(generic.ObjectListView):
    ...

Now locate the alter_obj method that we defined in the previous lab and add a line that assigns obj.duration to the default_duration that we coded in the previous step.

class MaintenanceNoticeCreateView(MaintenanceNoticeEditView):
    """Create view."""

    def alter_obj(self, obj, request, *args, **kwargs):
        """Insert default values into the object."""
        obj.created_by = request.user
        obj.duration = SETTINGS.get('default_duration')
        return obj

Save your file.

Step 1-3 - Verify the Change

In your web browser, navigate to Plugins > Maintenance Notices and click the +Add button. Note that the default setting of 120 is now populated in the Duration form field. This is the same value that we defined in MaintenanceNoticesConfig in the __init__.py file!

Screenshot: Default Duration

Step 1-4 - Override the Default Plugin Settings

A Nautobot administrator can modify this default value for an installed plugin by setting it in the installation's local nautobot_config.py file. Let's edit our nautobot_config.py file, which should be located in your nautobot users home folder. Locate the PLUGINS_CONFIG assignment and add a section for our plugin.

PLUGINS_CONFIG = {
    "maintenance_notices": {
        'default_duration': 240
    },
    ...
}

Save the nautobot_config.py file. Note that our nautobot_server does not monitor for changes in the nautobot_config.py file, so we need to stop the server if it is running, and restart it to load in the new settings. After reloading the server, once again add a maintenance notice and verify that the Duration field is now pre-populated with the value specified in the PLUGINS_CONFIG.

Screenshot: Default duration from config


Let's add a second menu item to display only active maintenance notices: those with an end_time in the future.

Note: Our goal here is to display notices that are current or pending, and to hide notices that have occurred in the past. active by this definition excludes expired notices and is not limited to those whose maintenance window is within the current time.

Step 2-1 - Add a View with a Custom QuerySet

In views.py, we'll add a second ListView subclass, similar to the current one. Since it will be nearly identical to the first, we can subclass the original and replace the queryset with one that employs a filter. This filtered queryset will only match maintenance notices with an end_time value in the future.

First we import django.utils.timezone, so that we can call timezone.now() to get the current time. The we subclass MaintenanceNoticeListView and provide a custom queryset.

Finally, we add the extra_context method to override the title that is displayed on the page.

"""Views for Maintenance Notices."""

from django.conf import settings
from django.utils import timezone
...

class MaintenanceNoticeListView(generic.ObjectListView):
    """List view."""

    queryset = models.MaintenanceNotice.objects.all()
    table = tables.MaintenanceNoticeTable
    action_buttons = ("add",)


class ActiveMaintenanceNoticeListView(MaintenanceNoticeListView):
    """List view for Active Notices."""

    queryset = models.MaintenanceNotice.objects.filter(end_time__gt=timezone.now())

    def extra_context(self):
        return {"title": "Active Maintenance Notices (that have not expired)"}

...

Note: Wondering why we use the timezone library instead of datetime? This ensures that we're using a timezone-aware datetime object specifying the timezone configured for Nautobot (this is UTC by default).

Step 2-2 - Add a Route in urls.py file

We need to add a URL pattern for this view in urls.py as well. Add it below the first list view.

urlpatterns = [
    ...
    path('maintenancenotice/active/', views.ActiveMaintenanceNoticeListView.as_view(), name='maintenancenotice_active_list'),
    ...
]

Step 2-3 - Add to Plugins Menu

Now we can add a second menu item in navigation.py linking to our new view, beneath the first menu item. (We don't need to include any buttons for this item.)

menu_items = (
    ...
    PluginMenuItem(
        link='plugins:maintenance_notices:maintenancenotice_active_list',
        link_text='Active Maintenance Notices',
        permissions=['maintenance_notices.view_maintenancenotice'],
        buttons=[add_maintenancenotice_button],
    ),
)

Step 2-4 - Verify the navigation.py file

Verify that your navigation.py file look like this:

"""Navigation Items to add to Nautobot for maintenance_notices."""

from nautobot.extras.plugins import PluginMenuButton, PluginMenuItem
from nautobot.utilities.choices import ButtonColorChoices


add_maintenancenotice_button = PluginMenuButton(
    link='plugins:maintenance_notices:maintenancenotice_add',
    title='Add a new maintenance notice',
    icon_class='mdi mdi-plus-thick',
    color=ButtonColorChoices.GREEN,
    permissions=['maintenance_notices.add_maintenancenotice'],
)


menu_items = (
    PluginMenuItem(
        link='plugins:maintenance_notices:maintenancenotice_list',
        link_text='Maintenance Notices',
        buttons=[add_maintenancenotice_button],
        permissions=['maintenance_notices.view_maintenancenotice'],
    ),
    PluginMenuItem(
        link='plugins:maintenance_notices:maintenancenotice_active_list',
        link_text='Active Maintenance Notices',
        permissions=['maintenance_notices.view_maintenancenotice'],
        buttons=[add_maintenancenotice_button],
    ),
)

Step 2-5 - Verify the Change

Go to your web browser and refresh the page. Click Plugins on the menu, and you should now see a second menu item titled "Active Maintenance Notices" that links to /plugins/maintenance_notices/maintenancenotice/active.

Screenshot: Active Maintenance Notices Menu

Click the link and verify that only Maintenance Notices that end in the future are shown in the list.


Task 3: Showing All Active Maintenance Notices for a Device

The maintenance notice detail view shows all the devices to which it applies. It would also be convenient to show the reverse relationship, listing any applicable maintenance notices when viewing a device. We can do this by extending Nautobot's device view template.

Step 3-1 - Create Files

To accomplish this, we need to create two new files: - templates/maintenance_notices/device_notices.html - template_content.py

nautobot@nautobot:~$ cd /opt/nautobot/plugin
nautobot@nautobot:~/plugin$ touch maintenance_notices/template_content.py
nautobot@nautobot:~/plugin$ touch maintenance_notices/templates/maintenance_notices/device_notices.html

Step 3-2 - Create the HTML Template

First, we'll create the HTML template that will be used to render the embedded content. Edit the templates/maintenance_notices/device_notices.html file and add the content below.

<div class="panel panel-danger">
    <div class="panel-heading"><strong>Maintenance Notices</strong></div>
    <ul class="list-group">
        {% for notice in object.maintenance_notices.all %}
            <li class="list-group-item">
                <a href="{{ notice.get_absolute_url }}">{{ notice }}</a>
            </li>
        {% empty %}
            <li class="list-group-item">None found.</li>
        {% endfor %}
    </ul>
</div>

Should you want to have comments in the Django Template Language (DTL) or Jinja2 templates, you may bracket with {# before and #} after the comment text.

Save the file.

Step 3-3 - Decipher the Code

Note that, unlike with our earlier templates, we are not extending base.html. This is because we're not rendering a complete page: we're merely providing a modular piece of content that will be embedded within another template.

Recall from an earlier lab, when we created the Maintenance Notice model in models.py, we added a ManyToManyField that allows us to assign dcim.Device objects to our maintenance notice.

    devices = models.ManyToManyField(
        to='dcim.Device',
        related_name='maintenance_notices',
    )

When we did this, the Django framework automatically created an attribute on the Device model called maintenance_notices (from the related_name we specified). This attribute is of the type ManyRelatedManager. This is a special Django object that can act like a query set. In the above template code, we are accessing this ManyRelatedManager through the name object.maintenance_notices and calling the .all method, to retrieve all MaintenanceNotice objects related to the Device object. The for loop iterates over each of these and provides a link to each notice. If there are no objects found, the {% empty %} clause will print "None found."

Step 3-4 - Create the PluginTemplateExtension

We need to register our template extensions by importing and subclassing Nautobot's PluginTemplateExtension class in a file named template_content.py. We'll set its model attribute to the core Nautobot device model. (Note that this is a string referencing the model, rather than the Python class itself).

Edit the template_content.py file and add the following code.

from nautobot.extras.plugins import PluginTemplateExtension


class DeviceMaintenanceNotices(PluginTemplateExtension):
    """Display Maintenance Notices in a panel on device page."""

    model = 'dcim.device'

    def right_page(self):
        return self.render('maintenance_notices/device_notices.html')


template_extensions = [DeviceMaintenanceNotices]

Step 3-5 - Decipher the Code

Nautobot permits plugins to embed content in up to four places within a core object's detail view:

  • left_page(): Left side of the page (below core content)
  • right_page(): Right side of the page (below core content)
  • full_width_page(): Across the entire bottom of the page
  • buttons(): Add buttons to the top of the page

We have chosen to embed our maintenance notices list on the right hand side of the page, so we define a right_page() method.

Note: One PluginTemplateExtension subclass can define as many of these four methods as required. It is not limited to a single method.

Our right_page() method renders the Django template we created above, using the render() method. Although this approach is recommended to maintain the separation of business logic from design, these methods could instead return content directly as a string.

Note: The render() provides four pieces of context data to the template: object, request, settings, and config. Additional context data may be passed to the template as a dictionary using the extra_context keyword argument. We can see in our Django template that we are referencing object.

When Nautobot loads a plugin, it checks for the presence of the template_content.py file, and if it is found, it looks for the variable template_extensions and loads all PluginTemplateExtension that are specified.

Step 3-6 - Check Your Progress

Ensure you have saved your device_notices.html and template_content.py files. Since these file are new, the StatReloader is not monitoring them, and so we need to either manually restart the server, or make it think a tracked file has changed. Let's do the latter:

nautobot@nautobot:~/plugin$ touch maintenance_notices/views.py

In your web browser, navigate to Devices > Devices and the choose a device from the list that you have included in some of your maintenance notices.

Now, when you view a device with one or more active maintenance notices, they will appear in a panel along the right hand side of the page.

Screenshot: Panel showing associated maintenance notices in the device view

Step 3-7 - Modify the HTML Template

When we started this task we said we would display only active notices for a device, but what we have built is displaying all related notices. We need to add some more code to restrict the notices to only those that are active.

Edit the device_notices.html file. Locate this line:

    <div class="panel-heading"><strong>Maintenance Notices</strong></div>

Replace the above line for this:

    <div class="panel-heading"><strong>Active Maintenance Notices</strong></div>

Next, locate this line:

        {% for notice in object.maintenance_notices.all %}

Replace the above line for this:

        {% for notice in active_notices %}

Step 3-8 - Filter Notices

We will be injecting a QuerySet of the related active notices and naming it active_notices so that the template can find it.

Edit the template_content.py file. Add the timezone import and modify the right_page method to match the following code:

from django.utils import timezone
...

class DeviceMaintenanceNotices(PluginTemplateExtension):
    ...

    def right_page(self):
        """Panel to display maintenance notices."""
        obj = self.context["object"]
        active_notices = obj.maintenance_notices.filter(end_time__gt=timezone.now())
        extra_context = {"active_notices": active_notices}
        return self.render(
            'maintenance_notices/device_notices.html',
            extra_context=extra_context,
        )
...

Step 3-9 - Decipher the Code

Each PluginTemplateExtension object has a context attribute, which is a dictionary providing the following key/value pairs: - object - The object being viewed - request - The current request - settings - Global Nautobot settings - config - Plugin-specific configuration parameter

Our right_page method is getting the current device object from self.context["object"]. It is generating a QuerySet by running the same filter we used in the previous task, except this filter is applied only to those notices related to the current device object. By creating a dictionary called extra_context, we can define any names that we want to be available within out Django template. We use this to pass our active_notices queryset to the template.

Step 3-10 - Verify the Change

Make sure you save both files and your server reloads. Reload the device page in your web browser and you should now see only the active notices.

Screenshot: Panel showing only active maintenance notices in the device view

Note: While the nautobot-server will watch for changed Python files, it does not monitor the html template files, so you will need to manually restart the server when you are only changing the template files and not the Python code.

Step 3-11 - Commit Your Changes

Remember to commit your changes to git:

nautobot@nautobot:~$ cd ~/plugin/
nautobot@nautobot:~/plugin$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: maintenance_notices/__init__.py
modified: maintenance_notices/navigation.py
modified: maintenance_notices/urls.py
modified: maintenance_notices/views.py

Untracked files:
(use "git add <file>..." to include in what will be committed)
maintenance_notices/template_content.py
maintenance_notices/templates/maintenance_notices/device_notices.html

no changes added to commit (use "git add" and/or "git commit -a")

nautobot@nautobot:~/plugin$ git add -A
nautobot@nautobot:~/plugin$ git commit -m "Completed Lab 6."

Challenge

Step 4-1 - Test Active View

Attempt to write a test to validate the Active Maintenance Notice View. Use what you learned in Lab 04, and remember to reference the URL path using a path name.


This lab is now complete. Check your work against the solution guide before proceeding with the next lab.