Skip to content

Nautobot Apps - Lab 05: Extending the Navigation Menu & Capturing the Username

So far, we've built a maintenance notice model and views for retrieving, creating, updating, and deleting instances of that model. Our app is really coming along! However, it's still not very tightly integrated with the rest of Nautobot. In this lab, we'll explore how we can extend Nautobot's navigation menu to link to our plugin's views, and even embed plugin content into core Nautobot views.


Task 1: Extending the Nautobot Navigation Menu

To better integrate with Nautobot, we're going to add a link in the navigation menu to view all maintenance notices, as well as a menu button to add a new notice. This is consistent with how the core Nautobot objects are arranged within the menu.

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 - Create the navigation.py file

Nautobot looks for a navigation.py module within each plugin to check for any menu extensions. Locate and edit this file. We will need to import some classes that will be used to create the menu items and buttons. Your file should include these lines near the top:

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

Define a tuple named menu_items to hold all our menu extensions. The file should already contain some example code that we can edit to meet our needs.

menu_items = (
   ...
)

Step 1-2 - Create a Menu Item

Custom menu items are added by instantiating Nautobot's PluginMenuItem class. There are four arguments we can pass when doing so:

  • link: The name of the URL path to us for this link
  • link_text: The menu item text
  • permissions: A list of permissions required to display this link (optional)
  • buttons: An iterable of embedded menu buttons to include (optional)

Let's add a link to our maintenance notice list view. First, import extras.plugins.PluginMenuItem, then add an instance to menu_items. We don't need to define any required permissions for this item, because everyone should be able to view maintenance notices.

menu_items = (
    PluginMenuItem(
        link='plugins:maintenance_notices:maintenancenotice_list',
        link_text='Maintenance Notices',
    ),
)

Notice that the link is set to plugins:maintenance_notices:maintenancenotice_list. This string is a made of three parts that are each separated by a colon (:). This tells Nautobot how to locate the link: look in plugins: look in our maintenance_notices plugin, and finally, find the maintenancenotice_list link. Recall that maintenancenotice_list refers to a specific URL route that we defined in the urls.py file.

Save the file.

Step 1-3 - Access the Menu Item

Your nautobot-server should detect the changed file and reload. Take a look at the terminal session where you are running your server to check that it reloaded successfully.

Note: If your nautobot-server displays TypeError: 'PluginMenuItem' object is not iterable, then you are likely missing a comma after the parenthesis enclosing your PluginMenuItem.

Load the Nautobot home page in your browser. If you already had this page open, hit the refresh button.

Click the "Plugins" dropdown menu and you should now see a Maintenance Notices section near the bottom of the drop-down menu. Within that section should be our Maintenance Notices link. Click the link and you'll be taken to the maintenance notices list.

Screenshot: Plugins navigation menu

You should see the Maintenance Notices list view page.

Screenshot: List View Page

This is a lot easier than pasting in the link every time you want to access this page.

Step 1-4 - Create a Menu Button

Next, we'll add a button within this menu item to create new notices. To do this we will employ Nautobot's PluginMenuButton class, as well as ButtonColorChoices. The later is a list of valid button colors that we'll reference when creating the button.

There are several parameters we can pass when instantiating PluginMenuButton to control how buttons are displayed:

  • link: The name of the URL path to us for this button
  • title: The tooltip text (displayed when the mouse hovers over the button)
  • icon_class: Button icon CSS class
  • color: One of the choices provided by ButtonColorChoices (optional)
  • permissions: A list of permissions required to display this button (optional)

The link, title, and icon class parameters are required. The icon class is a CSS icon passed as a string; for example, mdi mdi-plus-thick. In this example, the mdi represents Google's "Material Design Icons" and the mdi-plus-thick is a plus sign from that collection.

Create a new PluginMenuButton instance above menu_items in the navigation.py file. We'll use the mdi-plus-thick icon CSS class and the color GREEN. We'll also restrict the display of this button to users who possess the add_maintenancenotice permission.

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,
)


menu_items = (...)

Step 1-5 - Add a Button to a Menu Item

Now that we have our button defined, add it to the menu item by appending the buttons keyword argument to the PluginMenuItem instance:

menu_items = (
    PluginMenuItem(
        link='plugins:maintenance_notices:maintenancenotice_list',
        link_text='Maintenance Notices',
        buttons=[add_maintenancenotice_button],
    ),
)

Save the file.

Step 1-6 - Check Your Work

Refresh the Nautobot home page again. Click the Plugins drop-down menu. Next to the Maintenance Notices link you'll see a small green button with a + icon.

Screenshot: Plugins navigation menu with add button

Click the + button to visit the Maintenance Notice creation view.

Screenshot: Maintenance Notice Creation View


Task 2: Restrict Access to Maintenance Notices

In this task, we'll define permissions that restrict which users can create maintenance notices.

Back in Lab 3, when we created a new model for our plugin in models.py, we imported the Nautobot BaseModel class and inherited its behaviors. This was performed via these two lines:

from nautobot.core.models import BaseModel

class MaintenanceNotice(BaseModel):
    ...

One of the features provided by the BaseModel class is the automatic creation of a set of permission objects that allow us to control CRUD access to the data represented by our model MaintenanceNotice. These are the permissions that are automatically created for our model, along with the corresponding CRUD operations:

add_maintenancenotice     # Create
view_maintenancenotice    # Read
change_maintenancenotice  # Update
delete_maintenancenotice  # Delete

Up to this point we have been logging into Nautobot web console using the admin user account, which has superuser access to the platform.

Step 2-1 - Add Permissions

Using the Nautobot administrative interface, we will add two permissions to Nautobot. If you have not already done so, open Nautobot in your web browser and login as the admin user. Click the admin username in the top right hand corner, then click Admin.

Screenshot: Access Admin Page

Navigate to the Users section toward the bottom of the page and click the +Add button next to permissions.

Screenshot: Add Permissions Button

Step 2-2 - Add Read Only Permissions

First we will create a read only permission for our plugin model. Fill out the form with the following information:

  • Name: maintenance_notices_read_only
  • Enabled: Check box
  • Actions: Check box for Can view only (This is our Read access permission)
  • Object types: Locate maintenance_notices > Maintenance Notice in the list and click it.
  • Users: Locate user1 and click.

Screenshot: Add Permission Read Only 1

Screenshot: Add Permission Read Only 2

Screenshot: Add Permission Read Only 3

At the bottom of the page, click Save and add another.

Screenshot: Add Permission Read Only 4

You should see a banner across the top stating The permission “maintenance_notices_read_only” was added successfully. You may add another permission below..

Screenshot: Add Permission Read Only 5

Step 2-3 - Add Read/Write Permissions

Create a second permission to allow the full read/write access to our model. This permission will be assigned to user2.

  • Name: maintenance_notices_read_write
  • Enabled: Check box
  • Actions: Check boxes for all four actions:
  • Can view
  • Can add
  • Can change
  • Can delete
  • Object types: Locate maintenance_notices > Maintenance Notice in the list and click it.
  • Users: Locate user2 and click.

Click the Save button. This will take you to the Permission list view page. Our new permissions should be displayed toward the bottom of the page.

Screenshot: New Permissions Created

Step 2-4 - Check Additional Permissions

Lets make sure our user1 and user2 have access to dcim.Device objects. Navigate to the Admin site by clicking your user name and then clicking Admin. Alternatively, you can use the "breadcrumbs" in the left side banner to click Admin Home.

Click either Admin Home on the left or Admin on the right.

Screenshot: Admin Site Access

Click the link for Users under the Users section.

Screenshot: Admin Site Access

Click the link for user1.

Screenshot: Select User 1

Scroll down to the bottom of the page to find Permissions. Note that user1 has the demo_read_only permission that provides view access to the device model. Since this is what we want, we do not need to change anything here.

Screenshot: Permission Demo Read Only

Step 2-5 - Grant Additional Permissions

Use the "breadcrumbs" at the top of the page to navigate back to the users page.

Screenshot: Breadcrumbs to Users

Click the link for user2.

Screenshot: Select User 2

Scroll down to the bottom of the page to find Permissions. Click the drop-down in the first available slot (---------). Then select the demo permission. Click Save to save you change.

Screenshot: Add Permission Demo

Step 2-6 - Reset Passwords

Now that we have defined these two permissions and assigned them to different users, we can test access for each of the users by logging out of the admin account and logging in, one at a time, to the user1 and user2 accounts.

If you do not know the passwords to these user accounts you can reset them through the admin page or you can use the command line to reset the passwords.

Set the user1 account password to one of your choosing:

nautobot@nautobot:~/plugin$ nautobot-server changepassword user1
Changing password for user 'user1'
Password:
Password (again):
Password changed successfully for user 'user1'
nautobot@nautobot:~/plugin$

Let's do the same for the user2 account:

nautobot@nautobot:~/plugin$ nautobot-server changepassword user2
Changing password for user 'user2'
Password:
Password (again):
Password changed successfully for user 'user2'
nautobot@nautobot:~/plugin$

Step 2-7 - Test Read/Write Access for User2

Let's start with user2.

Open Nautobot in your web browser and logout of the current account.

Now login as user2. Remember we set user2 to have read/write access. From the navigation menu, click Plugins > Maintenance Notices. You should see the ListView page for Maintenance Notices.

Screenshot: List View

Note that this page includes a blue +Add button in the upper right hand corner. Click the +Add button to verify that you arrive at the Add a new Maintenance Notice page.

Screenshot: Add New Notice

This confirms that we have the correct permissions. You can use this page to add a new maintenance notice to be sure.

Step 2-8 - Test Read Access for User1

In your web browser, logout, and then login as user1.

Recall we set user1 to have read only access. From the navigation menu, click Plugins > Maintenance Notices. This should again bring up the ListView page for Maintenance Notices. Note this time that this page does not display the +Add button that was previously seen in the upper right hand corner.

Screenshot: View Only List View

You may have noticed that when we click Plugins from the navigation menu, the the + (Add) button is showing up next to Maintenance Notices on the dropdown menu. Click the + to attempt to access the Add page.

Screenshot: Add Notice Menu

You should be greeted with an error Access Denied: You do not have permission to access this page.

Screenshot: Access Denied

Step 2-9 - Hide the Add Button

Ideally, we only want to display an Add button on the menu when the logged in user has the add_maintenancenotice permission object.

Edit the navigation.py file and locate where we defined add_maintenancenotice_button.

After the line:

    color=ButtonColorChoices.GREEN,

Add this line to require the add permission object:

    permissions=['maintenance_notices.add_maintenancenotice'],

Note: When adding permissions argument, make sure the previous line ends with a comma. When your arguments are formatted on separate lines it is good practice to end all lines, including the last one, with a comma, as it minimizes the number of lines changed in your change control system and prevents errors from missing commas when a new argument is added.

The file should now 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],
    ),
)

Save the file and refresh your browser.

Step 2-10 - Verify the Button is Removed

If you are not logged in as read_only user1, login as that user. Again, click Plugins from the navigation menu and note that the + (Add) button has been removed from the Maintenance Notices menu item.

Screenshot: Add Button Removed

Step 2-11 - Restrict Access to the Menu Item

Let's add the same permissions argument to the PluginMenuItem.

Edit the navigation.py files and add the line permissions=['maintenance_notices.view_maintenancenotice'], into argument list for the PluginMenuItem:

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

Save the file.

Step 2-12 - Remove View Permission from User1

In your web browser, logout of Nautobot and the log in as the admin user.

Navigate to the Admin site by clicking your user name and then the Admin link. Click Users at the bottom of the page. This should bring you to the user list. Click the link for user1.

Screenshot: Select User 1

Scroll to the bottom of the page to the Permissions section. Locate the permission for maintenance_notices_read_only and select the checkbox located in the Delete? column. Then click the Save button. This will remove user1's view (read) permission to our app.

Screenshot: Delete Permissions

Step 2-13 - Verify the Menu Item View Restriction

Once again, in your web browser, logout, and then login as user1.

Now when you click on the Plugins menu, the Maintenance Notices menu item is grayed out and cannot be clicked. This is what we want to happen for users that do not have read (view) access to the plugin.

Screenshot: No View Permission


Task 3: Add a DateTimePicker Widget

Step 3-1 - Check the Current Behavior

Login to Nautobot with the admin user account. Navigate to Plugins > Maintenance Notices and click the +Add button.

Fill out the form. Notice that the Start Time value requires that a valid date and time must be entered. Nautobot includes utilities that aid us when building forms that are simple to fill out. We will add the DateTimePicker to our form to simplify choosing the start time.

Step 3-2 - Edit the forms.py file

Edit the forms.py file.

Add the DateTimePicker into your imports.

"""Forms for maintenance_notices."""
from django import forms
from nautobot.utilities.forms import (
    BootstrapMixin,
    DateTimePicker,
)

Locate the MaintenanceNoticeForm class definition. Between the docstring and the class Meta:, insert the following code:

    start_time = forms.DateTimeField(
        widget=DateTimePicker(attrs={"placeholder": "Maintenance Start Date and Time"}),
        label="Start Time",
    )

Step 3-3 - Verify the forms.py file

Verify that your forms.py file should look like this the following snippet and save your file.

"""Forms for maintenance_notices."""
from django import forms
from nautobot.utilities.forms import (
    BootstrapMixin,
    DateTimePicker,
)

from maintenance_notices import models

class MaintenanceNoticeForm(BootstrapMixin, forms.ModelForm):
    """MaintenanceNotice creation/edit form."""

    start_time = forms.DateTimeField(
        widget=DateTimePicker(attrs={"placeholder": "Maintenance Start Date and Time"}),
        label="Start Time",
    )

    class Meta:
        """Meta attributes."""

        model = models.MaintenanceNotice
        fields = ("__all__")

Step 3-4 - Test the DateTimePicker Widget

Navigate to Plugins > Maintenance Notices > Add to add a new Maintenance Notice. Click on the Start Time field. Notice the form now provides an easy way to select the date and time.

Screenshot: DateTimePicker


Task 4: Associate a User to the Maintenance Notice

Step 4-1 - Create a Maintenance Notice via Web Browser

Login to Nautobot with the admin user account. Navigate to Plugins > Maintenance Notices and click the +Add button.

On the Add a new Maintenance Notice page, Fill out the Start time, Duration, and select one or more devices. Click the Create button.

Screenshot: Maintenance notice creation form

If there were any errors while submitting the form, they will be flagged and you will return to the creation view. Otherwise, you should be automatically directed to the newly created maintenance notice.

Screenshot: A new maintenance notice has been created

Note that the Created By attribute of our new maintenance notice is empty, represented by a line, where we expect to see the name of the logged in user. This is because we never added the logic for assigning the authenticated user to the maintenance notice when it's created.

Step 4-2 - Split the Create and Edit Views

Currently we are using the same view (MaintenanceNoticeCreateView) for both the Create and Edit forms, which has been fine up to the point. However, we only want to set the created_by field when we first create the our notice and we do not want to overwrite it when someone edits the record.

Edit the views.py file and locate the MaintenanceNoticeCreateView.

class MaintenanceNoticeCreateView(generic.ObjectEditView):
    """Create view."""

    model = models.MaintenanceNotice
    queryset = models.MaintenanceNotice.objects.all()
    model_form = forms.MaintenanceNoticeForm

Since we do not plan to change the Edit view, but we do want to modify the create view, lets rename this class to MaintenanceNoticeEditView and create a new MaintenanceNoticeCreateView that inherits from the previous view.

class MaintenanceNoticeEditView(generic.ObjectEditView):
    """Edit view."""

    model = models.MaintenanceNotice
    queryset = models.MaintenanceNotice.objects.all()
    model_form = forms.MaintenanceNoticeForm


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

Note that MaintenanceNoticeCreateView is now a subclass of MaintenanceNoticeEditView and so MaintenanceNoticeEditView must precede MaintenanceNoticeCreateView.

Save the file.

Step 4-3 - Update the urls.py file

It is not enough to create the new Edit view, we need to update the path in our urls.py file. Edit urls.py and locate the path ending in edit. Change view to use the MaintenanceNoticeEditView

    path("maintenancenotice/<uuid:pk>/edit/", views.MaintenanceNoticeEditView.as_view(), name="maintenancenotice_edit"),

Save the file.

Step 4-4 - Verify the Views

Create a new maintenance notice and verify that the behavior has not changed.

Navigate to Plugins > Maintenance Notices. Click the edit icon, which is the one that looks like a pencil, for the first maintenance notice on the list.

Screenshot: Maintenance notice list view

This should allow you to edit the chosen maintenance notice. Make a change to the duration and click the Update button. Verify that the behavior is unchanged.

Step 4-5 - Check the Nautobot Source Code

The ObjectEditView class provides a built in method named alter_obj that can be used for this purpose. If we take a look at the source code, we see that the alter_obj method accepts an object named obj and several arguments and then simply returns the object that was passed to it. This method then gets called by the view's get method. From this we can see that alter_obj is a method that is intended as a hook for us to replace with our own method in our subclass, and this will allow us to modify the object that is presented to the form.

Nautobot ObjectEditView source code

    def alter_obj(self, obj, request, url_args, url_kwargs):
        # Allow views to add extra info to an object before it is processed. For example, a parent object can be defined
        # given some parameter from the request URL.
        return obj

    ...

    def get(self, request, *args, **kwargs):
        obj = self.alter_obj(self.get_object(kwargs), request, args, kwargs)
        ...

Step 4-6 - Modify the Model object from the View

Edit the views.py file and locate the MaintenanceNoticeCreateView.

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

Remove the line pass and add the alter_obj method definition as show below.

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

    def alter_obj(self, obj, request, *args, **kwargs):
        """Insert user into object."""
        import pdb; pdb.set_trace()
        return obj

Notice that we have inserted a debugger into the method. The method otherwise doesn't do anything but when the code hits this point, our console should provide display the Pdb prompt.

Step 4-7 - Enter the Debugger

Save the file and wait for the server to reload.

Now use the web browser to add another maintenance notice. Upon clicking the +Add button the browser should pause as if the page is not loading. This is normal as the debugger has paused execution of the server.

Open the terminal where you are running the nautobot-server. You terminal should be waiting at the (Pdb) prompt as shown below.

/opt/nautobot/plugin/maintenance_notices/views.py changed, reloading.
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
March 10, 2022 - 23:38:53
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.
> /opt/nautobot/plugin/maintenance_notices/views.py(36)alter_obj()
-> return obj
(Pdb)

Step 4-8 - Explore the Method Arguments

We now have the (Pdb) prompt that we saw in an earlier lab. We know that two named arguments are passed to the get_alter method: obj, and request. Let's look at obj first.

(Pdb) obj
*** TypeError: unsupported format string passed to NoneType.__format__
(Pdb) type(obj)
<class 'maintenance_notices.models.MaintenanceNotice'>

We can see that in this case obj is an instance of our MaintenanceNotice model.

Let's look into the request argument. Type dir(request) to see the attributes of the request object that was passed into this method.

(Pdb) dir(request)
['COOKIES', 'FILES', 'GET', 'META', 'POST', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_cached_user', '_cors_enabled', '_current_scheme_host', '_encoding', '_files', '_get_full_path', '_get_post', '_get_raw_host', '_get_scheme', '_initialize_handlers', '_load_post_and_files', '_mark_post_parse_error', '_messages', '_post', '_read_started', '_set_content_type_params', '_set_post', '_stream', '_upload_handlers', 'accepted_types', 'accepts', 'body', 'build_absolute_uri', 'close', 'content_params', 'content_type', 'csrf_processing_done', 'encoding', 'environ', 'get_full_path', 'get_full_path_info', 'get_host', 'get_port', 'get_raw_uri', 'get_signed_cookie', 'headers', 'id', 'is_ajax', 'is_secure', 'method', 'parse_file_upload', 'path', 'path_info', 'prometheus_after_middleware_event', 'prometheus_before_middleware_event', 'read', 'readline', 'readlines', 'resolver_match', 'scheme', 'session', 'upload_handlers', 'user']

Notice the user attribute. Let's see what that is:

(Pdb) request.user
<SimpleLazyObject: <User: admin>>

Great! We now know how to get the authenticated user by calling request.user. We will take advantage of that in the next step. Explore some more if you like. When you are ready to exit the debugger, type c (continue) and hit the Enter or Return key.

(Pdb) c
[24/Apr/2022 20:44:27] "GET /plugins/maintenance_notices/maintenancenotice/add/ HTTP/1.1" 200 251207

Step 4-9 - Assign the User to the Maintenance Notice

Let's make use of our new found knowledge.

Edit the views.py file. Remove the line import pdb; pdb.set_trace() and replace it with obj.created_by = request.user. The obj in this case is an instance of our MaintenanceNotice model, so we can assign the created_by attribute to the request.user object.

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

    def alter_obj(self, obj, request, *args, **kwargs):
        """Insert user into object."""
        obj.created_by = request.user
        return obj

Save the file. This should cause the nautobot-server to reload. Use the web browser to add another Maintenance Notice and validate our work. This time you should see your username in the created_by field.

Screenshot: Maintenance notice created with created_by assigned

Step 4-10 - Commit Your Changes

Remember to commit your changes to git:

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

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