From e7ff0ddff8db6d3c193dac9831f08c354082c0c6 Mon Sep 17 00:00:00 2001 From: Renato Almeida de Oliveira Zaroubin Date: Thu, 27 Mar 2025 22:33:25 +0000 Subject: [PATCH] Create ObjectContactsView feature view, and upadte object_contact.html template --- netbox/netbox/views/generic/feature_views.py | 76 +++++++++++++++++++ netbox/templates/tenancy/object_contacts.html | 56 +++++++++++++- 2 files changed, 130 insertions(+), 2 deletions(-) diff --git a/netbox/netbox/views/generic/feature_views.py b/netbox/netbox/views/generic/feature_views.py index 1e17d5354..bab249f58 100644 --- a/netbox/netbox/views/generic/feature_views.py +++ b/netbox/netbox/views/generic/feature_views.py @@ -12,12 +12,19 @@ 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', + 'ObjectContactsView', 'ObjectChangeLogView', 'ObjectJobsView', 'ObjectJournalView', @@ -25,6 +32,75 @@ __all__ = ( ) +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..8a03caff5 100644 --- a/netbox/templates/tenancy/object_contacts.html +++ b/netbox/templates/tenancy/object_contacts.html @@ -1,5 +1,4 @@ -{% extends 'generic/object_children.html' %} -{% load helpers %} +{% extends base_template %}{% load helpers %} {% load i18n %} {% block extra_controls %} @@ -11,3 +10,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