Просмотр исходного кода

Add contact assignments to models

jeremystretch 4 лет назад
Родитель
Сommit
f193f0d3f9

+ 10 - 0
netbox/circuits/models.py

@@ -62,6 +62,11 @@ class Provider(PrimaryModel):
         blank=True
     )
 
+    # Generic relations
+    contacts = GenericRelation(
+        to='tenancy.ContactAssignment'
+    )
+
     objects = RestrictedQuerySet.as_manager()
 
     clone_fields = [
@@ -203,6 +208,11 @@ class Circuit(PrimaryModel):
     comments = models.TextField(
         blank=True
     )
+
+    # Generic relations
+    contacts = GenericRelation(
+        to='tenancy.ContactAssignment'
+    )
     images = GenericRelation(
         to='extras.ImageAttachment'
     )

+ 10 - 0
netbox/dcim/models/devices.py

@@ -54,6 +54,11 @@ class Manufacturer(OrganizationalModel):
         blank=True
     )
 
+    # Generic relations
+    contacts = GenericRelation(
+        to='tenancy.ContactAssignment'
+    )
+
     objects = RestrictedQuerySet.as_manager()
 
     class Meta:
@@ -584,6 +589,11 @@ class Device(PrimaryModel, ConfigContextModel):
     comments = models.TextField(
         blank=True
     )
+
+    # Generic relations
+    contacts = GenericRelation(
+        to='tenancy.ContactAssignment'
+    )
     images = GenericRelation(
         to='extras.ImageAttachment'
     )

+ 5 - 0
netbox/dcim/models/power.py

@@ -40,6 +40,11 @@ class PowerPanel(PrimaryModel):
     name = models.CharField(
         max_length=100
     )
+
+    # Generic relations
+    contacts = GenericRelation(
+        to='tenancy.ContactAssignment'
+    )
     images = GenericRelation(
         to='extras.ImageAttachment'
     )

+ 5 - 0
netbox/dcim/models/racks.py

@@ -175,12 +175,17 @@ class Rack(PrimaryModel):
     comments = models.TextField(
         blank=True
     )
+
+    # Generic relations
     vlan_groups = GenericRelation(
         to='ipam.VLANGroup',
         content_type_field='scope_type',
         object_id_field='scope_id',
         related_query_name='rack'
     )
+    contacts = GenericRelation(
+        to='tenancy.ContactAssignment'
+    )
     images = GenericRelation(
         to='extras.ImageAttachment'
     )

+ 20 - 0
netbox/dcim/models/sites.py

@@ -52,12 +52,17 @@ class Region(NestedGroupModel):
         max_length=200,
         blank=True
     )
+
+    # Generic relations
     vlan_groups = GenericRelation(
         to='ipam.VLANGroup',
         content_type_field='scope_type',
         object_id_field='scope_id',
         related_query_name='region'
     )
+    contacts = GenericRelation(
+        to='tenancy.ContactAssignment'
+    )
 
     def get_absolute_url(self):
         return reverse('dcim:region', args=[self.pk])
@@ -100,12 +105,17 @@ class SiteGroup(NestedGroupModel):
         max_length=200,
         blank=True
     )
+
+    # Generic relations
     vlan_groups = GenericRelation(
         to='ipam.VLANGroup',
         content_type_field='scope_type',
         object_id_field='scope_id',
         related_query_name='site_group'
     )
+    contacts = GenericRelation(
+        to='tenancy.ContactAssignment'
+    )
 
     def get_absolute_url(self):
         return reverse('dcim:sitegroup', args=[self.pk])
@@ -221,12 +231,17 @@ class Site(PrimaryModel):
     comments = models.TextField(
         blank=True
     )
+
+    # Generic relations
     vlan_groups = GenericRelation(
         to='ipam.VLANGroup',
         content_type_field='scope_type',
         object_id_field='scope_id',
         related_query_name='site'
     )
+    contacts = GenericRelation(
+        to='tenancy.ContactAssignment'
+    )
     images = GenericRelation(
         to='extras.ImageAttachment'
     )
@@ -291,12 +306,17 @@ class Location(NestedGroupModel):
         max_length=200,
         blank=True
     )
+
+    # Generic relations
     vlan_groups = GenericRelation(
         to='ipam.VLANGroup',
         content_type_field='scope_type',
         object_id_field='scope_id',
         related_query_name='location'
     )
+    contacts = GenericRelation(
+        to='tenancy.ContactAssignment'
+    )
     images = GenericRelation(
         to='extras.ImageAttachment'
     )

+ 6 - 5
netbox/templates/circuits/circuit.html

@@ -70,11 +70,12 @@
         {% plugin_left_page object %}
 	</div>
 	<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_z side='Z' %}
-        {% include 'inc/image_attachments_panel.html' %}
-        {% plugin_right_page object %}
-    </div>
+    {% 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 'inc/contacts_panel.html' %}
+    {% include 'inc/image_attachments_panel.html' %}
+    {% plugin_right_page object %}
+  </div>
 </div>
 <div class="row">
     <div class="col col-md-12">

+ 2 - 1
netbox/templates/circuits/provider.html

@@ -47,12 +47,13 @@
                 </table>
             </div>
         </div>
+        {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='circuits:provider_list' %}
         {% plugin_left_page object %}
     </div>
     <div class="col col-md-6">
         {% include 'inc/custom_fields_panel.html' %}
-        {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='circuits:provider_list' %}
         {% include 'inc/comments_panel.html' %}
+        {% include 'inc/contacts_panel.html' %}
         {% plugin_right_page object %}
     </div>
     <div class="col col-md-12">

+ 1 - 0
netbox/templates/dcim/device.html

@@ -296,6 +296,7 @@
                 </div>
                 {% endif %}
             </div>
+            {% include 'inc/contacts_panel.html' %}
             {% include 'inc/image_attachments_panel.html' %}
             <div class="card noprint">
                 <h5 class="card-header">

+ 1 - 0
netbox/templates/dcim/location.html

@@ -72,6 +72,7 @@
   </div>
 	<div class="col col-md-6">
     {% include 'inc/custom_fields_panel.html' %}
+    {% include 'inc/contacts_panel.html' %}
     {% include 'inc/image_attachments_panel.html' %}
     {% plugin_right_page object %}
 	</div>

+ 1 - 0
netbox/templates/dcim/manufacturer.html

@@ -38,6 +38,7 @@
 	</div>
 	<div class="col col-md-6">
     {% include 'inc/custom_fields_panel.html' %}
+    {% include 'inc/contacts_panel.html' %}
     {% plugin_right_page object %}
   </div>
 </div>

+ 1 - 0
netbox/templates/dcim/powerpanel.html

@@ -44,6 +44,7 @@
     </div>
 	<div class="col col-md-6">
         {% include 'inc/custom_fields_panel.html' %}
+        {% include 'inc/contacts_panel.html' %}
         {% include 'inc/image_attachments_panel.html' %}
         {% plugin_right_page object %}
     </div>

+ 1 - 0
netbox/templates/dcim/rack.html

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

+ 1 - 0
netbox/templates/dcim/region.html

@@ -46,6 +46,7 @@
       </div>
     </div>
     {% include 'inc/custom_fields_panel.html' %}
+    {% include 'inc/contacts_panel.html' %}
     {% plugin_left_page object %}
   </div>
 	<div class="col col-md-6">

+ 52 - 37
netbox/templates/dcim/site.html

@@ -76,6 +76,10 @@
                         <th scope="row">Facility</th>
                         <td>{{ object.facility|placeholder }}</td>
                     </tr>
+                    <tr>
+                        <th scope="row">Description</th>
+                        <td>{{ object.description|placeholder }}</td>
+                    </tr>
                     <tr>
                         <th scope="row">AS Number</th>
                         <td>{{ object.asn|placeholder }}</td>
@@ -91,19 +95,6 @@
                             {% endif %}
                         </td>
                     </tr>
-                    <tr>
-                        <th scope="row">Description</th>
-                        <td>{{ object.description|placeholder }}</td>
-                    </tr>
-                </table>
-            </div>
-        </div>
-        <div class="card">
-            <h5 class="card-header">
-                Contact Info
-            </h5>
-            <div class="card-body">
-                <table class="table table-hover attr-table">
                     <tr>
                         <th scope="row">Physical Address</th>
                         <td>
@@ -138,33 +129,57 @@
                             {% endif %}
                         </td>
                     </tr>
-                    <tr>
-                        <th scope="row">Contact Name</th>
-                        <td>{{ object.contact_name|placeholder }}</td>
-                    </tr>
-                    <tr>
-                        <th scope="row">Contact Phone</th>
-                        <td>
-                            {% if object.contact_phone %}
-                                <a href="tel:{{ object.contact_phone }}">{{ object.contact_phone }}</a>
-                            {% else %}
-                                <span class="text-muted">&mdash;</span>
-                            {% endif %}
-                        </td>
-                    </tr>
-                    <tr>
-                        <th scope="row">Contact E-Mail</th>
-                        <td>
-                            {% if object.contact_email %}
-                                <a href="mailto:{{ object.contact_email }}">{{ object.contact_email }}</a>
-                            {% else %}
-                                <span class="text-muted">&mdash;</span>
-                            {% endif %}
-                        </td>
-                    </tr>
                 </table>
             </div>
         </div>
+        {% include 'inc/contacts_panel.html' %}
+        <div class="card">
+            <h5 class="card-header">Contact Info</h5>
+            <div class="card-body">
+              {% with deprecation_warning="This field will be removed in a future release. Please migrate this data to contact objects." %}
+                <table class="table table-hover attr-table">
+                  <tr>
+                    <th scope="row">Contact Name</th>
+                    <td>
+                      {% if object.contact_name %}
+                        <div class="float-end text-warning">
+                          <i class="mdi mdi-alert" title="{{ deprecation_warning }}"></i>
+                        </div>
+                      {% endif %}
+                      {{ object.contact_name|placeholder }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <th scope="row">Contact Phone</th>
+                    <td>
+                      {% if object.contact_phone %}
+                        <div class="float-end text-warning">
+                          <i class="mdi mdi-alert" title="{{ deprecation_warning }}"></i>
+                        </div>
+                        <a href="tel:{{ object.contact_phone }}">{{ object.contact_phone }}</a>
+                      {% else %}
+                        <span class="text-muted">&mdash;</span>
+                      {% endif %}
+                    </td>
+                  </tr>
+                  <tr>
+                    <th scope="row">Contact E-Mail</th>
+                    <td>
+                      {% if object.contact_email %}
+                        <div class="float-end text-warning">
+                          <i class="mdi mdi-alert" title="{{ deprecation_warning }}"></i>
+                        </div>
+                        <a href="tel:{{ object.contact_
+                        <a href="mailto:{{ object.contact_email }}">{{ object.contact_email }}</a>
+                      {% else %}
+                        <span class="text-muted">&mdash;</span>
+                      {% endif %}
+                    </td>
+                  </tr>
+                </table>
+              {% endwith %}
+            </div>
+        </div>
         {% include 'inc/custom_fields_panel.html' %}
         {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='dcim:site_list' %}
         {% include 'inc/comments_panel.html' %}

+ 1 - 0
netbox/templates/dcim/sitegroup.html

@@ -46,6 +46,7 @@
       </div>
     </div>
     {% include 'inc/custom_fields_panel.html' %}
+    {% include 'inc/contacts_panel.html' %}
     {% plugin_left_page object %}
   </div>
 	<div class="col col-md-6">

+ 49 - 0
netbox/templates/inc/contacts_panel.html

@@ -0,0 +1,49 @@
+{% 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></th>
+          </tr>
+          {% for contact in contacts %}
+            <tr>
+              <td>
+                <a href="{{ contact.contact.get_absolute_url }}">{{ contact.contact }}</a>
+              </td>
+              <td>{{ contact.role|placeholder }}</td>
+              <td>{{ contact.get_priority_display|placeholder }}</td>
+              <td class="text-end noprint">
+                {% if perms.tenancy.change_contactassignment %}
+                  <a href="{% url 'tenancy:contactassignment_edit' pk=contact.pk %}" 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 'extras:imageattachment_delete' pk=contact.pk %}" 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|meta:"app_label" }}.{{ object|meta:"model_name" }}&object_id={{ object.pk }}" class="btn btn-primary btn-sm">
+        <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add a contact
+      </a>
+    </div>
+  {% endif %}
+</div>

+ 1 - 0
netbox/templates/tenancy/tenant.html

@@ -38,6 +38,7 @@
         {% include 'inc/custom_fields_panel.html' %}
         {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='tenancy:tenant_list' %}
         {% include 'inc/comments_panel.html' %}
+        {% include 'inc/contacts_panel.html' %}
         {% plugin_left_page object %}
 	</div>
 	<div class="col col-md-5">

+ 1 - 0
netbox/templates/virtualization/cluster.html

@@ -62,6 +62,7 @@
   <div class="col col-md-6">
     {% include 'inc/custom_fields_panel.html' %}
     {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='virtualization:cluster_list' %}
+    {% include 'inc/contacts_panel.html' %}
     {% plugin_right_page object %}
   </div>
 </div>

+ 1 - 0
netbox/templates/virtualization/clustergroup.html

@@ -32,6 +32,7 @@
 	</div>
 	<div class="col col-md-6">
     {% include 'inc/custom_fields_panel.html' %}
+    {% include 'inc/contacts_panel.html' %}
     {% plugin_right_page object %}
   </div>
 </div>

+ 1 - 0
netbox/templates/virtualization/virtualmachine.html

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

+ 26 - 0
netbox/tenancy/forms/models.py

@@ -1,11 +1,15 @@
+from django import forms
+
 from extras.forms import CustomFieldModelForm
 from extras.models import Tag
 from tenancy.models import *
 from utilities.forms import (
     BootstrapMixin, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField, SmallTextarea,
+    StaticSelect,
 )
 
 __all__ = (
+    'ContactAssignmentForm',
     'ContactForm',
     'ContactGroupForm',
     'ContactRoleForm',
@@ -100,3 +104,25 @@ class ContactForm(BootstrapMixin, CustomFieldModelForm):
         widgets = {
             'address': SmallTextarea(attrs={'rows': 3}),
         }
+
+
+class ContactAssignmentForm(BootstrapMixin, forms.ModelForm):
+    group = DynamicModelChoiceField(
+        queryset=ContactGroup.objects.all(),
+        required=False
+    )
+    contact = DynamicModelChoiceField(
+        queryset=Contact.objects.all()
+    )
+    role = DynamicModelChoiceField(
+        queryset=ContactRole.objects.all()
+    )
+
+    class Meta:
+        model = ContactAssignment
+        fields = (
+            'group', 'contact', 'role', 'priority',
+        )
+        widgets = {
+            'priority': StaticSelect(),
+        }

+ 6 - 1
netbox/tenancy/models.py

@@ -1,4 +1,4 @@
-from django.contrib.contenttypes.fields import GenericForeignKey
+from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
 from django.contrib.contenttypes.models import ContentType
 from django.db import models
 from django.urls import reverse
@@ -86,6 +86,11 @@ class Tenant(PrimaryModel):
         blank=True
     )
 
+    # Generic relations
+    contacts = GenericRelation(
+        to='tenancy.ContactAssignment'
+    )
+
     objects = RestrictedQuerySet.as_manager()
 
     clone_fields = [

+ 5 - 0
netbox/tenancy/urls.py

@@ -67,4 +67,9 @@ urlpatterns = [
     path('contacts/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='contact_changelog', kwargs={'model': Contact}),
     path('contacts/<int:pk>/journal/', ObjectJournalView.as_view(), name='contact_journal', kwargs={'model': Contact}),
 
+    # Contact assignments
+    path('contact-assignments/add/', views.ContactAssignmentEditView.as_view(), name='contactassignment_add'),
+    path('contact-assignments/<int:pk>/edit/', views.ContactAssignmentEditView.as_view(), name='contactassignment_edit'),
+    path('contact-assignments/<int:pk>/delete/', views.ContactAssignmentDeleteView.as_view(), name='contactassignment_delete'),
+
 ]

+ 34 - 1
netbox/tenancy/views.py

@@ -1,9 +1,12 @@
+from django.contrib.contenttypes.models import ContentType
+from django.http import Http404
+from django.shortcuts import get_object_or_404
+
 from circuits.models import Circuit
 from dcim.models import Site, Rack, Device, RackReservation
 from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF
 from netbox.views import generic
 from utilities.tables import paginate_table
-from utilities.utils import count_related
 from virtualization.models import VirtualMachine, Cluster
 from . import filtersets, forms, tables
 from .models import *
@@ -309,3 +312,33 @@ class ContactBulkDeleteView(generic.BulkDeleteView):
     queryset = Contact.objects.prefetch_related('group')
     filterset = filtersets.ContactFilterSet
     table = tables.ContactTable
+
+
+#
+# Contact assignments
+#
+
+class ContactAssignmentEditView(generic.ObjectEditView):
+    queryset = ContactAssignment.objects.all()
+    model_form = forms.ContactAssignmentForm
+
+    def alter_obj(self, instance, request, args, kwargs):
+        if not instance.pk:
+            # Assign the object based on URL kwargs
+            try:
+                app_label, model = request.GET.get('content_type').split('.')
+            except (AttributeError, ValueError):
+                raise Http404("Content type not specified")
+            content_type = get_object_or_404(ContentType, app_label=app_label, model=model)
+            instance.object = get_object_or_404(content_type.model_class(), pk=request.GET.get('object_id'))
+        return instance
+
+    def get_return_url(self, request, obj=None):
+        return obj.object.get_absolute_url() if obj else super().get_return_url(request)
+
+
+class ContactAssignmentDeleteView(generic.ObjectDeleteView):
+    queryset = ContactAssignment.objects.all()
+
+    def get_return_url(self, request, obj=None):
+        return obj.object.get_absolute_url() if obj else super().get_return_url(request)

+ 15 - 0
netbox/virtualization/models.py

@@ -81,12 +81,17 @@ class ClusterGroup(OrganizationalModel):
         max_length=200,
         blank=True
     )
+
+    # Generic relations
     vlan_groups = GenericRelation(
         to='ipam.VLANGroup',
         content_type_field='scope_type',
         object_id_field='scope_id',
         related_query_name='cluster_group'
     )
+    contacts = GenericRelation(
+        to='tenancy.ContactAssignment'
+    )
 
     objects = RestrictedQuerySet.as_manager()
 
@@ -142,12 +147,17 @@ class Cluster(PrimaryModel):
     comments = models.TextField(
         blank=True
     )
+
+    # Generic relations
     vlan_groups = GenericRelation(
         to='ipam.VLANGroup',
         content_type_field='scope_type',
         object_id_field='scope_id',
         related_query_name='cluster'
     )
+    contacts = GenericRelation(
+        to='tenancy.ContactAssignment'
+    )
 
     objects = RestrictedQuerySet.as_manager()
 
@@ -268,6 +278,11 @@ class VirtualMachine(PrimaryModel, ConfigContextModel):
         blank=True
     )
 
+    # Generic relation
+    contacts = GenericRelation(
+        to='tenancy.ContactAssignment'
+    )
+
     objects = ConfigContextModelQuerySet.as_manager()
 
     clone_fields = [