1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Initial work on contacts

This commit is contained in:
jeremystretch
2021-10-18 11:45:05 -04:00
parent 6015c47587
commit 2e78568d4d
24 changed files with 1594 additions and 29 deletions

View File

@ -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'),
),
),
),
)

View File

@ -0,0 +1,66 @@
{% 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>
</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">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View 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">&mdash;</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 %}

View File

@ -0,0 +1,46 @@
{% 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>
</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 %}

View File

@ -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']

View File

@ -1,7 +1,9 @@
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 ContentTypeField
from netbox.api.serializers import NestedGroupModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer
from tenancy.models import *
from .nested_serializers import *
@ -43,3 +45,57 @@ 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)
class Meta:
model = ContactAssignment
fields = [
'id', 'url', 'display', 'content_type', 'object_id', 'contact', 'role', 'created', 'last_updated',
]

View File

@ -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

View File

@ -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
View 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'),
)

View File

@ -4,16 +4,24 @@ 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 .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,89 @@ 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(OrganizationalModelFilterSet):
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', 'priority']

View File

@ -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,53 @@ 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
)
class Meta:
nullable_fields = ['group', 'title', 'phone', 'email', 'address', 'comments']

View File

@ -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')

View File

@ -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)

View File

@ -1,16 +1,23 @@
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,
)
__all__ = (
'ContactForm',
'ContactGroupForm',
'ContactRoleForm',
'TenantForm',
'TenantGroupForm',
)
#
# Tenants
#
class TenantGroupForm(BootstrapMixin, CustomFieldModelForm):
parent = DynamicModelChoiceField(
queryset=TenantGroup.objects.all(),
@ -45,3 +52,51 @@ 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}),
}

View File

@ -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)

View File

@ -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

View File

@ -0,0 +1,98 @@
# Generated by Django 3.2.8 on 2021-10-18 16:12
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='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)),
],
options={
'ordering': ['name'],
},
),
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, unique=True)),
('slug', models.SlugField(max_length=100, unique=True)),
('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'],
},
),
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'),
},
),
migrations.AddField(
model_name='contact',
name='group',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='contacts', to='tenancy.contactgroup'),
),
migrations.AddField(
model_name='contact',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
]

View File

@ -1,19 +1,29 @@
from django.core.exceptions import ValidationError
from django.contrib.contenttypes.fields import GenericForeignKey
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):
"""
@ -90,3 +100,153 @@ 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,
unique=True
)
slug = models.SlugField(
max_length=100,
unique=True
)
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']
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']
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')

View File

@ -3,9 +3,13 @@ import django_tables2 as tables
from utilities.tables import (
BaseTable, ButtonsColumn, 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,69 @@ 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()
tags = TagColumn(
url_name='tenancy:tenant_list'
)
class Meta(BaseTable.Meta):
model = Contact
fields = ('pk', 'name', 'group', 'title', 'phone', 'email', 'address', 'comments', 'tags')
default_columns = ('pk', 'name', 'group', 'title', 'phone', 'email')
class ContactAssignmentTable(BaseTable):
pk = ToggleColumn()
contact = tables.Column(
linkify=True
)
class Meta(BaseTable.Meta):
model = ContactAssignment
fields = ('pk', 'contact', 'role', 'priority')
default_columns = ('pk', 'contact', 'role', 'priority')

View File

@ -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,
},
]

View File

@ -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)

View File

@ -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,
}

View File

@ -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,39 @@ 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}),
]

View File

@ -3,9 +3,10 @@ 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 +141,171 @@ 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)
paginate_table(contacts_table, request)
return {
'contacts_table': contacts_table,
'contact_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.all()
filterset = filtersets.ContactFilterSet
filterset_form = forms.ContactFilterForm
table = tables.ContactTable
class ContactView(generic.ObjectView):
queryset = Contact.objects.all()
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