We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Enable plugins to inject content within the top navigation bar
### NetBox version

v4.0.3

### Feature type

New functionality

### Proposed functionality

It is currently possible for a plugin to embed custom content in NetBox core views by employing one or more of the available [extension methods](https://netboxlabs.com/docs/netbox/en/stable/plugins/development/views/#extra-template-content) of the PluginTemplateExtension class. This proposal would add a new method, `navbar()`, to enable a plugin to inject custom content within the top navigation bar, to the left of the user menu (opposite the global search bar).

This will also require slight modification to the logic for [registering template extensions](https://github.com/netbox-community/netbox/blob/05c69f84e69ac4ea8197452a75837e07d032993f/netbox/netbox/plugins/registration.py#L35), which currently requires the association of an extension with a particular NetBox model.

### Use case

It may be desirable for a plugin to embed a notification or control widget of some kind which persists across multiple views.

### Database changes

None

### External dependencies

None

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `netbox/netbox/plugins/templates.py`
Content:
```
1 from django.template.loader import get_template
2 from django.utils.translation import gettext as _
3 
4 __all__ = (
5     'PluginTemplateExtension',
6 )
7 
8 
9 class PluginTemplateExtension:
10     """
11     This class is used to register plugin content to be injected into core NetBox templates. It contains methods
12     that are overridden by plugin authors to return template content.
13 
14     The `model` attribute on the class defines the which model detail page this class renders content for. It
15     should be set as a string in the form '<app_label>.<model_name>'. render() provides the following context data:
16 
17     * object - The object being viewed
18     * request - The current request
19     * settings - Global NetBox settings
20     * config - Plugin-specific configuration parameters
21     """
22     model = None
23 
24     def __init__(self, context):
25         self.context = context
26 
27     def render(self, template_name, extra_context=None):
28         """
29         Convenience method for rendering the specified Django template using the default context data. An additional
30         context dictionary may be passed as `extra_context`.
31         """
32         if extra_context is None:
33             extra_context = {}
34         elif not isinstance(extra_context, dict):
35             raise TypeError(_("extra_context must be a dictionary"))
36 
37         return get_template(template_name).render({**self.context, **extra_context})
38 
39     def left_page(self):
40         """
41         Content that will be rendered on the left of the detail page view. Content should be returned as an
42         HTML string. Note that content does not need to be marked as safe because this is automatically handled.
43         """
44         raise NotImplementedError
45 
46     def right_page(self):
47         """
48         Content that will be rendered on the right of the detail page view. Content should be returned as an
49         HTML string. Note that content does not need to be marked as safe because this is automatically handled.
50         """
51         raise NotImplementedError
52 
53     def full_width_page(self):
54         """
55         Content that will be rendered within the full width of the detail page view. Content should be returned as an
56         HTML string. Note that content does not need to be marked as safe because this is automatically handled.
57         """
58         raise NotImplementedError
59 
60     def buttons(self):
61         """
62         Buttons that will be rendered and added to the existing list of buttons on the detail page view. Content
63         should be returned as an HTML string. Note that content does not need to be marked as safe because this is
64         automatically handled.
65         """
66         raise NotImplementedError
67 
68     def list_buttons(self):
69         """
70         Buttons that will be rendered and added to the existing list of buttons on the list view. Content
71         should be returned as an HTML string. Note that content does not need to be marked as safe because this is
72         automatically handled.
73         """
74         raise NotImplementedError
75
```

Path: `netbox/utilities/templatetags/plugins.py`
Content:
```
1 from django import template as template_
2 from django.conf import settings
3 from django.utils.safestring import mark_safe
4 
5 from netbox.plugins import PluginTemplateExtension
6 from netbox.registry import registry
7 
8 register = template_.Library()
9 
10 
11 def _get_registered_content(obj, method, template_context):
12     """
13     Given an object and a PluginTemplateExtension method name and the template context, return all the
14     registered content for the object's model.
15     """
16     html = ''
17     context = {
18         'object': obj,
19         'request': template_context['request'],
20         'settings': template_context['settings'],
21         'csrf_token': template_context['csrf_token'],
22         'perms': template_context['perms'],
23     }
24 
25     model_name = obj._meta.label_lower
26     template_extensions = registry['plugins']['template_extensions'].get(model_name, [])
27     for template_extension in template_extensions:
28 
29         # If the class has not overridden the specified method, we can skip it (because we know it
30         # will raise NotImplementedError).
31         if getattr(template_extension, method) == getattr(PluginTemplateExtension, method):
32             continue
33 
34         # Update context with plugin-specific configuration parameters
35         plugin_name = template_extension.__module__.split('.')[0]
36         context['config'] = settings.PLUGINS_CONFIG.get(plugin_name, {})
37 
38         # Call the method to render content
39         instance = template_extension(context)
40         content = getattr(instance, method)()
41         html += content
42 
43     return mark_safe(html)
44 
45 
46 @register.simple_tag(takes_context=True)
47 def plugin_buttons(context, obj):
48     """
49     Render all buttons registered by plugins
50     """
51     return _get_registered_content(obj, 'buttons', context)
52 
53 
54 @register.simple_tag(takes_context=True)
55 def plugin_left_page(context, obj):
56     """
57     Render all left page content registered by plugins
58     """
59     return _get_registered_content(obj, 'left_page', context)
60 
61 
62 @register.simple_tag(takes_context=True)
63 def plugin_right_page(context, obj):
64     """
65     Render all right page content registered by plugins
66     """
67     return _get_registered_content(obj, 'right_page', context)
68 
69 
70 @register.simple_tag(takes_context=True)
71 def plugin_full_width_page(context, obj):
72     """
73     Render all full width page content registered by plugins
74     """
75     return _get_registered_content(obj, 'full_width_page', context)
76 
77 
78 @register.simple_tag(takes_context=True)
79 def plugin_list_buttons(context, model):
80     """
81     Render all list buttons registered by plugins
82     """
83     return _get_registered_content(model, 'list_buttons', context)
84
```

Path: `netbox/netbox/plugins/registration.py`
Content:
```
1 import inspect
2 
3 from django.utils.translation import gettext_lazy as _
4 from netbox.registry import registry
5 from .navigation import PluginMenu, PluginMenuButton, PluginMenuItem
6 from .templates import PluginTemplateExtension
7 
8 __all__ = (
9     'register_graphql_schema',
10     'register_menu',
11     'register_menu_items',
12     'register_template_extensions',
13     'register_user_preferences',
14 )
15 
16 
17 def register_template_extensions(class_list):
18     """
19     Register a list of PluginTemplateExtension classes
20     """
21     # Validation
22     for template_extension in class_list:
23         if not inspect.isclass(template_extension):
24             raise TypeError(
25                 _("PluginTemplateExtension class {template_extension} was passed as an instance!").format(
26                     template_extension=template_extension
27                 )
28             )
29         if not issubclass(template_extension, PluginTemplateExtension):
30             raise TypeError(
31                 _("{template_extension} is not a subclass of netbox.plugins.PluginTemplateExtension!").format(
32                     template_extension=template_extension
33                 )
34             )
35         if template_extension.model is None:
36             raise TypeError(
37                 _("PluginTemplateExtension class {template_extension} does not define a valid model!").format(
38                     template_extension=template_extension
39                 )
40             )
41 
42         registry['plugins']['template_extensions'][template_extension.model].append(template_extension)
43 
44 
45 def register_menu(menu):
46     if not isinstance(menu, PluginMenu):
47         raise TypeError(_("{item} must be an instance of netbox.plugins.PluginMenuItem").format(
48             item=menu_link
49         ))
50     registry['plugins']['menus'].append(menu)
51 
52 
53 def register_menu_items(section_name, class_list):
54     """
55     Register a list of PluginMenuItem instances for a given menu section (e.g. plugin name)
56     """
57     # Validation
58     for menu_link in class_list:
59         if not isinstance(menu_link, PluginMenuItem):
60             raise TypeError(_("{menu_link} must be an instance of netbox.plugins.PluginMenuItem").format(
61                 menu_link=menu_link
62             ))
63         for button in menu_link.buttons:
64             if not isinstance(button, PluginMenuButton):
65                 raise TypeError(_("{button} must be an instance of netbox.plugins.PluginMenuButton").format(
66                     button=button
67                 ))
68 
69     registry['plugins']['menu_items'][section_name] = class_list
70 
71 
72 def register_graphql_schema(graphql_schema):
73     """
74     Register a GraphQL schema class for inclusion in NetBox's GraphQL API.
75     """
76     registry['plugins']['graphql_schemas'].extend(graphql_schema)
77 
78 
79 def register_user_preferences(plugin_name, preferences):
80     """
81     Register a list of user preferences defined by a plugin.
82     """
83     registry['plugins']['preferences'][plugin_name] = preferences
84
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/netbox/netbox/plugins/registration.py b/netbox/netbox/plugins/registration.py
--- a/netbox/netbox/plugins/registration.py
+++ b/netbox/netbox/plugins/registration.py
@@ -32,21 +32,13 @@
                     template_extension=template_extension
                 )
             )
-        if template_extension.model is None:
-            raise TypeError(
-                _("PluginTemplateExtension class {template_extension} does not define a valid model!").format(
-                    template_extension=template_extension
-                )
-            )
 
         registry['plugins']['template_extensions'][template_extension.model].append(template_extension)
 
 
 def register_menu(menu):
     if not isinstance(menu, PluginMenu):
-        raise TypeError(_("{item} must be an instance of netbox.plugins.PluginMenuItem").format(
-            item=menu_link
-        ))
+        raise TypeError(_("{item} must be an instance of netbox.plugins.PluginMenuItem").format(item=menu))
     registry['plugins']['menus'].append(menu)
 
 
diff --git a/netbox/netbox/plugins/templates.py b/netbox/netbox/plugins/templates.py
--- a/netbox/netbox/plugins/templates.py
+++ b/netbox/netbox/plugins/templates.py
@@ -14,7 +14,8 @@
     The `model` attribute on the class defines the which model detail page this class renders content for. It
     should be set as a string in the form '<app_label>.<model_name>'. render() provides the following context data:
 
-    * object - The object being viewed
+    * object - The object being viewed (object views only)
+    * model - The type of object being viewed (list views only)
     * request - The current request
     * settings - Global NetBox settings
     * config - Plugin-specific configuration parameters
@@ -36,6 +37,13 @@
 
         return get_template(template_name).render({**self.context, **extra_context})
 
+    def navbar(self):
+        """
+        Content that will be rendered inside the top navigation menu. Content should be returned as an HTML
+        string. Note that content does not need to be marked as safe because this is automatically handled.
+        """
+        raise NotImplementedError
+
     def left_page(self):
         """
         Content that will be rendered on the left of the detail page view. Content should be returned as an
diff --git a/netbox/utilities/templatetags/plugins.py b/netbox/utilities/templatetags/plugins.py
--- a/netbox/utilities/templatetags/plugins.py
+++ b/netbox/utilities/templatetags/plugins.py
@@ -22,7 +22,7 @@
         'perms': template_context['perms'],
     }
 
-    model_name = obj._meta.label_lower
+    model_name = obj._meta.label_lower if obj is not None else None
     template_extensions = registry['plugins']['template_extensions'].get(model_name, [])
     for template_extension in template_extensions:
 
@@ -43,6 +43,14 @@
     return mark_safe(html)
 
 
+@register.simple_tag(takes_context=True)
+def plugin_navbar(context):
+    """
+    Render any navbar content embedded by plugins
+    """
+    return _get_registered_content(None, 'navbar', context)
+
+
 @register.simple_tag(takes_context=True)
 def plugin_buttons(context, obj):
     """

{"golden_diff": "diff --git a/netbox/netbox/plugins/registration.py b/netbox/netbox/plugins/registration.py\n--- a/netbox/netbox/plugins/registration.py\n+++ b/netbox/netbox/plugins/registration.py\n@@ -32,21 +32,13 @@\n                     template_extension=template_extension\n                 )\n             )\n-        if template_extension.model is None:\n-            raise TypeError(\n-                _(\"PluginTemplateExtension class {template_extension} does not define a valid model!\").format(\n-                    template_extension=template_extension\n-                )\n-            )\n \n         registry['plugins']['template_extensions'][template_extension.model].append(template_extension)\n \n \n def register_menu(menu):\n     if not isinstance(menu, PluginMenu):\n-        raise TypeError(_(\"{item} must be an instance of netbox.plugins.PluginMenuItem\").format(\n-            item=menu_link\n-        ))\n+        raise TypeError(_(\"{item} must be an instance of netbox.plugins.PluginMenuItem\").format(item=menu))\n     registry['plugins']['menus'].append(menu)\n \n \ndiff --git a/netbox/netbox/plugins/templates.py b/netbox/netbox/plugins/templates.py\n--- a/netbox/netbox/plugins/templates.py\n+++ b/netbox/netbox/plugins/templates.py\n@@ -14,7 +14,8 @@\n     The `model` attribute on the class defines the which model detail page this class renders content for. It\n     should be set as a string in the form '<app_label>.<model_name>'. render() provides the following context data:\n \n-    * object - The object being viewed\n+    * object - The object being viewed (object views only)\n+    * model - The type of object being viewed (list views only)\n     * request - The current request\n     * settings - Global NetBox settings\n     * config - Plugin-specific configuration parameters\n@@ -36,6 +37,13 @@\n \n         return get_template(template_name).render({**self.context, **extra_context})\n \n+    def navbar(self):\n+        \"\"\"\n+        Content that will be rendered inside the top navigation menu. Content should be returned as an HTML\n+        string. Note that content does not need to be marked as safe because this is automatically handled.\n+        \"\"\"\n+        raise NotImplementedError\n+\n     def left_page(self):\n         \"\"\"\n         Content that will be rendered on the left of the detail page view. Content should be returned as an\ndiff --git a/netbox/utilities/templatetags/plugins.py b/netbox/utilities/templatetags/plugins.py\n--- a/netbox/utilities/templatetags/plugins.py\n+++ b/netbox/utilities/templatetags/plugins.py\n@@ -22,7 +22,7 @@\n         'perms': template_context['perms'],\n     }\n \n-    model_name = obj._meta.label_lower\n+    model_name = obj._meta.label_lower if obj is not None else None\n     template_extensions = registry['plugins']['template_extensions'].get(model_name, [])\n     for template_extension in template_extensions:\n \n@@ -43,6 +43,14 @@\n     return mark_safe(html)\n \n \n+@register.simple_tag(takes_context=True)\n+def plugin_navbar(context):\n+    \"\"\"\n+    Render any navbar content embedded by plugins\n+    \"\"\"\n+    return _get_registered_content(None, 'navbar', context)\n+\n+\n @register.simple_tag(takes_context=True)\n def plugin_buttons(context, obj):\n     \"\"\"\n", "issue": "Enable plugins to inject content within the top navigation bar\n### NetBox version\n\nv4.0.3\n\n### Feature type\n\nNew functionality\n\n### Proposed functionality\n\nIt is currently possible for a plugin to embed custom content in NetBox core views by employing one or more of the available [extension methods](https://netboxlabs.com/docs/netbox/en/stable/plugins/development/views/#extra-template-content) of the PluginTemplateExtension class. This proposal would add a new method, `navbar()`, to enable a plugin to inject custom content within the top navigation bar, to the left of the user menu (opposite the global search bar).\r\n\r\nThis will also require slight modification to the logic for [registering template extensions](https://github.com/netbox-community/netbox/blob/05c69f84e69ac4ea8197452a75837e07d032993f/netbox/netbox/plugins/registration.py#L35), which currently requires the association of an extension with a particular NetBox model.\n\n### Use case\n\nIt may be desirable for a plugin to embed a notification or control widget of some kind which persists across multiple views.\n\n### Database changes\n\nNone\n\n### External dependencies\n\nNone\n", "before_files": [{"content": "from django.template.loader import get_template\nfrom django.utils.translation import gettext as _\n\n__all__ = (\n    'PluginTemplateExtension',\n)\n\n\nclass PluginTemplateExtension:\n    \"\"\"\n    This class is used to register plugin content to be injected into core NetBox templates. It contains methods\n    that are overridden by plugin authors to return template content.\n\n    The `model` attribute on the class defines the which model detail page this class renders content for. It\n    should be set as a string in the form '<app_label>.<model_name>'. render() provides the following context data:\n\n    * object - The object being viewed\n    * request - The current request\n    * settings - Global NetBox settings\n    * config - Plugin-specific configuration parameters\n    \"\"\"\n    model = None\n\n    def __init__(self, context):\n        self.context = context\n\n    def render(self, template_name, extra_context=None):\n        \"\"\"\n        Convenience method for rendering the specified Django template using the default context data. An additional\n        context dictionary may be passed as `extra_context`.\n        \"\"\"\n        if extra_context is None:\n            extra_context = {}\n        elif not isinstance(extra_context, dict):\n            raise TypeError(_(\"extra_context must be a dictionary\"))\n\n        return get_template(template_name).render({**self.context, **extra_context})\n\n    def left_page(self):\n        \"\"\"\n        Content that will be rendered on the left of the detail page view. Content should be returned as an\n        HTML string. Note that content does not need to be marked as safe because this is automatically handled.\n        \"\"\"\n        raise NotImplementedError\n\n    def right_page(self):\n        \"\"\"\n        Content that will be rendered on the right of the detail page view. Content should be returned as an\n        HTML string. Note that content does not need to be marked as safe because this is automatically handled.\n        \"\"\"\n        raise NotImplementedError\n\n    def full_width_page(self):\n        \"\"\"\n        Content that will be rendered within the full width of the detail page view. Content should be returned as an\n        HTML string. Note that content does not need to be marked as safe because this is automatically handled.\n        \"\"\"\n        raise NotImplementedError\n\n    def buttons(self):\n        \"\"\"\n        Buttons that will be rendered and added to the existing list of buttons on the detail page view. Content\n        should be returned as an HTML string. Note that content does not need to be marked as safe because this is\n        automatically handled.\n        \"\"\"\n        raise NotImplementedError\n\n    def list_buttons(self):\n        \"\"\"\n        Buttons that will be rendered and added to the existing list of buttons on the list view. Content\n        should be returned as an HTML string. Note that content does not need to be marked as safe because this is\n        automatically handled.\n        \"\"\"\n        raise NotImplementedError\n", "path": "netbox/netbox/plugins/templates.py"}, {"content": "from django import template as template_\nfrom django.conf import settings\nfrom django.utils.safestring import mark_safe\n\nfrom netbox.plugins import PluginTemplateExtension\nfrom netbox.registry import registry\n\nregister = template_.Library()\n\n\ndef _get_registered_content(obj, method, template_context):\n    \"\"\"\n    Given an object and a PluginTemplateExtension method name and the template context, return all the\n    registered content for the object's model.\n    \"\"\"\n    html = ''\n    context = {\n        'object': obj,\n        'request': template_context['request'],\n        'settings': template_context['settings'],\n        'csrf_token': template_context['csrf_token'],\n        'perms': template_context['perms'],\n    }\n\n    model_name = obj._meta.label_lower\n    template_extensions = registry['plugins']['template_extensions'].get(model_name, [])\n    for template_extension in template_extensions:\n\n        # If the class has not overridden the specified method, we can skip it (because we know it\n        # will raise NotImplementedError).\n        if getattr(template_extension, method) == getattr(PluginTemplateExtension, method):\n            continue\n\n        # Update context with plugin-specific configuration parameters\n        plugin_name = template_extension.__module__.split('.')[0]\n        context['config'] = settings.PLUGINS_CONFIG.get(plugin_name, {})\n\n        # Call the method to render content\n        instance = template_extension(context)\n        content = getattr(instance, method)()\n        html += content\n\n    return mark_safe(html)\n\n\n@register.simple_tag(takes_context=True)\ndef plugin_buttons(context, obj):\n    \"\"\"\n    Render all buttons registered by plugins\n    \"\"\"\n    return _get_registered_content(obj, 'buttons', context)\n\n\n@register.simple_tag(takes_context=True)\ndef plugin_left_page(context, obj):\n    \"\"\"\n    Render all left page content registered by plugins\n    \"\"\"\n    return _get_registered_content(obj, 'left_page', context)\n\n\n@register.simple_tag(takes_context=True)\ndef plugin_right_page(context, obj):\n    \"\"\"\n    Render all right page content registered by plugins\n    \"\"\"\n    return _get_registered_content(obj, 'right_page', context)\n\n\n@register.simple_tag(takes_context=True)\ndef plugin_full_width_page(context, obj):\n    \"\"\"\n    Render all full width page content registered by plugins\n    \"\"\"\n    return _get_registered_content(obj, 'full_width_page', context)\n\n\n@register.simple_tag(takes_context=True)\ndef plugin_list_buttons(context, model):\n    \"\"\"\n    Render all list buttons registered by plugins\n    \"\"\"\n    return _get_registered_content(model, 'list_buttons', context)\n", "path": "netbox/utilities/templatetags/plugins.py"}, {"content": "import inspect\n\nfrom django.utils.translation import gettext_lazy as _\nfrom netbox.registry import registry\nfrom .navigation import PluginMenu, PluginMenuButton, PluginMenuItem\nfrom .templates import PluginTemplateExtension\n\n__all__ = (\n    'register_graphql_schema',\n    'register_menu',\n    'register_menu_items',\n    'register_template_extensions',\n    'register_user_preferences',\n)\n\n\ndef register_template_extensions(class_list):\n    \"\"\"\n    Register a list of PluginTemplateExtension classes\n    \"\"\"\n    # Validation\n    for template_extension in class_list:\n        if not inspect.isclass(template_extension):\n            raise TypeError(\n                _(\"PluginTemplateExtension class {template_extension} was passed as an instance!\").format(\n                    template_extension=template_extension\n                )\n            )\n        if not issubclass(template_extension, PluginTemplateExtension):\n            raise TypeError(\n                _(\"{template_extension} is not a subclass of netbox.plugins.PluginTemplateExtension!\").format(\n                    template_extension=template_extension\n                )\n            )\n        if template_extension.model is None:\n            raise TypeError(\n                _(\"PluginTemplateExtension class {template_extension} does not define a valid model!\").format(\n                    template_extension=template_extension\n                )\n            )\n\n        registry['plugins']['template_extensions'][template_extension.model].append(template_extension)\n\n\ndef register_menu(menu):\n    if not isinstance(menu, PluginMenu):\n        raise TypeError(_(\"{item} must be an instance of netbox.plugins.PluginMenuItem\").format(\n            item=menu_link\n        ))\n    registry['plugins']['menus'].append(menu)\n\n\ndef register_menu_items(section_name, class_list):\n    \"\"\"\n    Register a list of PluginMenuItem instances for a given menu section (e.g. plugin name)\n    \"\"\"\n    # Validation\n    for menu_link in class_list:\n        if not isinstance(menu_link, PluginMenuItem):\n            raise TypeError(_(\"{menu_link} must be an instance of netbox.plugins.PluginMenuItem\").format(\n                menu_link=menu_link\n            ))\n        for button in menu_link.buttons:\n            if not isinstance(button, PluginMenuButton):\n                raise TypeError(_(\"{button} must be an instance of netbox.plugins.PluginMenuButton\").format(\n                    button=button\n                ))\n\n    registry['plugins']['menu_items'][section_name] = class_list\n\n\ndef register_graphql_schema(graphql_schema):\n    \"\"\"\n    Register a GraphQL schema class for inclusion in NetBox's GraphQL API.\n    \"\"\"\n    registry['plugins']['graphql_schemas'].extend(graphql_schema)\n\n\ndef register_user_preferences(plugin_name, preferences):\n    \"\"\"\n    Register a list of user preferences defined by a plugin.\n    \"\"\"\n    registry['plugins']['preferences'][plugin_name] = preferences\n", "path": "netbox/netbox/plugins/registration.py"}], "after_files": [{"content": "from django.template.loader import get_template\nfrom django.utils.translation import gettext as _\n\n__all__ = (\n    'PluginTemplateExtension',\n)\n\n\nclass PluginTemplateExtension:\n    \"\"\"\n    This class is used to register plugin content to be injected into core NetBox templates. It contains methods\n    that are overridden by plugin authors to return template content.\n\n    The `model` attribute on the class defines the which model detail page this class renders content for. It\n    should be set as a string in the form '<app_label>.<model_name>'. render() provides the following context data:\n\n    * object - The object being viewed (object views only)\n    * model - The type of object being viewed (list views only)\n    * request - The current request\n    * settings - Global NetBox settings\n    * config - Plugin-specific configuration parameters\n    \"\"\"\n    model = None\n\n    def __init__(self, context):\n        self.context = context\n\n    def render(self, template_name, extra_context=None):\n        \"\"\"\n        Convenience method for rendering the specified Django template using the default context data. An additional\n        context dictionary may be passed as `extra_context`.\n        \"\"\"\n        if extra_context is None:\n            extra_context = {}\n        elif not isinstance(extra_context, dict):\n            raise TypeError(_(\"extra_context must be a dictionary\"))\n\n        return get_template(template_name).render({**self.context, **extra_context})\n\n    def navbar(self):\n        \"\"\"\n        Content that will be rendered inside the top navigation menu. Content should be returned as an HTML\n        string. Note that content does not need to be marked as safe because this is automatically handled.\n        \"\"\"\n        raise NotImplementedError\n\n    def left_page(self):\n        \"\"\"\n        Content that will be rendered on the left of the detail page view. Content should be returned as an\n        HTML string. Note that content does not need to be marked as safe because this is automatically handled.\n        \"\"\"\n        raise NotImplementedError\n\n    def right_page(self):\n        \"\"\"\n        Content that will be rendered on the right of the detail page view. Content should be returned as an\n        HTML string. Note that content does not need to be marked as safe because this is automatically handled.\n        \"\"\"\n        raise NotImplementedError\n\n    def full_width_page(self):\n        \"\"\"\n        Content that will be rendered within the full width of the detail page view. Content should be returned as an\n        HTML string. Note that content does not need to be marked as safe because this is automatically handled.\n        \"\"\"\n        raise NotImplementedError\n\n    def buttons(self):\n        \"\"\"\n        Buttons that will be rendered and added to the existing list of buttons on the detail page view. Content\n        should be returned as an HTML string. Note that content does not need to be marked as safe because this is\n        automatically handled.\n        \"\"\"\n        raise NotImplementedError\n\n    def list_buttons(self):\n        \"\"\"\n        Buttons that will be rendered and added to the existing list of buttons on the list view. Content\n        should be returned as an HTML string. Note that content does not need to be marked as safe because this is\n        automatically handled.\n        \"\"\"\n        raise NotImplementedError\n", "path": "netbox/netbox/plugins/templates.py"}, {"content": "from django import template as template_\nfrom django.conf import settings\nfrom django.utils.safestring import mark_safe\n\nfrom netbox.plugins import PluginTemplateExtension\nfrom netbox.registry import registry\n\nregister = template_.Library()\n\n\ndef _get_registered_content(obj, method, template_context):\n    \"\"\"\n    Given an object and a PluginTemplateExtension method name and the template context, return all the\n    registered content for the object's model.\n    \"\"\"\n    html = ''\n    context = {\n        'object': obj,\n        'request': template_context['request'],\n        'settings': template_context['settings'],\n        'csrf_token': template_context['csrf_token'],\n        'perms': template_context['perms'],\n    }\n\n    model_name = obj._meta.label_lower if obj is not None else None\n    template_extensions = registry['plugins']['template_extensions'].get(model_name, [])\n    for template_extension in template_extensions:\n\n        # If the class has not overridden the specified method, we can skip it (because we know it\n        # will raise NotImplementedError).\n        if getattr(template_extension, method) == getattr(PluginTemplateExtension, method):\n            continue\n\n        # Update context with plugin-specific configuration parameters\n        plugin_name = template_extension.__module__.split('.')[0]\n        context['config'] = settings.PLUGINS_CONFIG.get(plugin_name, {})\n\n        # Call the method to render content\n        instance = template_extension(context)\n        content = getattr(instance, method)()\n        html += content\n\n    return mark_safe(html)\n\n\n@register.simple_tag(takes_context=True)\ndef plugin_navbar(context):\n    \"\"\"\n    Render any navbar content embedded by plugins\n    \"\"\"\n    return _get_registered_content(None, 'navbar', context)\n\n\n@register.simple_tag(takes_context=True)\ndef plugin_buttons(context, obj):\n    \"\"\"\n    Render all buttons registered by plugins\n    \"\"\"\n    return _get_registered_content(obj, 'buttons', context)\n\n\n@register.simple_tag(takes_context=True)\ndef plugin_left_page(context, obj):\n    \"\"\"\n    Render all left page content registered by plugins\n    \"\"\"\n    return _get_registered_content(obj, 'left_page', context)\n\n\n@register.simple_tag(takes_context=True)\ndef plugin_right_page(context, obj):\n    \"\"\"\n    Render all right page content registered by plugins\n    \"\"\"\n    return _get_registered_content(obj, 'right_page', context)\n\n\n@register.simple_tag(takes_context=True)\ndef plugin_full_width_page(context, obj):\n    \"\"\"\n    Render all full width page content registered by plugins\n    \"\"\"\n    return _get_registered_content(obj, 'full_width_page', context)\n\n\n@register.simple_tag(takes_context=True)\ndef plugin_list_buttons(context, model):\n    \"\"\"\n    Render all list buttons registered by plugins\n    \"\"\"\n    return _get_registered_content(model, 'list_buttons', context)\n", "path": "netbox/utilities/templatetags/plugins.py"}, {"content": "import inspect\n\nfrom django.utils.translation import gettext_lazy as _\nfrom netbox.registry import registry\nfrom .navigation import PluginMenu, PluginMenuButton, PluginMenuItem\nfrom .templates import PluginTemplateExtension\n\n__all__ = (\n    'register_graphql_schema',\n    'register_menu',\n    'register_menu_items',\n    'register_template_extensions',\n    'register_user_preferences',\n)\n\n\ndef register_template_extensions(class_list):\n    \"\"\"\n    Register a list of PluginTemplateExtension classes\n    \"\"\"\n    # Validation\n    for template_extension in class_list:\n        if not inspect.isclass(template_extension):\n            raise TypeError(\n                _(\"PluginTemplateExtension class {template_extension} was passed as an instance!\").format(\n                    template_extension=template_extension\n                )\n            )\n        if not issubclass(template_extension, PluginTemplateExtension):\n            raise TypeError(\n                _(\"{template_extension} is not a subclass of netbox.plugins.PluginTemplateExtension!\").format(\n                    template_extension=template_extension\n                )\n            )\n\n        registry['plugins']['template_extensions'][template_extension.model].append(template_extension)\n\n\ndef register_menu(menu):\n    if not isinstance(menu, PluginMenu):\n        raise TypeError(_(\"{item} must be an instance of netbox.plugins.PluginMenuItem\").format(item=menu))\n    registry['plugins']['menus'].append(menu)\n\n\ndef register_menu_items(section_name, class_list):\n    \"\"\"\n    Register a list of PluginMenuItem instances for a given menu section (e.g. plugin name)\n    \"\"\"\n    # Validation\n    for menu_link in class_list:\n        if not isinstance(menu_link, PluginMenuItem):\n            raise TypeError(_(\"{menu_link} must be an instance of netbox.plugins.PluginMenuItem\").format(\n                menu_link=menu_link\n            ))\n        for button in menu_link.buttons:\n            if not isinstance(button, PluginMenuButton):\n                raise TypeError(_(\"{button} must be an instance of netbox.plugins.PluginMenuButton\").format(\n                    button=button\n                ))\n\n    registry['plugins']['menu_items'][section_name] = class_list\n\n\ndef register_graphql_schema(graphql_schema):\n    \"\"\"\n    Register a GraphQL schema class for inclusion in NetBox's GraphQL API.\n    \"\"\"\n    registry['plugins']['graphql_schemas'].extend(graphql_schema)\n\n\ndef register_user_preferences(plugin_name, preferences):\n    \"\"\"\n    Register a list of user preferences defined by a plugin.\n    \"\"\"\n    registry['plugins']['preferences'][plugin_name] = preferences\n", "path": "netbox/netbox/plugins/registration.py"}]}