mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -06:00
Merge pull request #7575 from netbox-community/1344-contacts
Closes #1344: Contact objects
This commit is contained in:
commit
ba7361bdc7
5
docs/core-functionality/contacts.md
Normal file
5
docs/core-functionality/contacts.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Contacts
|
||||
|
||||
{!models/tenancy/contact.md!}
|
||||
{!models/tenancy/contactgroup.md!}
|
||||
{!models/tenancy/contactrole.md!}
|
31
docs/models/tenancy/contact.md
Normal file
31
docs/models/tenancy/contact.md
Normal file
@ -0,0 +1,31 @@
|
||||
# Contacts
|
||||
|
||||
A contact represent an individual or group that has been associated with an object in NetBox for administrative reasons. For example, you might assign one or more operational contacts to each site. Contacts can be arranged within nested contact groups.
|
||||
|
||||
Each contact must include a name, which is unique to its parent group (if any). The following optional descriptors are also available:
|
||||
|
||||
* Title
|
||||
* Phone
|
||||
* Email
|
||||
* Address
|
||||
|
||||
## Contact Assignment
|
||||
|
||||
Each contact can be assigned to one or more objects, allowing for the efficient reuse of contact information. When assigning a contact to an object, the user may optionally specify a role and/or priority (primary, secondary, tertiary, or inactive) to better convey the nature of the contact's relationship to the assigned object.
|
||||
|
||||
The following models support the assignment of contacts:
|
||||
|
||||
* circuits.Circuit
|
||||
* circuits.Provider
|
||||
* dcim.Device
|
||||
* dcim.Location
|
||||
* dcim.Manufacturer
|
||||
* dcim.PowerPanel
|
||||
* dcim.Rack
|
||||
* dcim.Region
|
||||
* dcim.Site
|
||||
* dcim.SiteGroup
|
||||
* tenancy.Tenant
|
||||
* virtualization.Cluster
|
||||
* virtualization.ClusterGroup
|
||||
* virtualization.VirtualMachine
|
3
docs/models/tenancy/contactgroup.md
Normal file
3
docs/models/tenancy/contactgroup.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Contact Groups
|
||||
|
||||
Contacts can be organized into arbitrary groups. These groups can be recursively nested for convenience. Each contact within a group must have a unique name, but other attributes can be repeated.
|
3
docs/models/tenancy/contactrole.md
Normal file
3
docs/models/tenancy/contactrole.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Contact Roles
|
||||
|
||||
Contacts can be organized by functional roles, which are fully customizable by the user. For example, you might create roles for administrative, operational, or emergency contacts.
|
@ -62,6 +62,7 @@ nav:
|
||||
- Circuits: 'core-functionality/circuits.md'
|
||||
- Power Tracking: 'core-functionality/power.md'
|
||||
- Tenancy: 'core-functionality/tenancy.md'
|
||||
- Contacts: 'core-functionality/contacts.md'
|
||||
- Customization:
|
||||
- Custom Fields: 'customization/custom-fields.md'
|
||||
- Custom Validation: 'customization/custom-validation.md'
|
||||
|
@ -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'
|
||||
)
|
||||
|
@ -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'
|
||||
)
|
||||
|
@ -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'
|
||||
)
|
||||
|
@ -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'
|
||||
)
|
||||
|
@ -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'
|
||||
)
|
||||
|
@ -120,6 +120,14 @@ ORGANIZATION_MENU = Menu(
|
||||
get_model_item('tenancy', 'tenantgroup', 'Tenant Groups'),
|
||||
),
|
||||
),
|
||||
MenuGroup(
|
||||
label='Contacts',
|
||||
items=(
|
||||
get_model_item('tenancy', 'contact', 'Contacts'),
|
||||
get_model_item('tenancy', 'contactgroup', 'Contact Groups'),
|
||||
get_model_item('tenancy', 'contactrole', 'Contact Roles'),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -332,6 +332,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'inc/contacts_panel.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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">
|
||||
|
@ -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">—</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">—</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">—</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">—</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' %}
|
||||
|
@ -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
netbox/templates/inc/contacts_panel.html
Normal file
49
netbox/templates/inc/contacts_panel.html
Normal file
@ -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>
|
79
netbox/templates/tenancy/contact.html
Normal file
79
netbox/templates/tenancy/contact.html
Normal file
@ -0,0 +1,79 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
{% if object.group %}
|
||||
<li class="breadcrumb-item"><a href="{% url 'tenancy:contact_list' %}?group_id={{ object.group.pk }}">{{ object.group }}</a></li>
|
||||
{% endif %}
|
||||
{% endblock breadcrumbs %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-md-7">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Tenant</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<td>Group</td>
|
||||
<td>
|
||||
{% if object.group %}
|
||||
<a href="{{ object.group.get_absolute_url }}">{{ object.group }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Title</td>
|
||||
<td>{{ object.tile|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Phone</td>
|
||||
<td>{{ object.phone|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Email</td>
|
||||
<td>{{ object.email|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Address</td>
|
||||
<td>{{ object.address|linebreaksbr|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Assignments</th>
|
||||
<td>
|
||||
<a href="{% url 'tenancy:contact_list' %}?contact_id={{ object.pk }}">{{ assignment_count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/comments_panel.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-5">
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='tenancy:tenant_list' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Assignments</h5>
|
||||
<div class="card-body">
|
||||
{% include 'inc/table.html' with table=contacts_table %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/paginator.html' with paginator=contacts_table.paginator page=contacts_table.page %}
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
76
netbox/templates/tenancy/contactgroup.html
Normal file
76
netbox/templates/tenancy/contactgroup.html
Normal file
@ -0,0 +1,76 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
{% for contactgroup in object.get_ancestors %}
|
||||
<li class="breadcrumb-item"><a href="{% url 'tenancy:contactgroup_list' %}?parent_id={{ contactgroup.pk }}">{{ contactgroup }}</a></li>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Contact Group
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Name</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Description</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Parent</th>
|
||||
<td>
|
||||
{% if object.parent %}
|
||||
<a href="{{ object.parent.get_absolute_url }}">{{ object.parent }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Contacts</th>
|
||||
<td>
|
||||
<a href="{% url 'tenancy:contact_list' %}?group_id={{ object.pk }}">{{ contacts_table.rows|length }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Tenants
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% include 'inc/table.html' with table=contacts_table %}
|
||||
</div>
|
||||
{% if perms.tenancy.add_contact %}
|
||||
<div class="card-footer text-end noprint">
|
||||
<a href="{% url 'tenancy:contact_add' %}?group={{ object.pk }}" class="btn btn-sm btn-primary">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Contact
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'inc/paginator.html' with paginator=contacts_table.paginator page=contacts_table.page %}
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
52
netbox/templates/tenancy/contactrole.html
Normal file
52
netbox/templates/tenancy/contactrole.html
Normal file
@ -0,0 +1,52 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<li class="breadcrumb-item"><a href="{% url 'tenancy:contactrole_list' %}">Contact Roles</a></li>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Contact Role</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Name</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Description</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Assignments</th>
|
||||
<td>
|
||||
<a href="{% url 'tenancy:contact_list' %}?role={{ object.slug }}">{{ assignment_count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Assigned Contacts</h5>
|
||||
<div class="card-body">
|
||||
{% include 'inc/table.html' with table=contacts_table %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/paginator.html' with paginator=contacts_table.paginator page=contacts_table.page %}
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -173,6 +173,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'inc/contacts_panel.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,9 +1,12 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from netbox.api import WritableNestedSerializer
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from tenancy.models import *
|
||||
|
||||
__all__ = [
|
||||
'NestedContactSerializer',
|
||||
'NestedContactGroupSerializer',
|
||||
'NestedContactRoleSerializer',
|
||||
'NestedTenantGroupSerializer',
|
||||
'NestedTenantSerializer',
|
||||
]
|
||||
@ -29,3 +32,33 @@ class NestedTenantSerializer(WritableNestedSerializer):
|
||||
class Meta:
|
||||
model = Tenant
|
||||
fields = ['id', 'url', 'display', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Contacts
|
||||
#
|
||||
|
||||
class NestedContactGroupSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactgroup-detail')
|
||||
contact_count = serializers.IntegerField(read_only=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ContactGroup
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'contact_count', '_depth']
|
||||
|
||||
|
||||
class NestedContactRoleSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactrole-detail')
|
||||
|
||||
class Meta:
|
||||
model = ContactRole
|
||||
fields = ['id', 'url', 'display', 'name', 'slug']
|
||||
|
||||
|
||||
class NestedContactSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contact-detail')
|
||||
|
||||
class Meta:
|
||||
model = Contact
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
@ -1,7 +1,10 @@
|
||||
from django.contrib.auth.models import ContentType
|
||||
from rest_framework import serializers
|
||||
|
||||
from netbox.api.serializers import NestedGroupModelSerializer, PrimaryModelSerializer
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from netbox.api import ChoiceField, ContentTypeField
|
||||
from netbox.api.serializers import NestedGroupModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer
|
||||
from tenancy.choices import ContactPriorityChoices
|
||||
from tenancy.models import *
|
||||
from .nested_serializers import *
|
||||
|
||||
|
||||
@ -43,3 +46,59 @@ class TenantSerializer(PrimaryModelSerializer):
|
||||
'created', 'last_updated', 'circuit_count', 'device_count', 'ipaddress_count', 'prefix_count', 'rack_count',
|
||||
'site_count', 'virtualmachine_count', 'vlan_count', 'vrf_count', 'cluster_count',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Contacts
|
||||
#
|
||||
|
||||
class ContactGroupSerializer(NestedGroupModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactgroup-detail')
|
||||
parent = NestedContactGroupSerializer(required=False, allow_null=True)
|
||||
contact_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ContactGroup
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'custom_fields', 'created', 'last_updated',
|
||||
'contact_count', '_depth',
|
||||
]
|
||||
|
||||
|
||||
class ContactRoleSerializer(OrganizationalModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactrole-detail')
|
||||
|
||||
class Meta:
|
||||
model = ContactRole
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'description', 'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
class ContactSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contact-detail')
|
||||
group = NestedContactGroupSerializer(required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = Contact
|
||||
fields = [
|
||||
'id', 'url', 'display', 'group', 'name', 'title', 'phone', 'email', 'address', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
class ContactAssignmentSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactassignment-detail')
|
||||
content_type = ContentTypeField(
|
||||
queryset=ContentType.objects.all()
|
||||
)
|
||||
contact = NestedContactSerializer()
|
||||
role = NestedContactRoleSerializer(required=False, allow_null=True)
|
||||
priority = ChoiceField(choices=ContactPriorityChoices, required=False)
|
||||
|
||||
class Meta:
|
||||
model = ContactAssignment
|
||||
fields = [
|
||||
'id', 'url', 'display', 'content_type', 'object_id', 'contact', 'role', 'priority', 'created',
|
||||
'last_updated',
|
||||
]
|
||||
|
@ -9,5 +9,11 @@ router.APIRootView = views.TenancyRootView
|
||||
router.register('tenant-groups', views.TenantGroupViewSet)
|
||||
router.register('tenants', views.TenantViewSet)
|
||||
|
||||
# Contacts
|
||||
router.register('contact-groups', views.ContactGroupViewSet)
|
||||
router.register('contact-roles', views.ContactRoleViewSet)
|
||||
router.register('contacts', views.ContactViewSet)
|
||||
router.register('contact-assignments', views.ContactAssignmentViewSet)
|
||||
|
||||
app_name = 'tenancy-api'
|
||||
urlpatterns = router.urls
|
||||
|
@ -5,7 +5,7 @@ from dcim.models import Device, Rack, Site
|
||||
from extras.api.views import CustomFieldModelViewSet
|
||||
from ipam.models import IPAddress, Prefix, VLAN, VRF
|
||||
from tenancy import filtersets
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from tenancy.models import *
|
||||
from utilities.utils import count_related
|
||||
from virtualization.models import VirtualMachine
|
||||
from . import serializers
|
||||
@ -20,7 +20,7 @@ class TenancyRootView(APIRootView):
|
||||
|
||||
|
||||
#
|
||||
# Tenant Groups
|
||||
# Tenants
|
||||
#
|
||||
|
||||
class TenantGroupViewSet(CustomFieldModelViewSet):
|
||||
@ -35,10 +35,6 @@ class TenantGroupViewSet(CustomFieldModelViewSet):
|
||||
filterset_class = filtersets.TenantGroupFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Tenants
|
||||
#
|
||||
|
||||
class TenantViewSet(CustomFieldModelViewSet):
|
||||
queryset = Tenant.objects.prefetch_related(
|
||||
'group', 'tags'
|
||||
@ -55,3 +51,41 @@ class TenantViewSet(CustomFieldModelViewSet):
|
||||
)
|
||||
serializer_class = serializers.TenantSerializer
|
||||
filterset_class = filtersets.TenantFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Contacts
|
||||
#
|
||||
|
||||
class ContactGroupViewSet(CustomFieldModelViewSet):
|
||||
queryset = ContactGroup.objects.add_related_count(
|
||||
ContactGroup.objects.all(),
|
||||
Contact,
|
||||
'group',
|
||||
'contact_count',
|
||||
cumulative=True
|
||||
)
|
||||
serializer_class = serializers.ContactGroupSerializer
|
||||
filterset_class = filtersets.ContactGroupFilterSet
|
||||
|
||||
|
||||
class ContactRoleViewSet(CustomFieldModelViewSet):
|
||||
queryset = ContactRole.objects.all()
|
||||
serializer_class = serializers.ContactRoleSerializer
|
||||
filterset_class = filtersets.ContactRoleFilterSet
|
||||
|
||||
|
||||
class ContactViewSet(CustomFieldModelViewSet):
|
||||
queryset = Contact.objects.prefetch_related(
|
||||
'group', 'tags'
|
||||
)
|
||||
serializer_class = serializers.ContactSerializer
|
||||
filterset_class = filtersets.ContactFilterSet
|
||||
|
||||
|
||||
class ContactAssignmentViewSet(CustomFieldModelViewSet):
|
||||
queryset = ContactAssignment.objects.prefetch_related(
|
||||
'contact', 'role'
|
||||
)
|
||||
serializer_class = serializers.ContactAssignmentSerializer
|
||||
filterset_class = filtersets.ContactAssignmentFilterSet
|
||||
|
19
netbox/tenancy/choices.py
Normal file
19
netbox/tenancy/choices.py
Normal file
@ -0,0 +1,19 @@
|
||||
from utilities.choices import ChoiceSet
|
||||
|
||||
|
||||
#
|
||||
# Contacts
|
||||
#
|
||||
|
||||
class ContactPriorityChoices(ChoiceSet):
|
||||
PRIORITY_PRIMARY = 'primary'
|
||||
PRIORITY_SECONDARY = 'secondary'
|
||||
PRIORITY_TERTIARY = 'tertiary'
|
||||
PRIORITY_INACTIVE = 'inactive'
|
||||
|
||||
CHOICES = (
|
||||
(PRIORITY_PRIMARY, 'Primary'),
|
||||
(PRIORITY_SECONDARY, 'Secondary'),
|
||||
(PRIORITY_TERTIARY, 'Tertiary'),
|
||||
(PRIORITY_INACTIVE, 'Inactive'),
|
||||
)
|
@ -2,18 +2,26 @@ import django_filters
|
||||
from django.db.models import Q
|
||||
|
||||
from extras.filters import TagFilter
|
||||
from netbox.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet
|
||||
from utilities.filters import TreeNodeMultipleChoiceFilter
|
||||
from .models import Tenant, TenantGroup
|
||||
from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
|
||||
from utilities.filters import ContentTypeFilter, TreeNodeMultipleChoiceFilter
|
||||
from .models import *
|
||||
|
||||
|
||||
__all__ = (
|
||||
'ContactAssignmentFilterSet',
|
||||
'ContactFilterSet',
|
||||
'ContactGroupFilterSet',
|
||||
'ContactRoleFilterSet',
|
||||
'TenancyFilterSet',
|
||||
'TenantFilterSet',
|
||||
'TenantGroupFilterSet',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Tenancy
|
||||
#
|
||||
|
||||
class TenantGroupFilterSet(OrganizationalModelFilterSet):
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=TenantGroup.objects.all(),
|
||||
@ -23,7 +31,7 @@ class TenantGroupFilterSet(OrganizationalModelFilterSet):
|
||||
field_name='parent__slug',
|
||||
queryset=TenantGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant group group (slug)',
|
||||
label='Tenant group (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@ -93,3 +101,90 @@ class TenancyFilterSet(django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Contacts
|
||||
#
|
||||
|
||||
class ContactGroupFilterSet(OrganizationalModelFilterSet):
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ContactGroup.objects.all(),
|
||||
label='Contact group (ID)',
|
||||
)
|
||||
parent = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='parent__slug',
|
||||
queryset=ContactGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Contact group (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ContactGroup
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
|
||||
|
||||
class ContactRoleFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ContactRole
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
class ContactFilterSet(PrimaryModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=ContactGroup.objects.all(),
|
||||
field_name='group',
|
||||
lookup_expr='in',
|
||||
label='Contact group (ID)',
|
||||
)
|
||||
group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=ContactGroup.objects.all(),
|
||||
field_name='group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Contact group (slug)',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = Contact
|
||||
fields = ['id', 'name', 'title', 'phone', 'email', 'address']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(title__icontains=value) |
|
||||
Q(phone__icontains=value) |
|
||||
Q(email__icontains=value) |
|
||||
Q(address__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class ContactAssignmentFilterSet(ChangeLoggedModelFilterSet):
|
||||
content_type = ContentTypeFilter()
|
||||
contact_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Contact.objects.all(),
|
||||
label='Contact (ID)',
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ContactRole.objects.all(),
|
||||
label='Contact role (ID)',
|
||||
)
|
||||
role = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='role__slug',
|
||||
queryset=ContactRole.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Contact role (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ContactAssignment
|
||||
fields = ['id', 'content_type_id', 'priority']
|
||||
|
@ -1,15 +1,22 @@
|
||||
from django import forms
|
||||
|
||||
from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from tenancy.models import *
|
||||
from utilities.forms import BootstrapMixin, DynamicModelChoiceField
|
||||
|
||||
__all__ = (
|
||||
'ContactBulkEditForm',
|
||||
'ContactGroupBulkEditForm',
|
||||
'ContactRoleBulkEditForm',
|
||||
'TenantBulkEditForm',
|
||||
'TenantGroupBulkEditForm',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Tenants
|
||||
#
|
||||
|
||||
class TenantGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=TenantGroup.objects.all(),
|
||||
@ -42,3 +49,68 @@ class TenantBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulk
|
||||
nullable_fields = [
|
||||
'group',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Contacts
|
||||
#
|
||||
|
||||
class ContactGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=ContactGroup.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
parent = DynamicModelChoiceField(
|
||||
queryset=ContactGroup.objects.all(),
|
||||
required=False
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['parent', 'description']
|
||||
|
||||
|
||||
class ContactRoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=ContactRole.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['description']
|
||||
|
||||
|
||||
class ContactBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Contact.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
)
|
||||
group = DynamicModelChoiceField(
|
||||
queryset=ContactGroup.objects.all(),
|
||||
required=False
|
||||
)
|
||||
title = forms.CharField(
|
||||
max_length=100,
|
||||
required=False
|
||||
)
|
||||
phone = forms.CharField(
|
||||
max_length=50,
|
||||
required=False
|
||||
)
|
||||
email = forms.EmailField(
|
||||
required=False
|
||||
)
|
||||
address = forms.CharField(
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['group', 'title', 'phone', 'email', 'address', 'comments']
|
||||
|
@ -1,13 +1,20 @@
|
||||
from extras.forms import CustomFieldModelCSVForm
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from tenancy.models import *
|
||||
from utilities.forms import CSVModelChoiceField, SlugField
|
||||
|
||||
__all__ = (
|
||||
'ContactCSVForm',
|
||||
'ContactGroupCSVForm',
|
||||
'ContactRoleCSVForm',
|
||||
'TenantCSVForm',
|
||||
'TenantGroupCSVForm',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Tenants
|
||||
#
|
||||
|
||||
class TenantGroupCSVForm(CustomFieldModelCSVForm):
|
||||
parent = CSVModelChoiceField(
|
||||
queryset=TenantGroup.objects.all(),
|
||||
@ -34,3 +41,43 @@ class TenantCSVForm(CustomFieldModelCSVForm):
|
||||
class Meta:
|
||||
model = Tenant
|
||||
fields = ('name', 'slug', 'group', 'description', 'comments')
|
||||
|
||||
|
||||
#
|
||||
# Contacts
|
||||
#
|
||||
|
||||
class ContactGroupCSVForm(CustomFieldModelCSVForm):
|
||||
parent = CSVModelChoiceField(
|
||||
queryset=ContactGroup.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Parent group'
|
||||
)
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
model = ContactGroup
|
||||
fields = ('name', 'slug', 'parent', 'description')
|
||||
|
||||
|
||||
class ContactRoleCSVForm(CustomFieldModelCSVForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
model = ContactRole
|
||||
fields = ('name', 'slug', 'description')
|
||||
|
||||
|
||||
class ContactCSVForm(CustomFieldModelCSVForm):
|
||||
slug = SlugField()
|
||||
group = CSVModelChoiceField(
|
||||
queryset=ContactGroup.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Assigned group'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Contact
|
||||
fields = ('name', 'title', 'phone', 'email', 'address', 'group', 'comments')
|
||||
|
@ -2,9 +2,21 @@ from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from extras.forms import CustomFieldModelFilterForm
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from tenancy.models import *
|
||||
from utilities.forms import BootstrapMixin, DynamicModelMultipleChoiceField, TagFilterField
|
||||
|
||||
__all__ = (
|
||||
'ContactFilterForm',
|
||||
'ContactGroupFilterForm',
|
||||
'ContactRoleFilterForm',
|
||||
'TenantFilterForm',
|
||||
'TenantGroupFilterForm',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Tenants
|
||||
#
|
||||
|
||||
class TenantGroupFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
model = TenantGroup
|
||||
@ -40,3 +52,55 @@ class TenantFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
fetch_trigger='open'
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
#
|
||||
# Contacts
|
||||
#
|
||||
|
||||
class ContactGroupFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
model = ContactGroup
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
parent_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ContactGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Parent group'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
|
||||
|
||||
class ContactRoleFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
model = ContactRole
|
||||
field_groups = [
|
||||
['q'],
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
|
||||
|
||||
class ContactFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
model = Contact
|
||||
field_groups = (
|
||||
('q', 'tag'),
|
||||
('group_id',),
|
||||
)
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ContactGroup.objects.all(),
|
||||
required=False,
|
||||
null_option='None',
|
||||
label=_('Group'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
@ -1,16 +1,27 @@
|
||||
from django import forms
|
||||
|
||||
from extras.forms import CustomFieldModelForm
|
||||
from extras.models import Tag
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from tenancy.models import *
|
||||
from utilities.forms import (
|
||||
BootstrapMixin, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField,
|
||||
BootstrapMixin, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField, SmallTextarea,
|
||||
StaticSelect,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
'ContactAssignmentForm',
|
||||
'ContactForm',
|
||||
'ContactGroupForm',
|
||||
'ContactRoleForm',
|
||||
'TenantForm',
|
||||
'TenantGroupForm',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Tenants
|
||||
#
|
||||
|
||||
class TenantGroupForm(BootstrapMixin, CustomFieldModelForm):
|
||||
parent = DynamicModelChoiceField(
|
||||
queryset=TenantGroup.objects.all(),
|
||||
@ -45,3 +56,79 @@ class TenantForm(BootstrapMixin, CustomFieldModelForm):
|
||||
fieldsets = (
|
||||
('Tenant', ('name', 'slug', 'group', 'description', 'tags')),
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Contacts
|
||||
#
|
||||
|
||||
class ContactGroupForm(BootstrapMixin, CustomFieldModelForm):
|
||||
parent = DynamicModelChoiceField(
|
||||
queryset=ContactGroup.objects.all(),
|
||||
required=False
|
||||
)
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
model = ContactGroup
|
||||
fields = ['parent', 'name', 'slug', 'description']
|
||||
|
||||
|
||||
class ContactRoleForm(BootstrapMixin, CustomFieldModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
model = ContactRole
|
||||
fields = ['name', 'slug', 'description']
|
||||
|
||||
|
||||
class ContactForm(BootstrapMixin, CustomFieldModelForm):
|
||||
group = DynamicModelChoiceField(
|
||||
queryset=ContactGroup.objects.all(),
|
||||
required=False
|
||||
)
|
||||
comments = CommentField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Contact
|
||||
fields = (
|
||||
'group', 'name', 'title', 'phone', 'email', 'address', 'comments', 'tags',
|
||||
)
|
||||
fieldsets = (
|
||||
('Contact', ('group', 'name', 'title', 'phone', 'email', 'address', 'tags')),
|
||||
)
|
||||
widgets = {
|
||||
'address': SmallTextarea(attrs={'rows': 3}),
|
||||
}
|
||||
|
||||
|
||||
class ContactAssignmentForm(BootstrapMixin, forms.ModelForm):
|
||||
group = DynamicModelChoiceField(
|
||||
queryset=ContactGroup.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'contacts': '$contact'
|
||||
}
|
||||
)
|
||||
contact = DynamicModelChoiceField(
|
||||
queryset=Contact.objects.all(),
|
||||
query_params={
|
||||
'group_id': '$group'
|
||||
}
|
||||
)
|
||||
role = DynamicModelChoiceField(
|
||||
queryset=ContactRole.objects.all()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ContactAssignment
|
||||
fields = (
|
||||
'group', 'contact', 'role', 'priority',
|
||||
)
|
||||
widgets = {
|
||||
'priority': StaticSelect(),
|
||||
}
|
||||
|
@ -10,3 +10,15 @@ class TenancyQuery(graphene.ObjectType):
|
||||
|
||||
tenant_group = ObjectField(TenantGroupType)
|
||||
tenant_group_list = ObjectListField(TenantGroupType)
|
||||
|
||||
contact = ObjectField(ContactType)
|
||||
contact_list = ObjectListField(ContactType)
|
||||
|
||||
contact_role = ObjectField(ContactRoleType)
|
||||
contact_role_list = ObjectListField(ContactRoleType)
|
||||
|
||||
contact_group = ObjectField(ContactGroupType)
|
||||
contact_group_list = ObjectListField(ContactGroupType)
|
||||
|
||||
contact_assignment = ObjectField(ContactAssignmentType)
|
||||
contact_assignment_list = ObjectListField(ContactAssignmentType)
|
||||
|
@ -1,12 +1,29 @@
|
||||
import graphene
|
||||
|
||||
from tenancy import filtersets, models
|
||||
from netbox.graphql.types import OrganizationalObjectType, PrimaryObjectType
|
||||
|
||||
__all__ = (
|
||||
'ContactAssignmentType',
|
||||
'ContactGroupType',
|
||||
'ContactRoleType',
|
||||
'ContactType',
|
||||
'TenantType',
|
||||
'TenantGroupType',
|
||||
)
|
||||
|
||||
|
||||
class ContactAssignmentsMixin:
|
||||
assignments = graphene.List('tenancy.graphql.types.ContactAssignmentType')
|
||||
|
||||
def resolve_assignments(self, info):
|
||||
return self.assignments.restrict(info.context.user, 'view')
|
||||
|
||||
|
||||
#
|
||||
# Tenants
|
||||
#
|
||||
|
||||
class TenantType(PrimaryObjectType):
|
||||
|
||||
class Meta:
|
||||
@ -21,3 +38,39 @@ class TenantGroupType(OrganizationalObjectType):
|
||||
model = models.TenantGroup
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.TenantGroupFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Contacts
|
||||
#
|
||||
|
||||
class ContactType(ContactAssignmentsMixin, PrimaryObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.Contact
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.ContactFilterSet
|
||||
|
||||
|
||||
class ContactRoleType(ContactAssignmentsMixin, OrganizationalObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.ContactRole
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.ContactRoleFilterSet
|
||||
|
||||
|
||||
class ContactGroupType(OrganizationalObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.ContactGroup
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.ContactGroupFilterSet
|
||||
|
||||
|
||||
class ContactAssignmentType(OrganizationalObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.ContactAssignment
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.ContactAssignmentFilterSet
|
||||
|
91
netbox/tenancy/migrations/0003_contacts.py
Normal file
91
netbox/tenancy/migrations/0003_contacts.py
Normal file
@ -0,0 +1,91 @@
|
||||
import django.core.serializers.json
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import mptt.fields
|
||||
import taggit.managers
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0062_clear_secrets_changelog'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('tenancy', '0002_tenant_ordering'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ContactRole',
|
||||
fields=[
|
||||
('created', models.DateField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=100, unique=True)),
|
||||
('slug', models.SlugField(max_length=100, unique=True)),
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ContactGroup',
|
||||
fields=[
|
||||
('created', models.DateField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('slug', models.SlugField(max_length=100)),
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
('lft', models.PositiveIntegerField(editable=False)),
|
||||
('rght', models.PositiveIntegerField(editable=False)),
|
||||
('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
|
||||
('level', models.PositiveIntegerField(editable=False)),
|
||||
('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='tenancy.contactgroup')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
'unique_together': {('parent', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Contact',
|
||||
fields=[
|
||||
('created', models.DateField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('title', models.CharField(blank=True, max_length=100)),
|
||||
('phone', models.CharField(blank=True, max_length=50)),
|
||||
('email', models.EmailField(blank=True, max_length=254)),
|
||||
('address', models.CharField(blank=True, max_length=200)),
|
||||
('comments', models.TextField(blank=True)),
|
||||
('group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='contacts', to='tenancy.contactgroup')),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
'unique_together': {('group', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ContactAssignment',
|
||||
fields=[
|
||||
('created', models.DateField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('object_id', models.PositiveIntegerField()),
|
||||
('priority', models.CharField(blank=True, max_length=50)),
|
||||
('contact', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='assignments', to='tenancy.contact')),
|
||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
|
||||
('role', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='assignments', to='tenancy.contactrole')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('priority', 'contact'),
|
||||
'unique_together': {('content_type', 'object_id', 'contact', 'role', 'priority')},
|
||||
},
|
||||
),
|
||||
]
|
@ -1,19 +1,29 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
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
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
|
||||
from extras.utils import extras_features
|
||||
from netbox.models import NestedGroupModel, PrimaryModel
|
||||
from netbox.models import ChangeLoggedModel, NestedGroupModel, OrganizationalModel, PrimaryModel
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from .choices import *
|
||||
|
||||
|
||||
__all__ = (
|
||||
'ContactAssignment',
|
||||
'Contact',
|
||||
'ContactGroup',
|
||||
'ContactRole',
|
||||
'Tenant',
|
||||
'TenantGroup',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Tenants
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class TenantGroup(NestedGroupModel):
|
||||
"""
|
||||
@ -76,6 +86,11 @@ class Tenant(PrimaryModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
# Generic relations
|
||||
contacts = GenericRelation(
|
||||
to='tenancy.ContactAssignment'
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
clone_fields = [
|
||||
@ -90,3 +105,163 @@ class Tenant(PrimaryModel):
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('tenancy:tenant', args=[self.pk])
|
||||
|
||||
|
||||
#
|
||||
# Contacts
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class ContactGroup(NestedGroupModel):
|
||||
"""
|
||||
An arbitrary collection of Contacts.
|
||||
"""
|
||||
name = models.CharField(
|
||||
max_length=100
|
||||
)
|
||||
slug = models.SlugField(
|
||||
max_length=100
|
||||
)
|
||||
parent = TreeForeignKey(
|
||||
to='self',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='children',
|
||||
blank=True,
|
||||
null=True,
|
||||
db_index=True
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
unique_together = (
|
||||
('parent', 'name')
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('tenancy:contactgroup', args=[self.pk])
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class ContactRole(OrganizationalModel):
|
||||
"""
|
||||
Functional role for a Contact assigned to an object.
|
||||
"""
|
||||
name = models.CharField(
|
||||
max_length=100,
|
||||
unique=True
|
||||
)
|
||||
slug = models.SlugField(
|
||||
max_length=100,
|
||||
unique=True
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=200,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('tenancy:contactrole', args=[self.pk])
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class Contact(PrimaryModel):
|
||||
"""
|
||||
Contact information for a particular object(s) in NetBox.
|
||||
"""
|
||||
group = models.ForeignKey(
|
||||
to='tenancy.ContactGroup',
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='contacts',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=100
|
||||
)
|
||||
title = models.CharField(
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
phone = models.CharField(
|
||||
max_length=50,
|
||||
blank=True
|
||||
)
|
||||
email = models.EmailField(
|
||||
blank=True
|
||||
)
|
||||
address = models.CharField(
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
comments = models.TextField(
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
clone_fields = [
|
||||
'group',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
unique_together = (
|
||||
('group', 'name')
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('tenancy:contact', args=[self.pk])
|
||||
|
||||
|
||||
@extras_features('webhooks')
|
||||
class ContactAssignment(ChangeLoggedModel):
|
||||
content_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
object_id = models.PositiveIntegerField()
|
||||
object = GenericForeignKey(
|
||||
ct_field='content_type',
|
||||
fk_field='object_id'
|
||||
)
|
||||
contact = models.ForeignKey(
|
||||
to='tenancy.Contact',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='assignments'
|
||||
)
|
||||
role = models.ForeignKey(
|
||||
to='tenancy.ContactRole',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='assignments'
|
||||
)
|
||||
priority = models.CharField(
|
||||
max_length=50,
|
||||
choices=ContactPriorityChoices,
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ('priority', 'contact')
|
||||
unique_together = ('content_type', 'object_id', 'contact', 'role', 'priority')
|
||||
|
||||
def __str__(self):
|
||||
if self.priority:
|
||||
return f"{self.contact} ({self.get_priority_display()})"
|
||||
return str(self.contact)
|
||||
|
@ -1,11 +1,15 @@
|
||||
import django_tables2 as tables
|
||||
|
||||
from utilities.tables import (
|
||||
BaseTable, ButtonsColumn, LinkedCountColumn, MarkdownColumn, MPTTColumn, TagColumn, ToggleColumn,
|
||||
BaseTable, ButtonsColumn, ContentTypeColumn, LinkedCountColumn, MarkdownColumn, MPTTColumn, TagColumn, ToggleColumn,
|
||||
)
|
||||
from .models import Tenant, TenantGroup
|
||||
from .models import *
|
||||
|
||||
__all__ = (
|
||||
'ContactAssignmentTable',
|
||||
'ContactGroupTable',
|
||||
'ContactRoleTable',
|
||||
'ContactTable',
|
||||
'TenantColumn',
|
||||
'TenantGroupTable',
|
||||
'TenantTable',
|
||||
@ -38,7 +42,7 @@ class TenantColumn(tables.TemplateColumn):
|
||||
|
||||
|
||||
#
|
||||
# Tenant groups
|
||||
# Tenants
|
||||
#
|
||||
|
||||
class TenantGroupTable(BaseTable):
|
||||
@ -59,10 +63,6 @@ class TenantGroupTable(BaseTable):
|
||||
default_columns = ('pk', 'name', 'tenant_count', 'description', 'actions')
|
||||
|
||||
|
||||
#
|
||||
# Tenants
|
||||
#
|
||||
|
||||
class TenantTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(
|
||||
@ -80,3 +80,82 @@ class TenantTable(BaseTable):
|
||||
model = Tenant
|
||||
fields = ('pk', 'name', 'slug', 'group', 'description', 'comments', 'tags')
|
||||
default_columns = ('pk', 'name', 'group', 'description')
|
||||
|
||||
|
||||
#
|
||||
# Contacts
|
||||
#
|
||||
|
||||
class ContactGroupTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = MPTTColumn(
|
||||
linkify=True
|
||||
)
|
||||
contact_count = LinkedCountColumn(
|
||||
viewname='tenancy:contact_list',
|
||||
url_params={'role_id': 'pk'},
|
||||
verbose_name='Contacts'
|
||||
)
|
||||
actions = ButtonsColumn(ContactGroup)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ContactGroup
|
||||
fields = ('pk', 'name', 'contact_count', 'description', 'slug', 'actions')
|
||||
default_columns = ('pk', 'name', 'contact_count', 'description', 'actions')
|
||||
|
||||
|
||||
class ContactRoleTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
actions = ButtonsColumn(ContactRole)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ContactRole
|
||||
fields = ('pk', 'name', 'description', 'slug', 'actions')
|
||||
default_columns = ('pk', 'name', 'description', 'actions')
|
||||
|
||||
|
||||
class ContactTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
group = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
comments = MarkdownColumn()
|
||||
assignment_count = tables.Column(
|
||||
verbose_name='Assignments'
|
||||
)
|
||||
tags = TagColumn(
|
||||
url_name='tenancy:tenant_list'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Contact
|
||||
fields = ('pk', 'name', 'group', 'title', 'phone', 'email', 'address', 'comments', 'assignment_count', 'tags')
|
||||
default_columns = ('pk', 'name', 'group', 'assignment_count', 'title', 'phone', 'email')
|
||||
|
||||
|
||||
class ContactAssignmentTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
content_type = ContentTypeColumn(
|
||||
verbose_name='Object Type'
|
||||
)
|
||||
object = tables.Column(
|
||||
linkify=True,
|
||||
orderable=False
|
||||
)
|
||||
contact = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
role = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ContactAssignment
|
||||
fields = ('pk', 'content_type', 'object', 'contact', 'role', 'priority')
|
||||
default_columns = ('pk', 'object', 'contact', 'role', 'priority')
|
||||
|
@ -1,6 +1,6 @@
|
||||
from django.urls import reverse
|
||||
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from tenancy.models import *
|
||||
from utilities.testing import APITestCase, APIViewTestCases
|
||||
|
||||
|
||||
@ -92,3 +92,112 @@ class TenantTest(APIViewTestCases.APIViewTestCase):
|
||||
'group': tenant_groups[1].pk,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class ContactGroupTest(APIViewTestCases.APIViewTestCase):
|
||||
model = ContactGroup
|
||||
brief_fields = ['_depth', 'contact_count', 'display', 'id', 'name', 'slug', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
parent_contact_groups = (
|
||||
ContactGroup.objects.create(name='Parent Contact Group 1', slug='parent-contact-group-1'),
|
||||
ContactGroup.objects.create(name='Parent Contact Group 2', slug='parent-contact-group-2'),
|
||||
)
|
||||
|
||||
ContactGroup.objects.create(name='Contact Group 1', slug='contact-group-1', parent=parent_contact_groups[0])
|
||||
ContactGroup.objects.create(name='Contact Group 2', slug='contact-group-2', parent=parent_contact_groups[0])
|
||||
ContactGroup.objects.create(name='Contact Group 3', slug='contact-group-3', parent=parent_contact_groups[0])
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
'name': 'Contact Group 4',
|
||||
'slug': 'contact-group-4',
|
||||
'parent': parent_contact_groups[1].pk,
|
||||
},
|
||||
{
|
||||
'name': 'Contact Group 5',
|
||||
'slug': 'contact-group-5',
|
||||
'parent': parent_contact_groups[1].pk,
|
||||
},
|
||||
{
|
||||
'name': 'Contact Group 6',
|
||||
'slug': 'contact-group-6',
|
||||
'parent': parent_contact_groups[1].pk,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class ContactRoleTest(APIViewTestCases.APIViewTestCase):
|
||||
model = ContactRole
|
||||
brief_fields = ['display', 'id', 'name', 'slug', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'name': 'Contact Role 4',
|
||||
'slug': 'contact-role-4',
|
||||
},
|
||||
{
|
||||
'name': 'Contact Role 5',
|
||||
'slug': 'contact-role-5',
|
||||
},
|
||||
{
|
||||
'name': 'Contact Role 6',
|
||||
'slug': 'contact-role-6',
|
||||
},
|
||||
]
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
contact_roles = (
|
||||
ContactRole(name='Contact Role 1', slug='contact-role-1'),
|
||||
ContactRole(name='Contact Role 2', slug='contact-role-2'),
|
||||
ContactRole(name='Contact Role 3', slug='contact-role-3'),
|
||||
)
|
||||
ContactRole.objects.bulk_create(contact_roles)
|
||||
|
||||
|
||||
class ContactTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Contact
|
||||
brief_fields = ['display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'group': None,
|
||||
'comments': 'New comments',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
contact_groups = (
|
||||
ContactGroup.objects.create(name='Contact Group 1', slug='contact-group-1'),
|
||||
ContactGroup.objects.create(name='Contact Group 2', slug='contact-group-2'),
|
||||
)
|
||||
|
||||
contacts = (
|
||||
Contact(name='Contact 1', group=contact_groups[0]),
|
||||
Contact(name='Contact 2', group=contact_groups[0]),
|
||||
Contact(name='Contact 3', group=contact_groups[0]),
|
||||
)
|
||||
Contact.objects.bulk_create(contacts)
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
'name': 'Contact 4',
|
||||
'group': contact_groups[1].pk,
|
||||
},
|
||||
{
|
||||
'name': 'Contact 5',
|
||||
'group': contact_groups[1].pk,
|
||||
},
|
||||
{
|
||||
'name': 'Contact 6',
|
||||
'group': contact_groups[1].pk,
|
||||
},
|
||||
]
|
||||
|
@ -1,7 +1,7 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from tenancy.filtersets import *
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from tenancy.models import *
|
||||
from utilities.testing import ChangeLoggedFilterSetTests
|
||||
|
||||
|
||||
@ -84,3 +84,103 @@ class TenantTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'group': [group[0].slug, group[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class ContactGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = ContactGroup.objects.all()
|
||||
filterset = ContactGroupFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
parent_contact_groups = (
|
||||
ContactGroup(name='Parent Contact Group 1', slug='parent-contact-group-1'),
|
||||
ContactGroup(name='Parent Contact Group 2', slug='parent-contact-group-2'),
|
||||
ContactGroup(name='Parent Contact Group 3', slug='parent-contact-group-3'),
|
||||
)
|
||||
for contactgroup in parent_contact_groups:
|
||||
contactgroup.save()
|
||||
|
||||
contact_groups = (
|
||||
ContactGroup(name='Contact Group 1', slug='contact-group-1', parent=parent_contact_groups[0], description='A'),
|
||||
ContactGroup(name='Contact Group 2', slug='contact-group-2', parent=parent_contact_groups[1], description='B'),
|
||||
ContactGroup(name='Contact Group 3', slug='contact-group-3', parent=parent_contact_groups[2], description='C'),
|
||||
)
|
||||
for contactgroup in contact_groups:
|
||||
contactgroup.save()
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Contact Group 1', 'Contact Group 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_slug(self):
|
||||
params = {'slug': ['contact-group-1', 'contact-group-2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['A', 'B']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_parent(self):
|
||||
parent_groups = ContactGroup.objects.filter(parent__isnull=True)[:2]
|
||||
params = {'parent_id': [parent_groups[0].pk, parent_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'parent': [parent_groups[0].slug, parent_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class ContactRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = ContactRole.objects.all()
|
||||
filterset = ContactRoleFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
contact_roles = (
|
||||
ContactRole(name='Contact Role 1', slug='contact-role-1'),
|
||||
ContactRole(name='Contact Role 2', slug='contact-role-2'),
|
||||
ContactRole(name='Contact Role 3', slug='contact-role-3'),
|
||||
)
|
||||
ContactRole.objects.bulk_create(contact_roles)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Contact Role 1', 'Contact Role 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_slug(self):
|
||||
params = {'slug': ['contact-role-1', 'contact-role-2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class ContactTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = Contact.objects.all()
|
||||
filterset = ContactFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
contact_groups = (
|
||||
ContactGroup(name='Contact Group 1', slug='contact-group-1'),
|
||||
ContactGroup(name='Contact Group 2', slug='contact-group-2'),
|
||||
ContactGroup(name='Contact Group 3', slug='contact-group-3'),
|
||||
)
|
||||
for contactgroup in contact_groups:
|
||||
contactgroup.save()
|
||||
|
||||
contacts = (
|
||||
Contact(name='Contact 1', group=contact_groups[0]),
|
||||
Contact(name='Contact 2', group=contact_groups[1]),
|
||||
Contact(name='Contact 3', group=contact_groups[2]),
|
||||
)
|
||||
Contact.objects.bulk_create(contacts)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Contact 1', 'Contact 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_group(self):
|
||||
group = ContactGroup.objects.all()[:2]
|
||||
params = {'group_id': [group[0].pk, group[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'group': [group[0].slug, group[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
@ -1,4 +1,4 @@
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from tenancy.models import *
|
||||
from utilities.testing import ViewTestCases, create_tags
|
||||
|
||||
|
||||
@ -74,3 +74,105 @@ class TenantTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
cls.bulk_edit_data = {
|
||||
'group': tenant_groups[1].pk,
|
||||
}
|
||||
|
||||
|
||||
class ContactGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = ContactGroup
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
contact_groups = (
|
||||
ContactGroup(name='Contact Group 1', slug='contact-group-1'),
|
||||
ContactGroup(name='Contact Group 2', slug='contact-group-2'),
|
||||
ContactGroup(name='Contact Group 3', slug='contact-group-3'),
|
||||
)
|
||||
for tenanantgroup in contact_groups:
|
||||
tenanantgroup.save()
|
||||
|
||||
cls.form_data = {
|
||||
'name': 'Contact Group X',
|
||||
'slug': 'contact-group-x',
|
||||
'description': 'A new contact group',
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"name,slug,description",
|
||||
"Contact Group 4,contact-group-4,Fourth contact group",
|
||||
"Contact Group 5,contact-group-5,Fifth contact group",
|
||||
"Contact Group 6,contact-group-6,Sixth contact group",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
|
||||
class ContactRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = ContactRole
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
ContactRole.objects.bulk_create([
|
||||
ContactRole(name='Contact Role 1', slug='contact-role-1'),
|
||||
ContactRole(name='Contact Role 2', slug='contact-role-2'),
|
||||
ContactRole(name='Contact Role 3', slug='contact-role-3'),
|
||||
])
|
||||
|
||||
cls.form_data = {
|
||||
'name': 'Devie Role X',
|
||||
'slug': 'contact-role-x',
|
||||
'description': 'New contact role',
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"name,slug",
|
||||
"Contact Role 4,contact-role-4",
|
||||
"Contact Role 5,contact-role-5",
|
||||
"Contact Role 6,contact-role-6",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
|
||||
class ContactTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = Contact
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
contact_groups = (
|
||||
ContactGroup(name='Contact Group 1', slug='contact-group-1'),
|
||||
ContactGroup(name='Contact Group 2', slug='contact-group-2'),
|
||||
)
|
||||
for contactgroup in contact_groups:
|
||||
contactgroup.save()
|
||||
|
||||
Contact.objects.bulk_create([
|
||||
Contact(name='Contact 1', group=contact_groups[0]),
|
||||
Contact(name='Contact 2', group=contact_groups[0]),
|
||||
Contact(name='Contact 3', group=contact_groups[0]),
|
||||
])
|
||||
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'name': 'Contact X',
|
||||
'group': contact_groups[1].pk,
|
||||
'comments': 'Some comments',
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"name,slug",
|
||||
"Contact 4,contact-4",
|
||||
"Contact 5,contact-5",
|
||||
"Contact 6,contact-6",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'group': contact_groups[1].pk,
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ from django.urls import path
|
||||
from extras.views import ObjectChangeLogView, ObjectJournalView
|
||||
from utilities.views import SlugRedirectView
|
||||
from . import views
|
||||
from .models import Tenant, TenantGroup
|
||||
from .models import *
|
||||
|
||||
app_name = 'tenancy'
|
||||
urlpatterns = [
|
||||
@ -32,4 +32,44 @@ urlpatterns = [
|
||||
path('tenants/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='tenant_changelog', kwargs={'model': Tenant}),
|
||||
path('tenants/<int:pk>/journal/', ObjectJournalView.as_view(), name='tenant_journal', kwargs={'model': Tenant}),
|
||||
|
||||
# Contact groups
|
||||
path('contact-groups/', views.ContactGroupListView.as_view(), name='contactgroup_list'),
|
||||
path('contact-groups/add/', views.ContactGroupEditView.as_view(), name='contactgroup_add'),
|
||||
path('contact-groups/import/', views.ContactGroupBulkImportView.as_view(), name='contactgroup_import'),
|
||||
path('contact-groups/edit/', views.ContactGroupBulkEditView.as_view(), name='contactgroup_bulk_edit'),
|
||||
path('contact-groups/delete/', views.ContactGroupBulkDeleteView.as_view(), name='contactgroup_bulk_delete'),
|
||||
path('contact-groups/<int:pk>/', views.ContactGroupView.as_view(), name='contactgroup'),
|
||||
path('contact-groups/<int:pk>/edit/', views.ContactGroupEditView.as_view(), name='contactgroup_edit'),
|
||||
path('contact-groups/<int:pk>/delete/', views.ContactGroupDeleteView.as_view(), name='contactgroup_delete'),
|
||||
path('contact-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='contactgroup_changelog', kwargs={'model': ContactGroup}),
|
||||
|
||||
# Contact roles
|
||||
path('contact-roles/', views.ContactRoleListView.as_view(), name='contactrole_list'),
|
||||
path('contact-roles/add/', views.ContactRoleEditView.as_view(), name='contactrole_add'),
|
||||
path('contact-roles/import/', views.ContactRoleBulkImportView.as_view(), name='contactrole_import'),
|
||||
path('contact-roles/edit/', views.ContactRoleBulkEditView.as_view(), name='contactrole_bulk_edit'),
|
||||
path('contact-roles/delete/', views.ContactRoleBulkDeleteView.as_view(), name='contactrole_bulk_delete'),
|
||||
path('contact-roles/<int:pk>/', views.ContactRoleView.as_view(), name='contactrole'),
|
||||
path('contact-roles/<int:pk>/edit/', views.ContactRoleEditView.as_view(), name='contactrole_edit'),
|
||||
path('contact-roles/<int:pk>/delete/', views.ContactRoleDeleteView.as_view(), name='contactrole_delete'),
|
||||
path('contact-roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='contactrole_changelog', kwargs={'model': ContactRole}),
|
||||
|
||||
# Contacts
|
||||
path('contacts/', views.ContactListView.as_view(), name='contact_list'),
|
||||
path('contacts/add/', views.ContactEditView.as_view(), name='contact_add'),
|
||||
path('contacts/import/', views.ContactBulkImportView.as_view(), name='contact_import'),
|
||||
path('contacts/edit/', views.ContactBulkEditView.as_view(), name='contact_bulk_edit'),
|
||||
path('contacts/delete/', views.ContactBulkDeleteView.as_view(), name='contact_bulk_delete'),
|
||||
path('contacts/<int:pk>/', views.ContactView.as_view(), name='contact'),
|
||||
path('contacts/<slug:slug>/', SlugRedirectView.as_view(), kwargs={'model': Contact}),
|
||||
path('contacts/<int:pk>/edit/', views.ContactEditView.as_view(), name='contact_edit'),
|
||||
path('contacts/<int:pk>/delete/', views.ContactDeleteView.as_view(), name='contact_delete'),
|
||||
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'),
|
||||
|
||||
]
|
||||
|
@ -1,11 +1,16 @@
|
||||
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 Tenant, TenantGroup
|
||||
from .models import *
|
||||
|
||||
|
||||
#
|
||||
@ -140,3 +145,217 @@ class TenantBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Tenant.objects.prefetch_related('group')
|
||||
filterset = filtersets.TenantFilterSet
|
||||
table = tables.TenantTable
|
||||
|
||||
|
||||
#
|
||||
# Contact groups
|
||||
#
|
||||
|
||||
class ContactGroupListView(generic.ObjectListView):
|
||||
queryset = ContactGroup.objects.add_related_count(
|
||||
ContactGroup.objects.all(),
|
||||
Contact,
|
||||
'group',
|
||||
'contact_count',
|
||||
cumulative=True
|
||||
)
|
||||
filterset = filtersets.ContactGroupFilterSet
|
||||
filterset_form = forms.ContactGroupFilterForm
|
||||
table = tables.ContactGroupTable
|
||||
|
||||
|
||||
class ContactGroupView(generic.ObjectView):
|
||||
queryset = ContactGroup.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
contacts = Contact.objects.restrict(request.user, 'view').filter(
|
||||
group=instance
|
||||
)
|
||||
contacts_table = tables.ContactTable(contacts, exclude=('group',))
|
||||
paginate_table(contacts_table, request)
|
||||
|
||||
return {
|
||||
'contacts_table': contacts_table,
|
||||
}
|
||||
|
||||
|
||||
class ContactGroupEditView(generic.ObjectEditView):
|
||||
queryset = ContactGroup.objects.all()
|
||||
model_form = forms.ContactGroupForm
|
||||
|
||||
|
||||
class ContactGroupDeleteView(generic.ObjectDeleteView):
|
||||
queryset = ContactGroup.objects.all()
|
||||
|
||||
|
||||
class ContactGroupBulkImportView(generic.BulkImportView):
|
||||
queryset = ContactGroup.objects.all()
|
||||
model_form = forms.ContactGroupCSVForm
|
||||
table = tables.ContactGroupTable
|
||||
|
||||
|
||||
class ContactGroupBulkEditView(generic.BulkEditView):
|
||||
queryset = ContactGroup.objects.add_related_count(
|
||||
ContactGroup.objects.all(),
|
||||
Contact,
|
||||
'group',
|
||||
'contact_count',
|
||||
cumulative=True
|
||||
)
|
||||
filterset = filtersets.ContactGroupFilterSet
|
||||
table = tables.ContactGroupTable
|
||||
form = forms.ContactGroupBulkEditForm
|
||||
|
||||
|
||||
class ContactGroupBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = ContactGroup.objects.add_related_count(
|
||||
ContactGroup.objects.all(),
|
||||
Contact,
|
||||
'group',
|
||||
'contact_count',
|
||||
cumulative=True
|
||||
)
|
||||
table = tables.ContactGroupTable
|
||||
|
||||
|
||||
#
|
||||
# Contact roles
|
||||
#
|
||||
|
||||
class ContactRoleListView(generic.ObjectListView):
|
||||
queryset = ContactRole.objects.all()
|
||||
filterset = filtersets.ContactRoleFilterSet
|
||||
filterset_form = forms.ContactRoleFilterForm
|
||||
table = tables.ContactRoleTable
|
||||
|
||||
|
||||
class ContactRoleView(generic.ObjectView):
|
||||
queryset = ContactRole.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
contact_assignments = ContactAssignment.objects.restrict(request.user, 'view').filter(
|
||||
role=instance
|
||||
)
|
||||
contacts_table = tables.ContactAssignmentTable(contact_assignments)
|
||||
contacts_table.columns.hide('role')
|
||||
paginate_table(contacts_table, request)
|
||||
|
||||
return {
|
||||
'contacts_table': contacts_table,
|
||||
'assignment_count': ContactAssignment.objects.filter(role=instance).count(),
|
||||
}
|
||||
|
||||
|
||||
class ContactRoleEditView(generic.ObjectEditView):
|
||||
queryset = ContactRole.objects.all()
|
||||
model_form = forms.ContactRoleForm
|
||||
|
||||
|
||||
class ContactRoleDeleteView(generic.ObjectDeleteView):
|
||||
queryset = ContactRole.objects.all()
|
||||
|
||||
|
||||
class ContactRoleBulkImportView(generic.BulkImportView):
|
||||
queryset = ContactRole.objects.all()
|
||||
model_form = forms.ContactRoleCSVForm
|
||||
table = tables.ContactRoleTable
|
||||
|
||||
|
||||
class ContactRoleBulkEditView(generic.BulkEditView):
|
||||
queryset = ContactRole.objects.all()
|
||||
filterset = filtersets.ContactRoleFilterSet
|
||||
table = tables.ContactRoleTable
|
||||
form = forms.ContactRoleBulkEditForm
|
||||
|
||||
|
||||
class ContactRoleBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = ContactRole.objects.all()
|
||||
table = tables.ContactRoleTable
|
||||
|
||||
|
||||
#
|
||||
# Contacts
|
||||
#
|
||||
|
||||
class ContactListView(generic.ObjectListView):
|
||||
queryset = Contact.objects.annotate(
|
||||
assignment_count=count_related(ContactAssignment, 'contact')
|
||||
)
|
||||
filterset = filtersets.ContactFilterSet
|
||||
filterset_form = forms.ContactFilterForm
|
||||
table = tables.ContactTable
|
||||
|
||||
|
||||
class ContactView(generic.ObjectView):
|
||||
queryset = Contact.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
contact_assignments = ContactAssignment.objects.restrict(request.user, 'view').filter(
|
||||
contact=instance
|
||||
)
|
||||
contacts_table = tables.ContactAssignmentTable(contact_assignments)
|
||||
contacts_table.columns.hide('contact')
|
||||
paginate_table(contacts_table, request)
|
||||
|
||||
return {
|
||||
'contacts_table': contacts_table,
|
||||
'assignment_count': ContactAssignment.objects.filter(contact=instance).count(),
|
||||
}
|
||||
|
||||
|
||||
class ContactEditView(generic.ObjectEditView):
|
||||
queryset = Contact.objects.all()
|
||||
model_form = forms.ContactForm
|
||||
|
||||
|
||||
class ContactDeleteView(generic.ObjectDeleteView):
|
||||
queryset = Contact.objects.all()
|
||||
|
||||
|
||||
class ContactBulkImportView(generic.BulkImportView):
|
||||
queryset = Contact.objects.all()
|
||||
model_form = forms.ContactCSVForm
|
||||
table = tables.ContactTable
|
||||
|
||||
|
||||
class ContactBulkEditView(generic.BulkEditView):
|
||||
queryset = Contact.objects.prefetch_related('group')
|
||||
filterset = filtersets.ContactFilterSet
|
||||
table = tables.ContactTable
|
||||
form = forms.ContactBulkEditForm
|
||||
|
||||
|
||||
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)
|
||||
|
@ -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 = [
|
||||
|
Loading…
Reference in New Issue
Block a user