Просмотр исходного кода

Implement layout declaration under view

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

+ 6 - 6
netbox/dcim/ui/panels.py

@@ -1,9 +1,9 @@
 from django.utils.translation import gettext_lazy as _
 
-from netbox.ui import attrs, components
+from netbox.ui import attrs, panels
 
 
-class SitePanel(components.ObjectPanel):
+class SitePanel(panels.ObjectPanel):
     region = attrs.NestedObjectAttr('region', label=_('Region'), linkify=True)
     group = attrs.NestedObjectAttr('group', label=_('Group'), linkify=True)
     status = attrs.ChoiceAttr('status', label=_('Status'))
@@ -16,14 +16,14 @@ class SitePanel(components.ObjectPanel):
     gps_coordinates = attrs.GPSCoordinatesAttr()
 
 
-class LocationPanel(components.NestedGroupObjectPanel):
+class LocationPanel(panels.NestedGroupObjectPanel):
     site = attrs.ObjectAttr('site', label=_('Site'), linkify=True, grouped_by='group')
     status = attrs.ChoiceAttr('status', label=_('Status'))
     tenant = attrs.ObjectAttr('tenant', label=_('Tenant'), linkify=True, grouped_by='group')
     facility = attrs.TextAttr('facility', label=_('Facility'))
 
 
-class RackPanel(components.ObjectPanel):
+class RackPanel(panels.ObjectPanel):
     region = attrs.NestedObjectAttr('site.region', label=_('Region'), linkify=True)
     site = attrs.ObjectAttr('site', label=_('Site'), linkify=True, grouped_by='group')
     location = attrs.NestedObjectAttr('location', label=_('Location'), linkify=True)
@@ -40,7 +40,7 @@ class RackPanel(components.ObjectPanel):
     power_utilization = attrs.UtilizationAttr('get_power_utilization', label=_('Power utilization'))
 
 
-class DevicePanel(components.ObjectPanel):
+class DevicePanel(panels.ObjectPanel):
     region = attrs.NestedObjectAttr('site.region', label=_('Region'), linkify=True)
     site = attrs.ObjectAttr('site', label=_('Site'), linkify=True, grouped_by='group')
     location = attrs.NestedObjectAttr('location', label=_('Location'), linkify=True)
@@ -61,7 +61,7 @@ class DevicePanel(components.ObjectPanel):
     config_template = attrs.ObjectAttr('config_template', label=_('Config template'), linkify=True)
 
 
-class DeviceManagementPanel(components.ObjectPanel):
+class DeviceManagementPanel(panels.ObjectPanel):
     status = attrs.ChoiceAttr('status', label=_('Status'))
     role = attrs.NestedObjectAttr('role', label=_('Role'), linkify=True, max_depth=3)
     platform = attrs.NestedObjectAttr('platform', label=_('Platform'), linkify=True, max_depth=3)

+ 11 - 4
netbox/dcim/views.py

@@ -17,7 +17,7 @@ from extras.views import ObjectConfigContextView, ObjectRenderConfigView
 from ipam.models import ASN, IPAddress, Prefix, VLANGroup, VLAN
 from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
 from netbox.object_actions import *
-from netbox.ui.components import NestedGroupObjectPanel
+from netbox.ui import layout
 from netbox.views import generic
 from utilities.forms import ConfirmationForm
 from utilities.paginator import EnhancedPaginator, get_paginate_count
@@ -228,7 +228,7 @@ class RegionView(GetRelatedModelsMixin, generic.ObjectView):
         regions = instance.get_descendants(include_self=True)
 
         return {
-            'region_panel': NestedGroupObjectPanel(instance, _('Region')),
+            'region_panel': panels.NestedGroupObjectPanel(instance, _('Region')),
             'related_models': self.get_related_models(
                 request,
                 regions,
@@ -340,7 +340,7 @@ class SiteGroupView(GetRelatedModelsMixin, generic.ObjectView):
         groups = instance.get_descendants(include_self=True)
 
         return {
-            'sitegroup_panel': NestedGroupObjectPanel(instance, _('Site Group')),
+            'sitegroup_panel': panels.NestedGroupObjectPanel(instance, _('Site Group')),
             'related_models': self.get_related_models(
                 request,
                 groups,
@@ -465,10 +465,17 @@ class SiteListView(generic.ObjectListView):
 @register_model_view(Site)
 class SiteView(GetRelatedModelsMixin, generic.ObjectView):
     queryset = Site.objects.prefetch_related('tenant__group')
+    layout = layout.Layout(
+        layout.Row(
+            layout.Column(
+                panels.SitePanel(_('Site'))
+            ),
+        )
+    )
 
     def get_extra_context(self, request, instance):
         return {
-            'site_panel': panels.SitePanel(instance, _('Site')),
+            # 'site_panel': panels.SitePanel(instance, _('Site')),
             'related_models': self.get_related_models(
                 request,
                 instance,

+ 8 - 8
netbox/netbox/ui/attrs.py

@@ -35,7 +35,7 @@ class Attr(ABC):
 
 
 class TextAttr(Attr):
-    template_name = 'components/attrs/text.html'
+    template_name = 'ui/attrs/text.html'
 
     def __init__(self, *args, style=None, copy_button=False, **kwargs):
         super().__init__(*args, **kwargs)
@@ -56,7 +56,7 @@ class TextAttr(Attr):
 
 
 class ChoiceAttr(Attr):
-    template_name = 'components/attrs/choice.html'
+    template_name = 'ui/attrs/choice.html'
 
     def render(self, obj, context=None):
         context = context or {}
@@ -78,7 +78,7 @@ class ChoiceAttr(Attr):
 
 
 class ObjectAttr(Attr):
-    template_name = 'components/attrs/object.html'
+    template_name = 'ui/attrs/object.html'
 
     def __init__(self, *args, linkify=None, grouped_by=None, **kwargs):
         super().__init__(*args, **kwargs)
@@ -101,7 +101,7 @@ class ObjectAttr(Attr):
 
 
 class NestedObjectAttr(Attr):
-    template_name = 'components/attrs/nested_object.html'
+    template_name = 'ui/attrs/nested_object.html'
 
     def __init__(self, *args, linkify=None, max_depth=None, **kwargs):
         super().__init__(*args, **kwargs)
@@ -124,7 +124,7 @@ class NestedObjectAttr(Attr):
 
 
 class AddressAttr(Attr):
-    template_name = 'components/attrs/address.html'
+    template_name = 'ui/attrs/address.html'
 
     def __init__(self, *args, map_url=True, **kwargs):
         super().__init__(*args, **kwargs)
@@ -148,7 +148,7 @@ class AddressAttr(Attr):
 
 
 class GPSCoordinatesAttr(Attr):
-    template_name = 'components/attrs/gps_coordinates.html'
+    template_name = 'ui/attrs/gps_coordinates.html'
 
     def __init__(self, latitude_attr='latitude', longitude_attr='longitude', map_url=True, **kwargs):
         kwargs.setdefault('label', _('GPS Coordinates'))
@@ -177,7 +177,7 @@ class GPSCoordinatesAttr(Attr):
 
 
 class TimezoneAttr(Attr):
-    template_name = 'components/attrs/timezone.html'
+    template_name = 'ui/attrs/timezone.html'
 
     def render(self, obj, context=None):
         context = context or {}
@@ -213,7 +213,7 @@ class TemplatedAttr(Attr):
 
 
 class UtilizationAttr(Attr):
-    template_name = 'components/attrs/utilization.html'
+    template_name = 'ui/attrs/utilization.html'
 
     def render(self, obj, context=None):
         context = context or {}

+ 44 - 0
netbox/netbox/ui/layout.py

@@ -0,0 +1,44 @@
+from netbox.ui.panels import Panel
+
+__all__ = (
+    'Column',
+    'Layout',
+    'Row',
+)
+
+
+class Layout:
+
+    def __init__(self, *rows):
+        for i, row in enumerate(rows):
+            if type(row) is not Row:
+                raise TypeError(f"Row {i} must be a Row instance, not {type(row)}.")
+        self.rows = rows
+
+    def render(self, context):
+        return ''.join([row.render(context) for row in self.rows])
+
+
+class Row:
+    template_name = 'ui/layout/row.html'
+
+    def __init__(self, *columns):
+        for i, column in enumerate(columns):
+            if type(column) is not Column:
+                raise TypeError(f"Column {i} must be a Column instance, not {type(column)}.")
+        self.columns = columns
+
+    def render(self, context):
+        return ''.join([column.render(context) for column in self.columns])
+
+
+class Column:
+
+    def __init__(self, *panels):
+        for i, panel in enumerate(panels):
+            if not isinstance(panel, Panel):
+                raise TypeError(f"Panel {i} must be an instance of a Panel, not {type(panel)}.")
+        self.panels = panels
+
+    def render(self, context):
+        return ''.join([panel.render(context) for panel in self.panels])

+ 19 - 21
netbox/netbox/ui/components.py → netbox/netbox/ui/panels.py

@@ -1,5 +1,4 @@
 from abc import ABC, ABCMeta, abstractmethod
-from functools import cached_property
 
 from django.template.loader import render_to_string
 from django.utils.translation import gettext_lazy as _
@@ -8,18 +7,21 @@ from netbox.ui import attrs
 from netbox.ui.attrs import Attr
 from utilities.string import title
 
+__all__ = (
+    'NestedGroupObjectPanel',
+    'ObjectPanel',
+    'Panel',
+)
 
-class Component(ABC):
+
+class Panel(ABC):
 
     @abstractmethod
-    def render(self):
+    def render(self, obj):
         pass
 
-    def __str__(self):
-        return self.render()
-
 
-class ObjectDetailsPanelMeta(ABCMeta):
+class ObjectPanelMeta(ABCMeta):
 
     def __new__(mcls, name, bases, namespace, **kwargs):
         declared = {}
@@ -46,33 +48,29 @@ class ObjectDetailsPanelMeta(ABCMeta):
         return cls
 
 
-class ObjectPanel(Component, metaclass=ObjectDetailsPanelMeta):
-    template_name = 'components/object_details_panel.html'
+class ObjectPanel(Panel, metaclass=ObjectPanelMeta):
+    template_name = 'ui/panels/object.html'
 
-    def __init__(self, obj, title=None):
-        self.object = obj
-        self.title = title or obj._meta.verbose_name
+    def __init__(self, title=None):
+        self.title = title
 
-    @cached_property
-    def attributes(self):
+    def get_attributes(self, obj):
         return [
             {
                 'label': attr.label or title(name),
-                'value': attr.render(self.object, {'name': name}),
+                'value': attr.render(obj, {'name': name}),
             } for name, attr in self._attrs.items()
         ]
 
-    def render(self):
+    def render(self, context):
+        obj = context.get('object')
         return render_to_string(self.template_name, {
             'title': self.title,
-            'attrs': self.attributes,
+            'attrs': self.get_attributes(obj),
         })
 
-    def __str__(self):
-        return self.render()
-
 
-class NestedGroupObjectPanel(ObjectPanel, metaclass=ObjectDetailsPanelMeta):
+class NestedGroupObjectPanel(ObjectPanel, metaclass=ObjectPanelMeta):
     name = attrs.TextAttr('name', label=_('Name'))
     description = attrs.TextAttr('description', label=_('Description'))
     parent = attrs.NestedObjectAttr('parent', label=_('Parent'), linkify=True)

+ 5 - 0
netbox/netbox/views/generic/object_views.py

@@ -47,6 +47,7 @@ class ObjectView(ActionsMixin, BaseObjectView):
         tab: A ViewTab instance for the view
         actions: An iterable of ObjectAction subclasses (see ActionsMixin)
     """
+    layout = None
     tab = None
     actions = (CloneObject, EditObject, DeleteObject)
 
@@ -58,6 +59,9 @@ class ObjectView(ActionsMixin, BaseObjectView):
         Return self.template_name if defined. Otherwise, dynamically resolve the template name using the queryset
         model's `app_label` and `model_name`.
         """
+        # TODO: Temporarily allow layout to override template_name
+        if self.layout is not None:
+            return 'generic/object.html'
         if self.template_name is not None:
             return self.template_name
         model_opts = self.queryset.model._meta
@@ -81,6 +85,7 @@ class ObjectView(ActionsMixin, BaseObjectView):
             'object': instance,
             'actions': actions,
             'tab': self.tab,
+            'layout': self.layout,
             **self.get_extra_context(request, instance),
         })
 

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

@@ -122,7 +122,20 @@ Context:
   {% plugin_alerts object %}
 {% endblock alerts %}
 
-{% block content %}{% endblock %}
+{% block content %}
+  {# Render panel layout declared on view class #}
+  {% for row in layout.rows %}
+    <div class="row">
+      {% for column in row.columns %}
+        <div class="col">
+          {% for panel in column.panels %}
+            {% render_panel panel %}
+          {% endfor %}
+        </div>
+      {% endfor %}
+    </div>
+  {% endfor %}
+{% endblock %}
 
 {% block modals %}
   {% include 'inc/htmx_modal.html' %}

+ 0 - 0
netbox/templates/components/attrs/address.html → netbox/templates/ui/attrs/address.html


+ 0 - 0
netbox/templates/components/attrs/choice.html → netbox/templates/ui/attrs/choice.html


+ 0 - 0
netbox/templates/components/attrs/gps_coordinates.html → netbox/templates/ui/attrs/gps_coordinates.html


+ 0 - 0
netbox/templates/components/attrs/nested_object.html → netbox/templates/ui/attrs/nested_object.html


+ 0 - 0
netbox/templates/components/attrs/object.html → netbox/templates/ui/attrs/object.html


+ 0 - 0
netbox/templates/components/attrs/text.html → netbox/templates/ui/attrs/text.html


+ 0 - 0
netbox/templates/components/attrs/timezone.html → netbox/templates/ui/attrs/timezone.html


+ 0 - 0
netbox/templates/components/attrs/utilization.html → netbox/templates/ui/attrs/utilization.html


+ 0 - 0
netbox/templates/components/object_details_panel.html → netbox/templates/ui/panels/object.html


+ 6 - 0
netbox/utilities/templatetags/builtins/tags.py

@@ -3,6 +3,7 @@ from urllib.parse import urlparse, urlunparse, parse_qs, urlencode
 
 from django import template
 from django.templatetags.static import static
+from django.utils.safestring import mark_safe
 
 from extras.choices import CustomFieldTypeChoices
 from utilities.querydict import dict_to_querydict
@@ -179,3 +180,8 @@ def static_with_params(path, **params):
     # Reconstruct the URL with the new query string
     new_parsed = parsed._replace(query=new_query)
     return urlunparse(new_parsed)
+
+
+@register.simple_tag(takes_context=True)
+def render_panel(context, panel):
+    return mark_safe(panel.render(context))