Limit object assignment to object panels
Some checks failed
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

View File

@ -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)

View File

@ -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'],
}

View File

@ -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,

View File

@ -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):