diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index facccb65a..8fe7db406 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -247,9 +247,9 @@ class RegionView(GetRelatedModelsMixin, generic.ObjectView): ObjectsTablePanel( model='dcim.Region', title=_('Child Regions'), - filters={'parent_id': lambda obj: obj.pk}, + filters={'parent_id': lambda ctx: ctx['object'].pk}, actions=[ - actions.AddObject('dcim.Region', url_params={'parent': lambda obj: obj.pk}), + actions.AddObject('dcim.Region', url_params={'parent': lambda ctx: ctx['object'].pk}), ], ), PluginContentPanel('full_width_page'), @@ -386,9 +386,9 @@ class SiteGroupView(GetRelatedModelsMixin, generic.ObjectView): ObjectsTablePanel( model='dcim.SiteGroup', title=_('Child Groups'), - filters={'parent_id': lambda obj: obj.pk}, + filters={'parent_id': lambda ctx: ctx['object'].pk}, actions=[ - actions.AddObject('dcim.Region', url_params={'parent': lambda obj: obj.pk}), + actions.AddObject('dcim.Region', url_params={'parent': lambda ctx: ctx['object'].pk}), ], ), PluginContentPanel('full_width_page'), @@ -543,21 +543,21 @@ class SiteView(GetRelatedModelsMixin, generic.ObjectView): layout.Column( ObjectsTablePanel( model='dcim.Location', - filters={'site_id': lambda obj: obj.pk}, + filters={'site_id': lambda ctx: ctx['object'].pk}, actions=[ - actions.AddObject('dcim.Location', url_params={'site': lambda obj: obj.pk}), + actions.AddObject('dcim.Location', url_params={'site': lambda ctx: ctx['object'].pk}), ], ), ObjectsTablePanel( model='dcim.Device', title=_('Non-Racked Devices'), filters={ - 'site_id': lambda obj: obj.pk, + 'site_id': lambda ctx: ctx['object'].pk, 'rack_id': settings.FILTERS_NULL_CHOICE_VALUE, 'parent_bay_id': settings.FILTERS_NULL_CHOICE_VALUE, }, actions=[ - actions.AddObject('dcim.Device', url_params={'site': lambda obj: obj.pk}), + actions.AddObject('dcim.Device', url_params={'site': lambda ctx: ctx['object'].pk}), ], ), PluginContentPanel('full_width_page'), @@ -684,13 +684,13 @@ class LocationView(GetRelatedModelsMixin, generic.ObjectView): ObjectsTablePanel( model='dcim.Location', title=_('Child Locations'), - filters={'parent_id': lambda obj: obj.pk}, + filters={'parent_id': lambda ctx: ctx['object'].pk}, actions=[ actions.AddObject( 'dcim.Location', url_params={ - 'site': lambda obj: obj.site.pk if obj.site else None, - 'parent': lambda obj: obj.pk, + 'site': lambda ctx: ctx['object'].site_id, + 'parent': lambda ctx: ctx['object'].pk, } ), ], @@ -699,7 +699,7 @@ class LocationView(GetRelatedModelsMixin, generic.ObjectView): model='dcim.Device', title=_('Non-Racked Devices'), filters={ - 'location_id': lambda obj: obj.pk, + 'location_id': lambda ctx: ctx['object'].pk, 'rack_id': settings.FILTERS_NULL_CHOICE_VALUE, 'parent_bay_id': settings.FILTERS_NULL_CHOICE_VALUE, }, @@ -707,8 +707,8 @@ class LocationView(GetRelatedModelsMixin, generic.ObjectView): actions.AddObject( 'dcim.Device', url_params={ - 'site': lambda obj: obj.site.pk if obj.site else None, - 'parent': lambda obj: obj.pk, + 'site': lambda ctx: ctx['object'].site_id, + 'parent': lambda ctx: ctx['object'].pk, } ), ], @@ -907,14 +907,14 @@ class RackTypeView(GetRelatedModelsMixin, generic.ObjectView): layout.Row( layout.Column( panels.RackTypePanel(), - panels.RackDimensionsPanel(_('Dimensions')), + panels.RackDimensionsPanel(title=_('Dimensions')), TagsPanel(), CommentsPanel(), PluginContentPanel('left_page'), ), layout.Column( - panels.RackNumberingPanel(_('Numbering')), - panels.RackWeightPanel(_('Weight')), + panels.RackNumberingPanel(title=_('Numbering')), + panels.RackWeightPanel(title=_('Weight'), exclude=['total_weight']), CustomFieldsPanel(), RelatedObjectsPanel(), PluginContentPanel('right_page'), @@ -1047,9 +1047,9 @@ class RackView(GetRelatedModelsMixin, generic.ObjectView): layout.Row( layout.Column( panels.RackPanel(), - panels.RackDimensionsPanel(_('Dimensions')), - panels.RackNumberingPanel(_('Numbering')), - panels.RackWeightPanel(_('Weight')), + panels.RackDimensionsPanel(title=_('Dimensions')), + panels.RackNumberingPanel(title=_('Numbering')), + panels.RackWeightPanel(title=_('Weight')), CustomFieldsPanel(), TagsPanel(), CommentsPanel(), @@ -1199,6 +1199,28 @@ class RackReservationListView(generic.ObjectListView): @register_model_view(RackReservation) class RackReservationView(generic.ObjectView): queryset = RackReservation.objects.all() + layout = layout.Layout( + layout.Row( + layout.Column( + panels.RackPanel(accessor='rack', only=['region', 'site', 'location']), + CustomFieldsPanel(), + TagsPanel(), + CommentsPanel(), + ImageAttachmentsPanel(), + PluginContentPanel('left_page'), + ), + layout.Column( + TemplatePanel('dcim/panels/rack_elevations.html'), + RelatedObjectsPanel(), + PluginContentPanel('right_page'), + ), + ), + layout.Row( + layout.Column( + PluginContentPanel('full_width_page'), + ), + ), + ) @register_model_view(RackReservation, 'add', detail=False) diff --git a/netbox/extras/ui/panels.py b/netbox/extras/ui/panels.py index 5d789f640..991a4aa3d 100644 --- a/netbox/extras/ui/panels.py +++ b/netbox/extras/ui/panels.py @@ -14,8 +14,10 @@ class CustomFieldsPanel(panels.Panel): template_name = 'ui/panels/custom_fields.html' title = _('Custom Fields') - def get_context(self, obj): + def get_context(self, context): + obj = context['object'] return { + **super().get_context(context), 'custom_fields': obj.get_custom_fields_by_group(), } @@ -25,9 +27,9 @@ class ImageAttachmentsPanel(panels.ObjectsTablePanel): actions.AddObject( 'extras.imageattachment', url_params={ - 'object_type': lambda obj: ContentType.objects.get_for_model(obj).pk, - 'object_id': lambda obj: obj.pk, - 'return_url': lambda obj: obj.get_absolute_url(), + 'object_type': lambda ctx: ContentType.objects.get_for_model(ctx['object']).pk, + 'object_id': lambda ctx: ctx['object'].pk, + 'return_url': lambda ctx: ctx['object'].get_absolute_url(), }, label=_('Attach an image'), ), @@ -40,3 +42,9 @@ class ImageAttachmentsPanel(panels.ObjectsTablePanel): class TagsPanel(panels.Panel): template_name = 'ui/panels/tags.html' title = _('Tags') + + def get_context(self, context): + return { + **super().get_context(context), + 'object': context['object'], + } diff --git a/netbox/netbox/ui/actions.py b/netbox/netbox/ui/actions.py index 79fd9a5d6..10be487c8 100644 --- a/netbox/netbox/ui/actions.py +++ b/netbox/netbox/ui/actions.py @@ -26,20 +26,20 @@ class PanelAction: if label is not None: self.label = label - def get_url(self, obj): + def get_url(self, context): url = reverse(self.view_name, kwargs=self.view_kwargs or {}) if self.url_params: url_params = { - k: v(obj) if callable(v) else v for k, v in self.url_params.items() + k: v(context) if callable(v) else v for k, v in self.url_params.items() } - if 'return_url' not in url_params: - url_params['return_url'] = obj.get_absolute_url() + 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, obj): + def get_context(self, context): return { - 'url': self.get_url(obj), + 'url': self.get_url(context), 'label': self.label, 'button_class': self.button_class, 'button_icon': self.button_icon, diff --git a/netbox/netbox/ui/panels.py b/netbox/netbox/ui/panels.py index 328ef98df..05eae3e36 100644 --- a/netbox/netbox/ui/panels.py +++ b/netbox/netbox/ui/panels.py @@ -34,18 +34,15 @@ class Panel(ABC): if actions is not None: self.actions = actions - def get_context(self, obj): - return {} + def get_context(self, context): + return { + 'request': context.get('request'), + 'title': self.title, + 'actions': [action.get_context(context) for action in self.actions], + } def render(self, context): - obj = context.get('object') - return render_to_string(self.template_name, { - 'request': context.get('request'), - 'object': obj, - 'title': self.title or title(obj._meta.verbose_name), - 'actions': [action.get_context(obj) for action in self.actions], - **self.get_context(obj), - }) + return render_to_string(self.template_name, self.get_context(context)) class ObjectPanelMeta(ABCMeta): @@ -76,17 +73,39 @@ class ObjectPanelMeta(ABCMeta): class ObjectPanel(Panel, metaclass=ObjectPanelMeta): + accessor = None template_name = 'ui/panels/object.html' - def get_context(self, obj): - attrs = [ - { - 'label': attr.label or title(name), - 'value': attr.render(obj, {'name': name}), - } for name, attr in self._attrs.items() - ] + def __init__(self, accessor=None, only=None, exclude=None, **kwargs): + 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.") + self.only = only or [] + self.exclude = exclude or [] + + def get_context(self, context): + # Determine which attributes to display in the panel based on only/exclude args + attr_names = set(self._attrs.keys()) + if self.only: + attr_names &= set(self.only) + elif self.exclude: + attr_names -= set(self.exclude) + + obj = getattr(context['object'], self.accessor) if self.accessor else context['object'] + return { - 'attrs': attrs, + **super().get_context(context), + 'object': obj, + 'attrs': [ + { + 'label': attr.label or title(name), + 'value': attr.render(obj, {'name': name}), + } for name, attr in self._attrs.items() if name in attr_names + ], } @@ -108,13 +127,11 @@ class RelatedObjectsPanel(Panel): template_name = 'ui/panels/related_objects.html' title = _('Related Objects') - # TODO: Handle related_models from context - def render(self, context): - return render_to_string(self.template_name, { - 'title': self.title, - 'object': context.get('object'), + def get_context(self, context): + return { + **super().get_context(context), 'related_models': context.get('related_models'), - }) + } class ObjectsTablePanel(Panel): @@ -131,13 +148,14 @@ class ObjectsTablePanel(Panel): if self.title is None: self.title = title(self.model._meta.verbose_name_plural) - def get_context(self, obj): + def get_context(self, context): url_params = { - k: v(obj) if callable(v) else v for k, v in self.filters.items() + k: v(context) if callable(v) else v for k, v in self.filters.items() } - if 'return_url' not in url_params: - url_params['return_url'] = obj.get_absolute_url() + if 'return_url' not in url_params and 'object' in context: + url_params['return_url'] = context['object'].get_absolute_url() return { + **super().get_context(context), 'viewname': get_viewname(self.model, 'list'), 'url_params': dict_to_querydict(url_params), } @@ -149,6 +167,10 @@ class TemplatePanel(Panel): super().__init__(**kwargs) self.template_name = template_name + def render(self, context): + # Pass the entire context to the template + return render_to_string(self.template_name, context.flatten()) + class PluginContentPanel(Panel):