From ea756b29e9409eef498c38aeff22aadfedc190ff Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Thu, 26 Mar 2026 09:16:31 -0700 Subject: [PATCH] #20923 - Convert tenancy to new UI layout (#21745) --- netbox/templates/tenancy/attrs/email.html | 1 + netbox/templates/tenancy/attrs/link.html | 1 + netbox/templates/tenancy/attrs/phone.html | 1 + netbox/templates/tenancy/contact.html | 99 -------------------- netbox/templates/tenancy/contactgroup.html | 56 +----------- netbox/templates/tenancy/contactrole.html | 41 --------- netbox/templates/tenancy/tenant.html | 39 +------- netbox/templates/tenancy/tenantgroup.html | 57 +----------- netbox/tenancy/ui/__init__.py | 0 netbox/tenancy/ui/panels.py | 19 ++++ netbox/tenancy/views.py | 100 +++++++++++++++++++++ 11 files changed, 129 insertions(+), 285 deletions(-) create mode 100644 netbox/templates/tenancy/attrs/email.html create mode 100644 netbox/templates/tenancy/attrs/link.html create mode 100644 netbox/templates/tenancy/attrs/phone.html create mode 100644 netbox/tenancy/ui/__init__.py create mode 100644 netbox/tenancy/ui/panels.py diff --git a/netbox/templates/tenancy/attrs/email.html b/netbox/templates/tenancy/attrs/email.html new file mode 100644 index 000000000..2ee254a64 --- /dev/null +++ b/netbox/templates/tenancy/attrs/email.html @@ -0,0 +1 @@ +{{ value }} diff --git a/netbox/templates/tenancy/attrs/link.html b/netbox/templates/tenancy/attrs/link.html new file mode 100644 index 000000000..30b750ba8 --- /dev/null +++ b/netbox/templates/tenancy/attrs/link.html @@ -0,0 +1 @@ +{{ value }} diff --git a/netbox/templates/tenancy/attrs/phone.html b/netbox/templates/tenancy/attrs/phone.html new file mode 100644 index 000000000..93bcde0c6 --- /dev/null +++ b/netbox/templates/tenancy/attrs/phone.html @@ -0,0 +1 @@ +{{ value }} diff --git a/netbox/templates/tenancy/contact.html b/netbox/templates/tenancy/contact.html index 790e08489..f15e1d050 100644 --- a/netbox/templates/tenancy/contact.html +++ b/netbox/templates/tenancy/contact.html @@ -1,100 +1 @@ {% extends 'generic/object.html' %} -{% load helpers %} -{% load plugins %} -{% load render_table from django_tables2 %} -{% load i18n %} - -{% block breadcrumbs %} - {{ block.super }} - {% if object.group %} - - {% endif %} -{% endblock breadcrumbs %} - -{% block content %} -
-
-
-

{% trans "Contact" %}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{% trans "Groups" %} - {% if object.groups.all|length > 0 %} -
    - {% for group in object.groups.all %} -
  1. {{ group|linkify|placeholder }}
  2. - {% endfor %} -
- {% else %} - {{ ''|placeholder }} - {% endif %} -
{% trans "Name" %}{{ object.name }}
{% trans "Title" %}{{ object.title|placeholder }}
{% trans "Phone" %} - {% if object.phone %} - {{ object.phone }} - {% else %} - {{ ''|placeholder }} - {% endif %} -
{% trans "Email" %} - {% if object.email %} - {{ object.email }} - {% else %} - {{ ''|placeholder }} - {% endif %} -
{% trans "Address" %}{{ object.address|linebreaksbr|placeholder }}
{% trans "Link" %} - {% if object.link %} - {{ object.link }} - {% else %} - {{ ''|placeholder }} - {% endif %} -
{% trans "Description" %}{{ object.description|placeholder }}
-
- {% include 'inc/panels/tags.html' %} - {% plugin_left_page object %} -
-
- {% include 'inc/panels/comments.html' %} - {% include 'inc/panels/custom_fields.html' %} - {% plugin_right_page object %} -
-
-
-
-
-

{% trans "Assignments" %}

- {% htmx_table 'tenancy:contactassignment_list' contact_id=object.pk %} -
- {% plugin_full_width_page object %} -
-
-{% endblock %} diff --git a/netbox/templates/tenancy/contactgroup.html b/netbox/templates/tenancy/contactgroup.html index bdcf675dd..20f0e6ef2 100644 --- a/netbox/templates/tenancy/contactgroup.html +++ b/netbox/templates/tenancy/contactgroup.html @@ -1,60 +1,8 @@ {% extends 'generic/object.html' %} -{% load helpers %} -{% load plugins %} -{% load render_table from django_tables2 %} -{% load i18n %} {% block breadcrumbs %} {{ block.super }} - {% for contactgroup in object.get_ancestors %} - + {% for ancestor in object.get_ancestors %} + {% endfor %} {% endblock %} - -{% block content %} -
-
-
-

{% trans "Contact Group" %}

- - - - - - - - - - - - - -
{% trans "Name" %}{{ object.name }}
{% trans "Description" %}{{ object.description|placeholder }}
{% trans "Parent" %}{{ object.parent|linkify|placeholder }}
-
- {% include 'inc/panels/tags.html' %} - {% include 'inc/panels/comments.html' %} - {% plugin_left_page object %} -
-
- {% include 'inc/panels/related_objects.html' %} - {% include 'inc/panels/custom_fields.html' %} - {% plugin_right_page object %} -
-
-
-
-

- {% trans "Child Groups" %} - {% if perms.tenancy.add_contactgroup %} - - {% endif %} -

- {% htmx_table 'tenancy:contactgroup_list' parent_id=object.pk %} -
- {% plugin_full_width_page object %} -
-{% endblock %} diff --git a/netbox/templates/tenancy/contactrole.html b/netbox/templates/tenancy/contactrole.html index 44e004d21..f15e1d050 100644 --- a/netbox/templates/tenancy/contactrole.html +++ b/netbox/templates/tenancy/contactrole.html @@ -1,42 +1 @@ {% extends 'generic/object.html' %} -{% load helpers %} -{% load plugins %} -{% load render_table from django_tables2 %} -{% load i18n %} - -{% block breadcrumbs %} - -{% endblock %} - -{% block content %} -
-
-
-

{% trans "Contact Role" %}

- - - - - - - - - -
{% trans "Name" %}{{ object.name }}
{% trans "Description" %}{{ object.description|placeholder }}
-
- {% include 'inc/panels/tags.html' %} - {% plugin_left_page object %} -
-
- {% include 'inc/panels/related_objects.html' %} - {% include 'inc/panels/comments.html' %} - {% include 'inc/panels/custom_fields.html' %} - {% plugin_right_page object %} -
-
-
-
- {% plugin_full_width_page object %} -
-
-{% endblock %} diff --git a/netbox/templates/tenancy/tenant.html b/netbox/templates/tenancy/tenant.html index a3a977697..09f401fa8 100644 --- a/netbox/templates/tenancy/tenant.html +++ b/netbox/templates/tenancy/tenant.html @@ -1,44 +1,11 @@ {% extends 'generic/object.html' %} -{% load helpers %} -{% load plugins %} -{% load i18n %} {% block breadcrumbs %} {{ block.super }} {% if object.group %} + {% for group in object.group.get_ancestors %} + + {% endfor %} {% endif %} -{% endblock breadcrumbs %} - -{% block content %} -
-
-
-

{% trans "Tenant" %}

- - - - - - - - - -
{% trans "Group" %}{{ object.group|linkify|placeholder }}
{% trans "Description" %}{{ object.description|placeholder }}
-
- {% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' %} - {% include 'inc/panels/comments.html' %} - {% plugin_left_page object %} -
-
- {% include 'inc/panels/related_objects.html' %} - {% plugin_right_page object %} -
-
-
-
- {% plugin_full_width_page object %} -
-
{% endblock %} diff --git a/netbox/templates/tenancy/tenantgroup.html b/netbox/templates/tenancy/tenantgroup.html index 5ca3ba554..f4e32b697 100644 --- a/netbox/templates/tenancy/tenantgroup.html +++ b/netbox/templates/tenancy/tenantgroup.html @@ -1,13 +1,10 @@ {% extends 'generic/object.html' %} -{% load helpers %} -{% load plugins %} -{% load render_table from django_tables2 %} {% load i18n %} {% block breadcrumbs %} {{ block.super }} - {% for tenantgroup in object.get_ancestors %} - + {% for ancestor in object.get_ancestors %} + {% endfor %} {% endblock %} @@ -18,53 +15,3 @@ {% endif %} {% endblock extra_controls %} - -{% block content %} -
-
-
-

{% trans "Tenant Group" %}

- - - - - - - - - - - - - -
{% trans "Name" %}{{ object.name }}
{% trans "Description" %}{{ object.description|placeholder }}
{% trans "Parent" %}{{ object.parent|linkify|placeholder }}
-
- {% include 'inc/panels/tags.html' %} - {% include 'inc/panels/comments.html' %} - {% plugin_left_page object %} -
-
- {% include 'inc/panels/related_objects.html' %} - {% include 'inc/panels/custom_fields.html' %} - {% plugin_right_page object %} -
-
-
-
-
-

- {% trans "Child Groups" %} - {% if perms.tenancy.add_tenantgroup %} - - {% endif %} -

- {% htmx_table 'tenancy:tenantgroup_list' parent_id=object.pk %} -
- {% plugin_full_width_page object %} -
-
-{% endblock %} diff --git a/netbox/tenancy/ui/__init__.py b/netbox/tenancy/ui/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/tenancy/ui/panels.py b/netbox/tenancy/ui/panels.py new file mode 100644 index 000000000..75d512385 --- /dev/null +++ b/netbox/tenancy/ui/panels.py @@ -0,0 +1,19 @@ +from django.utils.translation import gettext_lazy as _ + +from netbox.ui import attrs, panels + + +class TenantPanel(panels.ObjectAttributesPanel): + group = attrs.RelatedObjectAttr('group', linkify=True) + description = attrs.TextAttr('description') + + +class ContactPanel(panels.ObjectAttributesPanel): + groups = attrs.RelatedObjectListAttr('groups', linkify=True, label=_('Groups')) + name = attrs.TextAttr('name') + title = attrs.TextAttr('title') + phone = attrs.TemplatedAttr('phone', label=_('Phone'), template_name='tenancy/attrs/phone.html') + email = attrs.TemplatedAttr('email', label=_('Email'), template_name='tenancy/attrs/email.html') + address = attrs.AddressAttr('address', map_url=False) + link = attrs.TemplatedAttr('link', label=_('Link'), template_name='tenancy/attrs/link.html') + description = attrs.TextAttr('description') diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index 3e427a3ce..26b0ac5ab 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -1,13 +1,24 @@ from django.contrib.contenttypes.models import ContentType from django.shortcuts import get_object_or_404 +from django.utils.translation import gettext_lazy as _ +from extras.ui.panels import CustomFieldsPanel, TagsPanel from netbox.object_actions import BulkDelete, BulkEdit, BulkExport, BulkImport +from netbox.ui import actions, layout +from netbox.ui.panels import ( + CommentsPanel, + NestedGroupObjectPanel, + ObjectsTablePanel, + OrganizationalObjectPanel, + RelatedObjectsPanel, +) from netbox.views import generic from utilities.query import count_related from utilities.views import GetRelatedModelsMixin, register_model_view from . import filtersets, forms, tables from .models import * +from .ui import panels # # Tenant groups @@ -31,6 +42,31 @@ class TenantGroupListView(generic.ObjectListView): @register_model_view(TenantGroup) class TenantGroupView(GetRelatedModelsMixin, generic.ObjectView): queryset = TenantGroup.objects.all() + layout = layout.SimpleLayout( + left_panels=[ + NestedGroupObjectPanel(), + TagsPanel(), + CommentsPanel(), + ], + right_panels=[ + RelatedObjectsPanel(), + CustomFieldsPanel(), + ], + bottom_panels=[ + ObjectsTablePanel( + 'tenancy.tenantgroup', + filters={'parent_id': lambda ctx: ctx['object'].pk}, + title=_('Child Groups'), + actions=[ + actions.AddObject( + 'tenancy.tenantgroup', + url_params={'parent': lambda ctx: ctx['object'].pk}, + label=_('Add Tenant Group'), + ), + ], + ), + ], + ) def get_extra_context(self, request, instance): groups = instance.get_descendants(include_self=True) @@ -106,6 +142,17 @@ class TenantListView(generic.ObjectListView): @register_model_view(Tenant) class TenantView(GetRelatedModelsMixin, generic.ObjectView): queryset = Tenant.objects.all() + layout = layout.SimpleLayout( + left_panels=[ + panels.TenantPanel(), + CustomFieldsPanel(), + TagsPanel(), + CommentsPanel(), + ], + right_panels=[ + RelatedObjectsPanel(), + ], + ) def get_extra_context(self, request, instance): return { @@ -173,6 +220,31 @@ class ContactGroupListView(generic.ObjectListView): @register_model_view(ContactGroup) class ContactGroupView(GetRelatedModelsMixin, generic.ObjectView): queryset = ContactGroup.objects.all() + layout = layout.SimpleLayout( + left_panels=[ + NestedGroupObjectPanel(), + TagsPanel(), + CommentsPanel(), + ], + right_panels=[ + RelatedObjectsPanel(), + CustomFieldsPanel(), + ], + bottom_panels=[ + ObjectsTablePanel( + 'tenancy.contactgroup', + filters={'parent_id': lambda ctx: ctx['object'].pk}, + title=_('Child Groups'), + actions=[ + actions.AddObject( + 'tenancy.contactgroup', + url_params={'parent': lambda ctx: ctx['object'].pk}, + label=_('Add Contact Group'), + ), + ], + ), + ], + ) def get_extra_context(self, request, instance): groups = instance.get_descendants(include_self=True) @@ -254,6 +326,17 @@ class ContactRoleListView(generic.ObjectListView): @register_model_view(ContactRole) class ContactRoleView(GetRelatedModelsMixin, generic.ObjectView): queryset = ContactRole.objects.all() + layout = layout.SimpleLayout( + left_panels=[ + OrganizationalObjectPanel(), + TagsPanel(), + ], + right_panels=[ + RelatedObjectsPanel(), + CommentsPanel(), + CustomFieldsPanel(), + ], + ) def get_extra_context(self, request, instance): return { @@ -317,6 +400,23 @@ class ContactListView(generic.ObjectListView): @register_model_view(Contact) class ContactView(generic.ObjectView): queryset = Contact.objects.all() + layout = layout.SimpleLayout( + left_panels=[ + panels.ContactPanel(), + TagsPanel(), + ], + right_panels=[ + CommentsPanel(), + CustomFieldsPanel(), + ], + bottom_panels=[ + ObjectsTablePanel( + 'tenancy.contactassignment', + filters={'contact_id': lambda ctx: ctx['object'].pk}, + title=_('Assignments'), + ), + ], + ) @register_model_view(Contact, 'add', detail=False)