Limit object assignment to object panels
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled

This commit is contained in:
Jeremy Stretch
2025-11-03 17:04:24 -05:00
parent 17429c4257
commit c05106f9b2
4 changed files with 110 additions and 58 deletions
+42 -20
View File
@@ -247,9 +247,9 @@ class RegionView(GetRelatedModelsMixin, generic.ObjectView):
ObjectsTablePanel( ObjectsTablePanel(
model='dcim.Region', model='dcim.Region',
title=_('Child Regions'), title=_('Child Regions'),
filters={'parent_id': lambda obj: obj.pk}, filters={'parent_id': lambda ctx: ctx['object'].pk},
actions=[ 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'), PluginContentPanel('full_width_page'),
@@ -386,9 +386,9 @@ class SiteGroupView(GetRelatedModelsMixin, generic.ObjectView):
ObjectsTablePanel( ObjectsTablePanel(
model='dcim.SiteGroup', model='dcim.SiteGroup',
title=_('Child Groups'), title=_('Child Groups'),
filters={'parent_id': lambda obj: obj.pk}, filters={'parent_id': lambda ctx: ctx['object'].pk},
actions=[ 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'), PluginContentPanel('full_width_page'),
@@ -543,21 +543,21 @@ class SiteView(GetRelatedModelsMixin, generic.ObjectView):
layout.Column( layout.Column(
ObjectsTablePanel( ObjectsTablePanel(
model='dcim.Location', model='dcim.Location',
filters={'site_id': lambda obj: obj.pk}, filters={'site_id': lambda ctx: ctx['object'].pk},
actions=[ 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( ObjectsTablePanel(
model='dcim.Device', model='dcim.Device',
title=_('Non-Racked Devices'), title=_('Non-Racked Devices'),
filters={ filters={
'site_id': lambda obj: obj.pk, 'site_id': lambda ctx: ctx['object'].pk,
'rack_id': settings.FILTERS_NULL_CHOICE_VALUE, 'rack_id': settings.FILTERS_NULL_CHOICE_VALUE,
'parent_bay_id': settings.FILTERS_NULL_CHOICE_VALUE, 'parent_bay_id': settings.FILTERS_NULL_CHOICE_VALUE,
}, },
actions=[ 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'), PluginContentPanel('full_width_page'),
@@ -684,13 +684,13 @@ class LocationView(GetRelatedModelsMixin, generic.ObjectView):
ObjectsTablePanel( ObjectsTablePanel(
model='dcim.Location', model='dcim.Location',
title=_('Child Locations'), title=_('Child Locations'),
filters={'parent_id': lambda obj: obj.pk}, filters={'parent_id': lambda ctx: ctx['object'].pk},
actions=[ actions=[
actions.AddObject( actions.AddObject(
'dcim.Location', 'dcim.Location',
url_params={ url_params={
'site': lambda obj: obj.site.pk if obj.site else None, 'site': lambda ctx: ctx['object'].site_id,
'parent': lambda obj: obj.pk, 'parent': lambda ctx: ctx['object'].pk,
} }
), ),
], ],
@@ -699,7 +699,7 @@ class LocationView(GetRelatedModelsMixin, generic.ObjectView):
model='dcim.Device', model='dcim.Device',
title=_('Non-Racked Devices'), title=_('Non-Racked Devices'),
filters={ filters={
'location_id': lambda obj: obj.pk, 'location_id': lambda ctx: ctx['object'].pk,
'rack_id': settings.FILTERS_NULL_CHOICE_VALUE, 'rack_id': settings.FILTERS_NULL_CHOICE_VALUE,
'parent_bay_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( actions.AddObject(
'dcim.Device', 'dcim.Device',
url_params={ url_params={
'site': lambda obj: obj.site.pk if obj.site else None, 'site': lambda ctx: ctx['object'].site_id,
'parent': lambda obj: obj.pk, 'parent': lambda ctx: ctx['object'].pk,
} }
), ),
], ],
@@ -907,14 +907,14 @@ class RackTypeView(GetRelatedModelsMixin, generic.ObjectView):
layout.Row( layout.Row(
layout.Column( layout.Column(
panels.RackTypePanel(), panels.RackTypePanel(),
panels.RackDimensionsPanel(_('Dimensions')), panels.RackDimensionsPanel(title=_('Dimensions')),
TagsPanel(), TagsPanel(),
CommentsPanel(), CommentsPanel(),
PluginContentPanel('left_page'), PluginContentPanel('left_page'),
), ),
layout.Column( layout.Column(
panels.RackNumberingPanel(_('Numbering')), panels.RackNumberingPanel(title=_('Numbering')),
panels.RackWeightPanel(_('Weight')), panels.RackWeightPanel(title=_('Weight'), exclude=['total_weight']),
CustomFieldsPanel(), CustomFieldsPanel(),
RelatedObjectsPanel(), RelatedObjectsPanel(),
PluginContentPanel('right_page'), PluginContentPanel('right_page'),
@@ -1047,9 +1047,9 @@ class RackView(GetRelatedModelsMixin, generic.ObjectView):
layout.Row( layout.Row(
layout.Column( layout.Column(
panels.RackPanel(), panels.RackPanel(),
panels.RackDimensionsPanel(_('Dimensions')), panels.RackDimensionsPanel(title=_('Dimensions')),
panels.RackNumberingPanel(_('Numbering')), panels.RackNumberingPanel(title=_('Numbering')),
panels.RackWeightPanel(_('Weight')), panels.RackWeightPanel(title=_('Weight')),
CustomFieldsPanel(), CustomFieldsPanel(),
TagsPanel(), TagsPanel(),
CommentsPanel(), CommentsPanel(),
@@ -1199,6 +1199,28 @@ class RackReservationListView(generic.ObjectListView):
@register_model_view(RackReservation) @register_model_view(RackReservation)
class RackReservationView(generic.ObjectView): class RackReservationView(generic.ObjectView):
queryset = RackReservation.objects.all() 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) @register_model_view(RackReservation, 'add', detail=False)
+12 -4
View File
@@ -14,8 +14,10 @@ class CustomFieldsPanel(panels.Panel):
template_name = 'ui/panels/custom_fields.html' template_name = 'ui/panels/custom_fields.html'
title = _('Custom Fields') title = _('Custom Fields')
def get_context(self, obj): def get_context(self, context):
obj = context['object']
return { return {
**super().get_context(context),
'custom_fields': obj.get_custom_fields_by_group(), 'custom_fields': obj.get_custom_fields_by_group(),
} }
@@ -25,9 +27,9 @@ class ImageAttachmentsPanel(panels.ObjectsTablePanel):
actions.AddObject( actions.AddObject(
'extras.imageattachment', 'extras.imageattachment',
url_params={ url_params={
'object_type': lambda obj: ContentType.objects.get_for_model(obj).pk, 'object_type': lambda ctx: ContentType.objects.get_for_model(ctx['object']).pk,
'object_id': lambda obj: obj.pk, 'object_id': lambda ctx: ctx['object'].pk,
'return_url': lambda obj: obj.get_absolute_url(), 'return_url': lambda ctx: ctx['object'].get_absolute_url(),
}, },
label=_('Attach an image'), label=_('Attach an image'),
), ),
@@ -40,3 +42,9 @@ class ImageAttachmentsPanel(panels.ObjectsTablePanel):
class TagsPanel(panels.Panel): class TagsPanel(panels.Panel):
template_name = 'ui/panels/tags.html' template_name = 'ui/panels/tags.html'
title = _('Tags') title = _('Tags')
def get_context(self, context):
return {
**super().get_context(context),
'object': context['object'],
}
+6 -6
View File
@@ -26,20 +26,20 @@ class PanelAction:
if label is not None: if label is not None:
self.label = label self.label = label
def get_url(self, obj): def get_url(self, context):
url = reverse(self.view_name, kwargs=self.view_kwargs or {}) url = reverse(self.view_name, kwargs=self.view_kwargs or {})
if self.url_params: if self.url_params:
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: if 'return_url' not in url_params and 'object' in context:
url_params['return_url'] = obj.get_absolute_url() url_params['return_url'] = context['object'].get_absolute_url()
url = f'{url}?{urlencode(url_params)}' url = f'{url}?{urlencode(url_params)}'
return url return url
def get_context(self, obj): def get_context(self, context):
return { return {
'url': self.get_url(obj), 'url': self.get_url(context),
'label': self.label, 'label': self.label,
'button_class': self.button_class, 'button_class': self.button_class,
'button_icon': self.button_icon, 'button_icon': self.button_icon,
+50 -28
View File
@@ -34,18 +34,15 @@ class Panel(ABC):
if actions is not None: if actions is not None:
self.actions = actions self.actions = actions
def get_context(self, obj): def get_context(self, context):
return {} return {
'request': context.get('request'),
'title': self.title,
'actions': [action.get_context(context) for action in self.actions],
}
def render(self, context): def render(self, context):
obj = context.get('object') return render_to_string(self.template_name, self.get_context(context))
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),
})
class ObjectPanelMeta(ABCMeta): class ObjectPanelMeta(ABCMeta):
@@ -76,17 +73,39 @@ class ObjectPanelMeta(ABCMeta):
class ObjectPanel(Panel, metaclass=ObjectPanelMeta): class ObjectPanel(Panel, metaclass=ObjectPanelMeta):
accessor = None
template_name = 'ui/panels/object.html' template_name = 'ui/panels/object.html'
def get_context(self, obj): def __init__(self, accessor=None, only=None, exclude=None, **kwargs):
attrs = [ super().__init__(**kwargs)
{ if accessor is not None:
'label': attr.label or title(name), self.accessor = accessor
'value': attr.render(obj, {'name': name}),
} for name, attr in self._attrs.items() # 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 { 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' template_name = 'ui/panels/related_objects.html'
title = _('Related Objects') title = _('Related Objects')
# TODO: Handle related_models from context def get_context(self, context):
def render(self, context): return {
return render_to_string(self.template_name, { **super().get_context(context),
'title': self.title,
'object': context.get('object'),
'related_models': context.get('related_models'), 'related_models': context.get('related_models'),
}) }
class ObjectsTablePanel(Panel): class ObjectsTablePanel(Panel):
@@ -131,13 +148,14 @@ class ObjectsTablePanel(Panel):
if self.title is None: if self.title is None:
self.title = title(self.model._meta.verbose_name_plural) self.title = title(self.model._meta.verbose_name_plural)
def get_context(self, obj): def get_context(self, context):
url_params = { 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: if 'return_url' not in url_params and 'object' in context:
url_params['return_url'] = obj.get_absolute_url() url_params['return_url'] = context['object'].get_absolute_url()
return { return {
**super().get_context(context),
'viewname': get_viewname(self.model, 'list'), 'viewname': get_viewname(self.model, 'list'),
'url_params': dict_to_querydict(url_params), 'url_params': dict_to_querydict(url_params),
} }
@@ -149,6 +167,10 @@ class TemplatePanel(Panel):
super().__init__(**kwargs) super().__init__(**kwargs)
self.template_name = template_name 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): class PluginContentPanel(Panel):