Adds contact tabs (#12460)

* adds contact tabs #11599

* fixed lint issues

* changes as per review

* changes as per review

* replaces generic object template with base template
This commit is contained in:
Abhimanyu Saharan 2023-05-12 19:56:26 +05:30 committed by GitHub
parent e71a98499f
commit 4eb5e90ccc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 137 additions and 83 deletions

View File

@ -1,10 +1,10 @@
from django.contrib import messages from django.contrib import messages
from django.db import transaction from django.db import transaction
from django.db.models import Q
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from dcim.views import PathTraceView from dcim.views import PathTraceView
from netbox.views import generic from netbox.views import generic
from tenancy.views import ObjectContactsView
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from utilities.utils import count_related from utilities.utils import count_related
from utilities.views import register_model_view from utilities.views import register_model_view
@ -73,6 +73,11 @@ class ProviderBulkDeleteView(generic.BulkDeleteView):
table = tables.ProviderTable table = tables.ProviderTable
@register_model_view(Provider, 'contacts')
class ProviderContactsView(ObjectContactsView):
queryset = Provider.objects.all()
# #
# ProviderAccounts # ProviderAccounts
# #
@ -134,6 +139,11 @@ class ProviderAccountBulkDeleteView(generic.BulkDeleteView):
table = tables.ProviderAccountTable table = tables.ProviderAccountTable
@register_model_view(ProviderAccount, 'contacts')
class ProviderAccountContactsView(ObjectContactsView):
queryset = ProviderAccount.objects.all()
# #
# Provider networks # Provider networks
# #
@ -389,6 +399,11 @@ class CircuitSwapTerminations(generic.ObjectEditView):
}) })
@register_model_view(Circuit, 'contacts')
class CircuitContactsView(ObjectContactsView):
queryset = Circuit.objects.all()
# #
# Circuit terminations # Circuit terminations
# #

View File

@ -20,6 +20,7 @@ from extras.views import ObjectConfigContextView
from ipam.models import ASN, IPAddress, Prefix, VLAN, VLANGroup from ipam.models import ASN, IPAddress, Prefix, VLAN, VLANGroup
from ipam.tables import InterfaceVLANTable from ipam.tables import InterfaceVLANTable
from netbox.views import generic from netbox.views import generic
from tenancy.views import ObjectContactsView
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from utilities.paginator import EnhancedPaginator, get_paginate_count from utilities.paginator import EnhancedPaginator, get_paginate_count
from utilities.permissions import get_permission_for_model from utilities.permissions import get_permission_for_model
@ -267,6 +268,11 @@ class RegionBulkDeleteView(generic.BulkDeleteView):
table = tables.RegionTable table = tables.RegionTable
@register_model_view(Region, 'contacts')
class RegionContactsView(ObjectContactsView):
queryset = Region.objects.all()
# #
# Site groups # Site groups
# #
@ -342,6 +348,11 @@ class SiteGroupBulkDeleteView(generic.BulkDeleteView):
table = tables.SiteGroupTable table = tables.SiteGroupTable
@register_model_view(SiteGroup, 'contacts')
class SiteGroupContactsView(ObjectContactsView):
queryset = SiteGroup.objects.all()
# #
# Sites # Sites
# #
@ -435,6 +446,11 @@ class SiteBulkDeleteView(generic.BulkDeleteView):
table = tables.SiteTable table = tables.SiteTable
@register_model_view(Site, 'contacts')
class SiteContactsView(ObjectContactsView):
queryset = Site.objects.all()
# #
# Locations # Locations
# #
@ -523,6 +539,11 @@ class LocationBulkDeleteView(generic.BulkDeleteView):
table = tables.LocationTable table = tables.LocationTable
@register_model_view(Location, 'contacts')
class LocationContactsView(ObjectContactsView):
queryset = Location.objects.all()
# #
# Rack roles # Rack roles
# #
@ -740,6 +761,11 @@ class RackBulkDeleteView(generic.BulkDeleteView):
table = tables.RackTable table = tables.RackTable
@register_model_view(Rack, 'contacts')
class RackContactsView(ObjectContactsView):
queryset = Rack.objects.all()
# #
# Rack reservations # Rack reservations
# #
@ -874,6 +900,11 @@ class ManufacturerBulkDeleteView(generic.BulkDeleteView):
table = tables.ManufacturerTable table = tables.ManufacturerTable
@register_model_view(Manufacturer, 'contacts')
class ManufacturerContactsView(ObjectContactsView):
queryset = Manufacturer.objects.all()
# #
# Device types # Device types
# #
@ -2088,6 +2119,11 @@ class DeviceBulkRenameView(generic.BulkRenameView):
table = tables.DeviceTable table = tables.DeviceTable
@register_model_view(Device, 'contacts')
class DeviceContactsView(ObjectContactsView):
queryset = Device.objects.all()
# #
# Modules # Modules
# #
@ -3469,6 +3505,11 @@ class PowerPanelBulkDeleteView(generic.BulkDeleteView):
table = tables.PowerPanelTable table = tables.PowerPanelTable
@register_model_view(PowerPanel, 'contacts')
class PowerPanelContactsView(ObjectContactsView):
queryset = PowerPanel.objects.all()
# #
# Power feeds # Power feeds
# #

View File

@ -9,6 +9,7 @@ from circuits.models import Provider
from dcim.filtersets import InterfaceFilterSet from dcim.filtersets import InterfaceFilterSet
from dcim.models import Interface, Site from dcim.models import Interface, Site
from netbox.views import generic from netbox.views import generic
from tenancy.views import ObjectContactsView
from utilities.utils import count_related from utilities.utils import count_related
from utilities.views import ViewTab, register_model_view from utilities.views import ViewTab, register_model_view
from virtualization.filtersets import VMInterfaceFilterSet from virtualization.filtersets import VMInterfaceFilterSet
@ -1300,6 +1301,11 @@ class L2VPNBulkDeleteView(generic.BulkDeleteView):
table = tables.L2VPNTable table = tables.L2VPNTable
@register_model_view(L2VPN, 'contacts')
class L2VPNContactsView(ObjectContactsView):
queryset = L2VPN.objects.all()
# #
# L2VPN terminations # L2VPN terminations
# #

View File

@ -70,7 +70,6 @@
<div class="col col-md-6"> <div class="col col-md-6">
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_a side='A' %} {% include 'circuits/inc/circuit_termination.html' with termination=object.termination_a side='A' %}
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_z side='Z' %} {% include 'circuits/inc/circuit_termination.html' with termination=object.termination_z side='Z' %}
{% include 'inc/panels/contacts.html' %}
{% include 'inc/panels/image_attachments.html' %} {% include 'inc/panels/image_attachments.html' %}
{% plugin_right_page object %} {% plugin_right_page object %}
</div> </div>

View File

@ -43,7 +43,6 @@
<div class="col col-md-6"> <div class="col col-md-6">
{% include 'inc/panels/related_objects.html' %} {% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/custom_fields.html' %} {% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/contacts.html' %}
{% plugin_right_page object %} {% plugin_right_page object %}
</div> </div>
</div> </div>

View File

@ -38,7 +38,6 @@
{% include 'inc/panels/related_objects.html' %} {% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/comments.html' %} {% include 'inc/panels/comments.html' %}
{% include 'inc/panels/custom_fields.html' %} {% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/contacts.html' %}
{% plugin_right_page object %} {% plugin_right_page object %}
</div> </div>
<div class="col col-md-12"> <div class="col col-md-12">

View File

@ -298,7 +298,6 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% include 'inc/panels/contacts.html' %}
{% include 'inc/panels/image_attachments.html' %} {% include 'inc/panels/image_attachments.html' %}
<div class="card"> <div class="card">
<h5 class="card-header">Dimensions</h5> <h5 class="card-header">Dimensions</h5>

View File

@ -65,7 +65,6 @@
</div> </div>
<div class="col col-md-6"> <div class="col col-md-6">
{% include 'inc/panels/related_objects.html' %} {% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/contacts.html' %}
{% include 'dcim/inc/nonracked_devices.html' %} {% include 'dcim/inc/nonracked_devices.html' %}
{% include 'inc/panels/image_attachments.html' %} {% include 'inc/panels/image_attachments.html' %}
{% plugin_right_page object %} {% plugin_right_page object %}

View File

@ -51,7 +51,6 @@
<div class="col col-md-6"> <div class="col col-md-6">
{% include 'inc/panels/related_objects.html' %} {% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/custom_fields.html' %} {% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/contacts.html' %}
{% plugin_right_page object %} {% plugin_right_page object %}
</div> </div>
</div> </div>

View File

@ -40,7 +40,6 @@
<div class="col col-md-6"> <div class="col col-md-6">
{% include 'inc/panels/related_objects.html' %} {% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/custom_fields.html' %} {% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/contacts.html' %}
{% include 'inc/panels/image_attachments.html' %} {% include 'inc/panels/image_attachments.html' %}
{% plugin_right_page object %} {% plugin_right_page object %}
</div> </div>

View File

@ -191,7 +191,6 @@
</div> </div>
{% include 'inc/panels/related_objects.html' %} {% include 'inc/panels/related_objects.html' %}
{% include 'dcim/inc/nonracked_devices.html' %} {% include 'dcim/inc/nonracked_devices.html' %}
{% include 'inc/panels/contacts.html' %}
{% plugin_right_page object %} {% plugin_right_page object %}
</div> </div>
</div> </div>

View File

@ -46,7 +46,6 @@
</div> </div>
<div class="col col-md-6"> <div class="col col-md-6">
{% include 'inc/panels/related_objects.html' %} {% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/contacts.html' %}
{% plugin_right_page object %} {% plugin_right_page object %}
</div> </div>
</div> </div>

View File

@ -131,7 +131,6 @@
</div> </div>
<div class="col col-md-6"> <div class="col col-md-6">
{% include 'inc/panels/related_objects.html' with filter_name='site_id' %} {% include 'inc/panels/related_objects.html' with filter_name='site_id' %}
{% include 'inc/panels/contacts.html' %}
<div class="card"> <div class="card">
<h5 class="card-header">Locations</h5> <h5 class="card-header">Locations</h5>
<div class='card-body'> <div class='card-body'>

View File

@ -42,7 +42,6 @@
</div> </div>
{% include 'inc/panels/tags.html' %} {% include 'inc/panels/tags.html' %}
{% include 'inc/panels/custom_fields.html' %} {% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/contacts.html' %}
{% plugin_left_page object %} {% plugin_left_page object %}
</div> </div>
<div class="col col-md-6"> <div class="col col-md-6">

View File

@ -1,63 +0,0 @@
{% load helpers %}
<div class="card">
<h5 class="card-header">Contacts</h5>
<div class="card-body">
{% with contacts=object.contacts.all %}
{% if contacts.exists %}
<table class="table table-hover">
<tr>
<th>Name</th>
<th>Role</th>
<th>Priority</th>
<th>Phone</th>
<th>Email</th>
<th></th>
</tr>
{% for contact in contacts %}
<tr>
<td>{{ contact.contact|linkify }}</td>
<td>{{ contact.role|placeholder }}</td>
<td>{{ contact.get_priority_display|placeholder }}</td>
<td>
{% if contact.contact.phone %}
<a href="tel:{{ contact.contact.phone }}">{{ contact.contact.phone }}</a>
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
<td>
{% if contact.contact.email %}
<a href="mailto:{{ contact.contact.email }}">{{ contact.contact.email }}</a>
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
<td class="text-end noprint">
{% if perms.tenancy.change_contactassignment %}
<a href="{% url 'tenancy:contactassignment_edit' pk=contact.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-warning btn-sm lh-1" title="Edit">
<i class="mdi mdi-pencil" aria-hidden="true"></i>
</a>
{% endif %}
{% if perms.tenancy.delete_contactassignment %}
<a href="{% url 'tenancy:contactassignment_delete' pk=contact.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-danger btn-sm lh-1" title="Delete">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i>
</a>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% else %}
<div class="text-muted">None</div>
{% endif %}
{% endwith %}
</div>
{% if perms.tenancy.add_contactassignment %}
<div class="card-footer text-end noprint">
<a href="{% url 'tenancy:contactassignment_add' %}?content_type={{ object|content_type_id }}&object_id={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add a contact
</a>
</div>
{% endif %}
</div>

View File

@ -37,7 +37,6 @@
{% plugin_left_page object %} {% plugin_left_page object %}
</div> </div>
<div class="col col-md-6"> <div class="col col-md-6">
{% include 'inc/panels/contacts.html' %}
{% include 'inc/panels/custom_fields.html' %} {% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/comments.html' %} {% include 'inc/panels/comments.html' %}
{% plugin_right_page object %} {% plugin_right_page object %}

View File

@ -0,0 +1,27 @@
{% extends base_template %}
{% load helpers %}
{% block extra_controls %}
{% if perms.tenancy.add_contactassignment %}
<a href="{% url 'tenancy:contactassignment_add' %}?content_type={{ object|content_type_id }}&object_id={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add a contact
</a>
{% endif %}
{% endblock %}
{% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="ContactTable_config" %}
<form method="post">
{% csrf_token %}
<div class="card">
<div class="card-body" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
</form>
{% endblock content %}
{% block modals %}
{{ block.super }}
{% table_config_form table %}
{% endblock modals %}

View File

@ -30,7 +30,6 @@
{% include 'inc/panels/custom_fields.html' %} {% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %} {% include 'inc/panels/tags.html' %}
{% include 'inc/panels/comments.html' %} {% include 'inc/panels/comments.html' %}
{% include 'inc/panels/contacts.html' %}
{% plugin_left_page object %} {% plugin_left_page object %}
</div> </div>
<div class="col col-md-5"> <div class="col col-md-5">

View File

@ -84,7 +84,6 @@
</div> </div>
{% include 'inc/panels/custom_fields.html' %} {% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %} {% include 'inc/panels/tags.html' %}
{% include 'inc/panels/contacts.html' %}
{% plugin_right_page object %} {% plugin_right_page object %}
</div> </div>
</div> </div>

View File

@ -37,7 +37,6 @@
<div class="col col-md-6"> <div class="col col-md-6">
{% include 'inc/panels/related_objects.html' %} {% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/custom_fields.html' %} {% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/contacts.html' %}
{% plugin_right_page object %} {% plugin_right_page object %}
</div> </div>
</div> </div>

View File

@ -158,7 +158,6 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% include 'inc/panels/contacts.html' %}
{% plugin_right_page object %} {% plugin_right_page object %}
</div> </div>
</div> </div>

View File

@ -7,17 +7,40 @@ from dcim.models import Cable, Device, Location, Rack, RackReservation, Site, Vi
from ipam.models import Aggregate, ASN, IPAddress, IPRange, L2VPN, Prefix, VLAN, VRF from ipam.models import Aggregate, ASN, IPAddress, IPRange, L2VPN, Prefix, VLAN, VRF
from netbox.views import generic from netbox.views import generic
from utilities.utils import count_related from utilities.utils import count_related
from utilities.views import register_model_view from utilities.views import register_model_view, ViewTab
from virtualization.models import VirtualMachine, Cluster from virtualization.models import VirtualMachine, Cluster
from wireless.models import WirelessLAN, WirelessLink from wireless.models import WirelessLAN, WirelessLink
from . import filtersets, forms, tables from . import filtersets, forms, tables
from .models import * from .models import *
class ObjectContactsView(generic.ObjectChildrenView):
child_model = Contact
table = tables.ContactTable
filterset = filtersets.ContactFilterSet
template_name = 'tenancy/object_contacts.html'
tab = ViewTab(
label=_('Contacts'),
badge=lambda obj: obj.contacts.count(),
permission='tenancy.view_contact',
weight=5000
)
def get_children(self, request, parent):
return Contact.objects.annotate(
assignment_count=count_related(ContactAssignment, 'contact')
).restrict(request.user, 'view').filter(assignments__object_id=parent.pk)
def get_extra_context(self, request, instance):
return {
'base_template': f'{instance._meta.app_label}/{instance._meta.model_name}.html',
}
# #
# Tenant groups # Tenant groups
# #
class TenantGroupListView(generic.ObjectListView): class TenantGroupListView(generic.ObjectListView):
queryset = TenantGroup.objects.add_related_count( queryset = TenantGroup.objects.add_related_count(
TenantGroup.objects.all(), TenantGroup.objects.all(),
@ -165,6 +188,11 @@ class TenantBulkDeleteView(generic.BulkDeleteView):
table = tables.TenantTable table = tables.TenantTable
@register_model_view(Tenant, 'contacts')
class TenantContactsView(ObjectContactsView):
queryset = Tenant.objects.all()
# #
# Contact groups # Contact groups
# #
@ -342,11 +370,11 @@ class ContactBulkDeleteView(generic.BulkDeleteView):
filterset = filtersets.ContactFilterSet filterset = filtersets.ContactFilterSet
table = tables.ContactTable table = tables.ContactTable
# #
# Contact assignments # Contact assignments
# #
class ContactAssignmentListView(generic.ObjectListView): class ContactAssignmentListView(generic.ObjectListView):
queryset = ContactAssignment.objects.all() queryset = ContactAssignment.objects.all()
filterset = filtersets.ContactAssignmentFilterSet filterset = filtersets.ContactAssignmentFilterSet

View File

@ -9,9 +9,10 @@ from dcim.filtersets import DeviceFilterSet
from dcim.models import Device from dcim.models import Device
from dcim.tables import DeviceTable from dcim.tables import DeviceTable
from extras.views import ObjectConfigContextView from extras.views import ObjectConfigContextView
from ipam.models import IPAddress, Service from ipam.models import IPAddress
from ipam.tables import InterfaceVLANTable from ipam.tables import InterfaceVLANTable
from netbox.views import generic from netbox.views import generic
from tenancy.views import ObjectContactsView
from utilities.utils import count_related from utilities.utils import count_related
from utilities.views import ViewTab, register_model_view from utilities.views import ViewTab, register_model_view
from . import filtersets, forms, tables from . import filtersets, forms, tables
@ -140,6 +141,11 @@ class ClusterGroupBulkDeleteView(generic.BulkDeleteView):
table = tables.ClusterGroupTable table = tables.ClusterGroupTable
@register_model_view(ClusterGroup, 'contacts')
class ClusterGroupContactsView(ObjectContactsView):
queryset = ClusterGroup.objects.all()
# #
# Clusters # Clusters
# #
@ -312,6 +318,11 @@ class ClusterRemoveDevicesView(generic.ObjectEditView):
}) })
@register_model_view(Cluster, 'contacts')
class ClusterContactsView(ObjectContactsView):
queryset = Cluster.objects.all()
# #
# Virtual machines # Virtual machines
# #
@ -390,6 +401,11 @@ class VirtualMachineBulkDeleteView(generic.BulkDeleteView):
table = tables.VirtualMachineTable table = tables.VirtualMachineTable
@register_model_view(VirtualMachine, 'contacts')
class VirtualMachineContactsView(ObjectContactsView):
queryset = VirtualMachine.objects.all()
# #
# VM interfaces # VM interfaces
# #