From eb4794ded7c10a71e4371346b2073c8953386a94 Mon Sep 17 00:00:00 2001 From: Renato Almeida de Oliveira Zaroubin Date: Wed, 26 Mar 2025 22:30:05 +0000 Subject: [PATCH] Created ObjectContactsView feature view, and refactor object_contacts.html template --- netbox/netbox/views/generic/feature_views.py | 78 +++++++++++++++++++ netbox/templates/tenancy/object_contacts.html | 55 ++++++++++++- 2 files changed, 132 insertions(+), 1 deletion(-) diff --git a/netbox/netbox/views/generic/feature_views.py b/netbox/netbox/views/generic/feature_views.py index 01c4b2862..ee4c4e6a1 100644 --- a/netbox/netbox/views/generic/feature_views.py +++ b/netbox/netbox/views/generic/feature_views.py @@ -12,9 +12,15 @@ from core.tables import JobTable, ObjectChangeTable from extras.forms import JournalEntryForm from extras.models import JournalEntry from extras.tables import JournalEntryTable +from tenancy.models import ContactAssignment +from tenancy.tables import ContactAssignmentTable +from tenancy.filtersets import ContactAssignmentFilterSet +from tenancy.forms import ContactAssignmentFilterForm from utilities.permissions import get_permission_for_model from utilities.views import ConditionalLoginRequiredMixin, GetReturnURLMixin, ViewTab +from utilities.htmx import htmx_partial from .base import BaseMultiObjectView +from .mixins import ActionsMixin, TableMixin __all__ = ( 'BulkSyncDataView', @@ -22,9 +28,81 @@ __all__ = ( 'ObjectJobsView', 'ObjectJournalView', 'ObjectSyncDataView', + 'ObjectContactsView', ) +class ObjectContactsView(ConditionalLoginRequiredMixin, ActionsMixin, TableMixin, View): + """ + Show all Contacts associated with an object. The model class must be passed as a keyword argument when referencing + this view in a URL path. For example: + + path('sites//contacts/', ObjectContactsView.as_view(), name='site_contacts', kwargs={'model': Site}), + + Attributes: + base_template: The name of the template to extend. If not provided, "{app}/{model}.html" will be used. + """ + base_template = None + tab = ViewTab( + label=_('Contacts'), + badge=lambda obj: obj.contacts.count(), + permission='tenancy.view_contactassignment', + weight=9000 + ) + table = ContactAssignmentTable + filterset = ContactAssignmentFilterSet + filterset_form = ContactAssignmentFilterForm + + def get(self, request, model, **kwargs): + # Handle QuerySet restriction of parent object if needed + if hasattr(model.objects, 'restrict'): + obj = get_object_or_404(model.objects.restrict(request.user, 'view'), **kwargs) + else: + obj = get_object_or_404(model, **kwargs) + + # Gather all Contact Assignments for this object + content_type = ContentType.objects.get_for_model(model) + contactassignments = ContactAssignment.objects.restrict(request.user, 'view').prefetch_related( + 'contact', + ).filter( + object_type=content_type, + object_id=obj.pk + ).order_by('priority', 'contact', 'role') + contactassignments = self.filterset(request.GET, contactassignments, request=request).qs + + # Determine the available actions + actions = self.get_permitted_actions(request.user, model=ContactAssignment) + has_bulk_actions = any([a.startswith('bulk_') for a in actions]) + + table = self.get_table(contactassignments, request, has_bulk_actions) + table.columns.hide('object_type') + table.columns.hide('object') + + if htmx_partial(request): + return render(request, 'htmx/table.html', { + 'object': obj, + 'table': table, + 'model': ContactAssignment, + }) + + # Default to using "/.html" as the template, if it exists. Otherwise, + # fall back to using base.html. + if self.base_template is None: + self.base_template = f"{model._meta.app_label}/{model._meta.model_name}.html" + + return render(request, 'tenancy/object_contacts.html', { + 'object': obj, + 'model': ContactAssignment, + 'base_template': self.base_template, + 'table': table, + 'table_config': f'{table.name}_config', + 'filter_form': self.filterset_form(request.GET) if self.filterset_form else None, + 'actions': actions, + 'tab': self.tab, + 'return_url': request.get_full_path(), + }) + + class ObjectChangeLogView(ConditionalLoginRequiredMixin, View): """ Present a history of changes made to a particular object. The model class must be passed as a keyword argument diff --git a/netbox/templates/tenancy/object_contacts.html b/netbox/templates/tenancy/object_contacts.html index 304972e4a..a7a7bd62b 100644 --- a/netbox/templates/tenancy/object_contacts.html +++ b/netbox/templates/tenancy/object_contacts.html @@ -1,4 +1,4 @@ -{% extends 'generic/object_children.html' %} +{% extends base_template %} {% load helpers %} {% load i18n %} @@ -11,3 +11,56 @@ {% endwith %} {% endif %} {% endblock %} + +{% block content %} + {% block table_controls %} + {% include 'inc/table_controls_htmx.html' with table_modal=table_config %} + {% endblock table_controls %} +
+ {% csrf_token %} +
+
+ {% include 'htmx/table.html' %} +
+
+
+ {% block bulk_controls %} +
+ {# Bulk edit buttons #} + {% block bulk_edit_controls %} + {% with bulk_edit_view=model|validated_viewname:"bulk_edit" %} + {% if 'bulk_edit' in actions and bulk_edit_view %} + + {% endif %} + {% endwith %} + {% endblock bulk_edit_controls %} +
+
+ {# Bulk delete buttons #} + {% block bulk_delete_controls %} + {% with bulk_delete_view=model|validated_viewname:"bulk_delete" %} + {% if 'bulk_delete' in actions and bulk_delete_view %} + + {% endif %} + {% endwith %} + {% endblock bulk_delete_controls %} +
+ {# Other bulk action buttons #} + {% block bulk_extra_controls %}{% endblock %} + {% endblock bulk_controls %} +
+
+{% endblock content %} + +{% block modals %} + {{ block.super }} + {% table_config_form table %} +{% endblock modals %} \ No newline at end of file