Jeremy Stretch 3 месяцев назад
Родитель
Сommit
59899d0d9a

+ 1 - 1
netbox/dcim/views.py

@@ -1202,7 +1202,7 @@ class RackReservationView(generic.ObjectView):
     layout = layout.Layout(
         layout.Row(
             layout.Column(
-                panels.RackPanel(accessor='rack', only=['region', 'site', 'location']),
+                panels.RackPanel(title=_('Rack'), accessor='rack', only=['region', 'site', 'location']),
                 CustomFieldsPanel(),
                 TagsPanel(),
                 CommentsPanel(),

+ 64 - 9
netbox/netbox/ui/actions.py

@@ -1,6 +1,7 @@
 from urllib.parse import urlencode
 
 from django.apps import apps
+from django.template.loader import render_to_string
 from django.urls import reverse
 from django.utils.translation import gettext_lazy as _
 
@@ -14,48 +15,102 @@ __all__ = (
 
 
 class PanelAction:
+    """
+    A link (typically a button) within a panel to perform some associated action, such as adding an object.
+
+    Attributes:
+        template_name: The name of the template to render
+        label: The default human-friendly button text
+        button_class: Bootstrap CSS class for the button
+        button_icon: Name of the button's MDI icon
+    """
+    template_name = 'ui/action.html'
     label = None
     button_class = 'primary'
     button_icon = None
 
     def __init__(self, view_name, view_kwargs=None, url_params=None, permissions=None, label=None):
+        """
+        Initialize a new PanelAction.
+
+        Parameters:
+            view_name: Name of the view to which the action will link
+            view_kwargs: Additional keyword arguments to pass to the view when resolving its URL
+            url_params: A dictionary of arbitrary URL parameters to append to the action's URL
+            permissions: A list of permissions required to display the action
+            label: The human-friendly button text
+        """
         self.view_name = view_name
-        self.view_kwargs = view_kwargs
+        self.view_kwargs = view_kwargs or {}
         self.url_params = url_params or {}
         self.permissions = permissions
         if label is not None:
             self.label = label
 
     def get_url(self, context):
-        url = reverse(self.view_name, kwargs=self.view_kwargs or {})
+        """
+        Resolve the URL for the action from its view name and kwargs. Append any additional URL parameters.
+
+        Parameters:
+            context: The template context
+        """
+        url = reverse(self.view_name, kwargs=self.view_kwargs)
         if self.url_params:
+            # If the param value is callable, call it with the context and save the result.
             url_params = {
                 k: v(context) if callable(v) else v for k, v in self.url_params.items()
             }
+            # Set the return URL if not already set and an object is available.
             if 'return_url' not in url_params and 'object' in context:
                 url_params['return_url'] = context['object'].get_absolute_url()
             url = f'{url}?{urlencode(url_params)}'
         return url
 
-    def get_context(self, context):
-        return {
+    def render(self, context):
+        """
+        Render the action as HTML.
+
+        Parameters:
+            context: The template context
+        """
+        # Enforce permissions
+        user = context['request'].user
+        if not user.has_perms(self.permissions):
+            return ''
+
+        return render_to_string(self.template_name, {
             'url': self.get_url(context),
             'label': self.label,
             'button_class': self.button_class,
             'button_icon': self.button_icon,
-        }
+        })
 
 
 class AddObject(PanelAction):
+    """
+    An action to add a new object.
+    """
     label = _('Add')
     button_icon = 'plus-thick'
 
-    def __init__(self, model, label=None, url_params=None):
+    def __init__(self, model, url_params=None, label=None):
+        """
+        Initialize a new AddObject action.
+
+        Parameters:
+            model: The dotted label of the model to be added (e.g. "dcim.site")
+            url_params: A dictionary of arbitrary URL parameters to append to the resolved URL
+            label: The human-friendly button text
+        """
         # Resolve the model class from its app.name label
-        app_label, model_name = model.split('.')
-        model = apps.get_model(app_label, model_name)
+        try:
+            app_label, model_name = model.split('.')
+            model = apps.get_model(app_label, model_name)
+        except (ValueError, LookupError):
+            raise ValueError(f"Invalid model label: {model}")
         view_name = get_viewname(model, 'add')
+
         super().__init__(view_name=view_name, label=label, url_params=url_params)
 
-        # Require "add" permission on the model by default
+        # Require "add" permission on the model
         self.permissions = [get_permission_for_model(model, 'add')]

+ 109 - 7
netbox/netbox/ui/panels.py

@@ -24,24 +24,52 @@ __all__ = (
 
 
 class Panel(ABC):
+    """
+    A block of content rendered within an HTML template.
+
+    Attributes:
+        template_name: The name of the template to render
+        title: The human-friendly title of the panel
+        actions: A list of PanelActions to include in the panel header
+    """
     template_name = None
     title = None
     actions = []
 
     def __init__(self, title=None, actions=None):
+        """
+        Instantiate a new Panel.
+
+        Parameters:
+            title: The human-friendly title of the panel
+            actions: A list of PanelActions to include in the panel header
+        """
         if title is not None:
             self.title = title
         if actions is not None:
             self.actions = actions
 
     def get_context(self, context):
+        """
+        Return the context data to be used when rendering the panel.
+
+        Parameters:
+            context: The template context
+        """
         return {
             'request': context.get('request'),
+            'object': context.get('object'),
             'title': self.title,
-            'actions': [action.get_context(context) for action in self.actions],
+            'actions': self.actions,
         }
 
     def render(self, context):
+        """
+        Render the panel as HTML.
+
+        Parameters:
+            context: The template context
+        """
         return render_to_string(self.template_name, self.get_context(context))
 
 
@@ -73,21 +101,43 @@ class ObjectPanelMeta(ABCMeta):
 
 
 class ObjectPanel(Panel, metaclass=ObjectPanelMeta):
-    accessor = None
+    """
+    A panel which displays selected attributes of an object.
+
+    Attributes:
+        template_name: The name of the template to render
+        accessor: The name of the attribute on the object
+    """
     template_name = 'ui/panels/object.html'
+    accessor = None
 
     def __init__(self, accessor=None, only=None, exclude=None, **kwargs):
+        """
+        Instantiate a new ObjectPanel.
+
+        Parameters:
+            accessor: The name of the attribute on the object
+            only: If specified, only attributes in this list will be displayed
+            exclude: If specified, attributes in this list will be excluded from display
+        """
         super().__init__(**kwargs)
+
         if accessor is not None:
             self.accessor = accessor
 
         # Set included/excluded attributes
         if only is not None and exclude is not None:
-            raise ValueError("attrs and exclude cannot both be specified.")
+            raise ValueError("only and exclude cannot both be specified.")
         self.only = only or []
         self.exclude = exclude or []
 
     def get_context(self, context):
+        """
+        Return the context data to be used when rendering the panel.
+
+        Parameters:
+            context: The template context
+        """
         # Determine which attributes to display in the panel based on only/exclude args
         attr_names = set(self._attrs.keys())
         if self.only:
@@ -99,7 +149,6 @@ class ObjectPanel(Panel, metaclass=ObjectPanelMeta):
 
         return {
             **super().get_context(context),
-            'object': obj,
             'attrs': [
                 {
                     'label': attr.label or title(name),
@@ -110,24 +159,42 @@ class ObjectPanel(Panel, metaclass=ObjectPanelMeta):
 
 
 class OrganizationalObjectPanel(ObjectPanel, metaclass=ObjectPanelMeta):
+    """
+    An ObjectPanel with attributes common to OrganizationalModels.
+    """
     name = attrs.TextAttr('name', label=_('Name'))
     description = attrs.TextAttr('description', label=_('Description'))
 
 
 class NestedGroupObjectPanel(OrganizationalObjectPanel, metaclass=ObjectPanelMeta):
+    """
+    An ObjectPanel with attributes common to NestedGroupObjects.
+    """
     parent = attrs.NestedObjectAttr('parent', label=_('Parent'), linkify=True)
 
 
 class CommentsPanel(Panel):
+    """
+    A panel which displays comments associated with an object.
+    """
     template_name = 'ui/panels/comments.html'
     title = _('Comments')
 
 
 class RelatedObjectsPanel(Panel):
+    """
+    A panel which displays the types and counts of related objects.
+    """
     template_name = 'ui/panels/related_objects.html'
     title = _('Related Objects')
 
     def get_context(self, context):
+        """
+        Return the context data to be used when rendering the panel.
+
+        Parameters:
+            context: The template context
+        """
         return {
             **super().get_context(context),
             'related_models': context.get('related_models'),
@@ -135,20 +202,42 @@ class RelatedObjectsPanel(Panel):
 
 
 class ObjectsTablePanel(Panel):
+    """
+    A panel which displays a table of objects (rendered via HTMX).
+    """
     template_name = 'ui/panels/objects_table.html'
     title = None
 
     def __init__(self, model, filters=None, **kwargs):
+        """
+        Instantiate a new ObjectsTablePanel.
+
+        Parameters:
+            model: The dotted label of the model to be added (e.g. "dcim.site")
+            filters: A dictionary of arbitrary URL parameters to append to the table's URL
+        """
         super().__init__(**kwargs)
 
         # Resolve the model class from its app.name label
-        app_label, model_name = model.split('.')
-        self.model = apps.get_model(app_label, model_name)
+        try:
+            app_label, model_name = model.split('.')
+            self.model = apps.get_model(app_label, model_name)
+        except (ValueError, LookupError):
+            raise ValueError(f"Invalid model label: {model}")
+
         self.filters = filters or {}
+
+        # If no title is specified, derive one from the model name
         if self.title is None:
             self.title = title(self.model._meta.verbose_name_plural)
 
     def get_context(self, context):
+        """
+        Return the context data to be used when rendering the panel.
+
+        Parameters:
+            context: The template context
+        """
         url_params = {
             k: v(context) if callable(v) else v for k, v in self.filters.items()
         }
@@ -162,8 +251,16 @@ class ObjectsTablePanel(Panel):
 
 
 class TemplatePanel(Panel):
-
+    """
+    A panel which renders content using an HTML template.
+    """
     def __init__(self, template_name, **kwargs):
+        """
+        Instantiate a new TemplatePanel.
+
+        Parameters:
+            template_name: The name of the template to render
+        """
         super().__init__(**kwargs)
         self.template_name = template_name
 
@@ -173,7 +270,12 @@ class TemplatePanel(Panel):
 
 
 class PluginContentPanel(Panel):
+    """
+    A panel which displays embedded plugin content.
 
+    Parameters:
+        method: The name of the plugin method to render (e.g. left_page)
+    """
     def __init__(self, method, **kwargs):
         super().__init__(**kwargs)
         self.method = method

+ 1 - 1
netbox/templates/generic/object.html

@@ -129,7 +129,7 @@ Context:
       {% for column in row.columns %}
         <div class="col">
           {% for panel in column.panels %}
-            {% render_panel panel %}
+            {% render panel %}
           {% endfor %}
         </div>
       {% endfor %}

+ 6 - 0
netbox/templates/ui/action.html

@@ -0,0 +1,6 @@
+<a href="{{ url }}" class="btn btn-ghost-{{ button_class }} btn-sm">
+  {% if button_icon %}
+    <i class="mdi mdi-{{ button_icon }}" aria-hidden="true"></i>
+  {% endif %}
+  {{ label }}
+</a>

+ 1 - 6
netbox/templates/ui/panels/_base.html

@@ -4,12 +4,7 @@
     {% if actions %}
       <div class="card-actions">
         {% for action in actions %}
-          <a href="{{ action.url }}" class="btn btn-ghost-{{ action.button_class|default:"primary" }} btn-sm">
-            {% if action.button_icon %}
-              <i class="mdi mdi-{{ action.button_icon }}" aria-hidden="true"></i>
-            {% endif %}
-            {{ action.label }}
-          </a>
+          {% render action %}
         {% endfor %}
       </div>
     {% endif %}

+ 2 - 2
netbox/utilities/templatetags/builtins/tags.py

@@ -183,5 +183,5 @@ def static_with_params(path, **params):
 
 
 @register.simple_tag(takes_context=True)
-def render_panel(context, panel):
-    return mark_safe(panel.render(context))
+def render(context, component):
+    return mark_safe(component.render(context))