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'), 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 rest_framework import serializers
from netbox.api import WritableNestedSerializer from netbox.api import WritableNestedSerializer
from tenancy.models import Tenant, TenantGroup from tenancy.models import *
__all__ = [ __all__ = [
'NestedContactSerializer',
'NestedContactGroupSerializer',
'NestedContactRoleSerializer',
'NestedTenantGroupSerializer', 'NestedTenantGroupSerializer',
'NestedTenantSerializer', 'NestedTenantSerializer',
] ]
@ -29,3 +32,33 @@ class NestedTenantSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = Tenant model = Tenant
fields = ['id', 'url', 'display', 'name', 'slug'] 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 rest_framework import serializers
from netbox.api.serializers import NestedGroupModelSerializer, PrimaryModelSerializer from netbox.api import ContentTypeField
from tenancy.models import Tenant, TenantGroup from netbox.api.serializers import NestedGroupModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer
from tenancy.models import *
from .nested_serializers 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', 'created', 'last_updated', 'circuit_count', 'device_count', 'ipaddress_count', 'prefix_count', 'rack_count',
'site_count', 'virtualmachine_count', 'vlan_count', 'vrf_count', 'cluster_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('tenant-groups', views.TenantGroupViewSet)
router.register('tenants', views.TenantViewSet) 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' app_name = 'tenancy-api'
urlpatterns = router.urls urlpatterns = router.urls

View File

@ -5,7 +5,7 @@ from dcim.models import Device, Rack, Site
from extras.api.views import CustomFieldModelViewSet from extras.api.views import CustomFieldModelViewSet
from ipam.models import IPAddress, Prefix, VLAN, VRF from ipam.models import IPAddress, Prefix, VLAN, VRF
from tenancy import filtersets from tenancy import filtersets
from tenancy.models import Tenant, TenantGroup from tenancy.models import *
from utilities.utils import count_related from utilities.utils import count_related
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine
from . import serializers from . import serializers
@ -20,7 +20,7 @@ class TenancyRootView(APIRootView):
# #
# Tenant Groups # Tenants
# #
class TenantGroupViewSet(CustomFieldModelViewSet): class TenantGroupViewSet(CustomFieldModelViewSet):
@ -35,10 +35,6 @@ class TenantGroupViewSet(CustomFieldModelViewSet):
filterset_class = filtersets.TenantGroupFilterSet filterset_class = filtersets.TenantGroupFilterSet
#
# Tenants
#
class TenantViewSet(CustomFieldModelViewSet): class TenantViewSet(CustomFieldModelViewSet):
queryset = Tenant.objects.prefetch_related( queryset = Tenant.objects.prefetch_related(
'group', 'tags' 'group', 'tags'
@ -55,3 +51,41 @@ class TenantViewSet(CustomFieldModelViewSet):
) )
serializer_class = serializers.TenantSerializer serializer_class = serializers.TenantSerializer
filterset_class = filtersets.TenantFilterSet 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 extras.filters import TagFilter
from netbox.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet from netbox.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet
from utilities.filters import TreeNodeMultipleChoiceFilter from utilities.filters import TreeNodeMultipleChoiceFilter
from .models import Tenant, TenantGroup from .models import *
__all__ = ( __all__ = (
'ContactAssignmentFilterSet',
'ContactFilterSet',
'ContactGroupFilterSet',
'ContactRoleFilterSet',
'TenancyFilterSet', 'TenancyFilterSet',
'TenantFilterSet', 'TenantFilterSet',
'TenantGroupFilterSet', 'TenantGroupFilterSet',
) )
#
# Tenancy
#
class TenantGroupFilterSet(OrganizationalModelFilterSet): class TenantGroupFilterSet(OrganizationalModelFilterSet):
parent_id = django_filters.ModelMultipleChoiceFilter( parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=TenantGroup.objects.all(), queryset=TenantGroup.objects.all(),
@ -23,7 +31,7 @@ class TenantGroupFilterSet(OrganizationalModelFilterSet):
field_name='parent__slug', field_name='parent__slug',
queryset=TenantGroup.objects.all(), queryset=TenantGroup.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Tenant group group (slug)', label='Tenant group (slug)',
) )
class Meta: class Meta:
@ -93,3 +101,89 @@ class TenancyFilterSet(django_filters.FilterSet):
to_field_name='slug', to_field_name='slug',
label='Tenant (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 django import forms
from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm
from tenancy.models import Tenant, TenantGroup from tenancy.models import *
from utilities.forms import BootstrapMixin, DynamicModelChoiceField from utilities.forms import BootstrapMixin, DynamicModelChoiceField
__all__ = ( __all__ = (
'ContactBulkEditForm',
'ContactGroupBulkEditForm',
'ContactRoleBulkEditForm',
'TenantBulkEditForm', 'TenantBulkEditForm',
'TenantGroupBulkEditForm', 'TenantGroupBulkEditForm',
) )
#
# Tenants
#
class TenantGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): class TenantGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=TenantGroup.objects.all(), queryset=TenantGroup.objects.all(),
@ -42,3 +49,53 @@ class TenantBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulk
nullable_fields = [ nullable_fields = [
'group', '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 extras.forms import CustomFieldModelCSVForm
from tenancy.models import Tenant, TenantGroup from tenancy.models import *
from utilities.forms import CSVModelChoiceField, SlugField from utilities.forms import CSVModelChoiceField, SlugField
__all__ = ( __all__ = (
'ContactCSVForm',
'ContactGroupCSVForm',
'ContactRoleCSVForm',
'TenantCSVForm', 'TenantCSVForm',
'TenantGroupCSVForm', 'TenantGroupCSVForm',
) )
#
# Tenants
#
class TenantGroupCSVForm(CustomFieldModelCSVForm): class TenantGroupCSVForm(CustomFieldModelCSVForm):
parent = CSVModelChoiceField( parent = CSVModelChoiceField(
queryset=TenantGroup.objects.all(), queryset=TenantGroup.objects.all(),
@ -34,3 +41,43 @@ class TenantCSVForm(CustomFieldModelCSVForm):
class Meta: class Meta:
model = Tenant model = Tenant
fields = ('name', 'slug', 'group', 'description', 'comments') 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 django.utils.translation import gettext as _
from extras.forms import CustomFieldModelFilterForm from extras.forms import CustomFieldModelFilterForm
from tenancy.models import Tenant, TenantGroup from tenancy.models import *
from utilities.forms import BootstrapMixin, DynamicModelMultipleChoiceField, TagFilterField from utilities.forms import BootstrapMixin, DynamicModelMultipleChoiceField, TagFilterField
__all__ = (
'ContactFilterForm',
'ContactGroupFilterForm',
'ContactRoleFilterForm',
'TenantFilterForm',
'TenantGroupFilterForm',
)
#
# Tenants
#
class TenantGroupFilterForm(BootstrapMixin, CustomFieldModelFilterForm): class TenantGroupFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
model = TenantGroup model = TenantGroup
@ -40,3 +52,55 @@ class TenantFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
fetch_trigger='open' fetch_trigger='open'
) )
tag = TagFilterField(model) 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.forms import CustomFieldModelForm
from extras.models import Tag from extras.models import Tag
from tenancy.models import Tenant, TenantGroup from tenancy.models import *
from utilities.forms import ( from utilities.forms import (
BootstrapMixin, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField, BootstrapMixin, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField, SmallTextarea,
) )
__all__ = ( __all__ = (
'ContactForm',
'ContactGroupForm',
'ContactRoleForm',
'TenantForm', 'TenantForm',
'TenantGroupForm', 'TenantGroupForm',
) )
#
# Tenants
#
class TenantGroupForm(BootstrapMixin, CustomFieldModelForm): class TenantGroupForm(BootstrapMixin, CustomFieldModelForm):
parent = DynamicModelChoiceField( parent = DynamicModelChoiceField(
queryset=TenantGroup.objects.all(), queryset=TenantGroup.objects.all(),
@ -45,3 +52,51 @@ class TenantForm(BootstrapMixin, CustomFieldModelForm):
fieldsets = ( fieldsets = (
('Tenant', ('name', 'slug', 'group', 'description', 'tags')), ('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 = ObjectField(TenantGroupType)
tenant_group_list = ObjectListField(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 tenancy import filtersets, models
from netbox.graphql.types import OrganizationalObjectType, PrimaryObjectType from netbox.graphql.types import OrganizationalObjectType, PrimaryObjectType
__all__ = ( __all__ = (
'ContactAssignmentType',
'ContactGroupType',
'ContactRoleType',
'ContactType',
'TenantType', 'TenantType',
'TenantGroupType', '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 TenantType(PrimaryObjectType):
class Meta: class Meta:
@ -21,3 +38,39 @@ class TenantGroupType(OrganizationalObjectType):
model = models.TenantGroup model = models.TenantGroup
fields = '__all__' fields = '__all__'
filterset_class = filtersets.TenantGroupFilterSet 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.db import models
from django.urls import reverse from django.urls import reverse
from mptt.models import MPTTModel, TreeForeignKey from mptt.models import MPTTModel, TreeForeignKey
from extras.utils import extras_features 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 utilities.querysets import RestrictedQuerySet
from .choices import *
__all__ = ( __all__ = (
'ContactAssignment',
'Contact',
'ContactGroup',
'ContactRole',
'Tenant', 'Tenant',
'TenantGroup', 'TenantGroup',
) )
#
# Tenants
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class TenantGroup(NestedGroupModel): class TenantGroup(NestedGroupModel):
""" """
@ -90,3 +100,153 @@ class Tenant(PrimaryModel):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('tenancy:tenant', args=[self.pk]) 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 ( from utilities.tables import (
BaseTable, ButtonsColumn, LinkedCountColumn, MarkdownColumn, MPTTColumn, TagColumn, ToggleColumn, BaseTable, ButtonsColumn, LinkedCountColumn, MarkdownColumn, MPTTColumn, TagColumn, ToggleColumn,
) )
from .models import Tenant, TenantGroup from .models import *
__all__ = ( __all__ = (
'ContactAssignmentTable',
'ContactGroupTable',
'ContactRoleTable',
'ContactTable',
'TenantColumn', 'TenantColumn',
'TenantGroupTable', 'TenantGroupTable',
'TenantTable', 'TenantTable',
@ -38,7 +42,7 @@ class TenantColumn(tables.TemplateColumn):
# #
# Tenant groups # Tenants
# #
class TenantGroupTable(BaseTable): class TenantGroupTable(BaseTable):
@ -59,10 +63,6 @@ class TenantGroupTable(BaseTable):
default_columns = ('pk', 'name', 'tenant_count', 'description', 'actions') default_columns = ('pk', 'name', 'tenant_count', 'description', 'actions')
#
# Tenants
#
class TenantTable(BaseTable): class TenantTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.Column( name = tables.Column(
@ -80,3 +80,69 @@ class TenantTable(BaseTable):
model = Tenant model = Tenant
fields = ('pk', 'name', 'slug', 'group', 'description', 'comments', 'tags') fields = ('pk', 'name', 'slug', 'group', 'description', 'comments', 'tags')
default_columns = ('pk', 'name', 'group', 'description') 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 django.urls import reverse
from tenancy.models import Tenant, TenantGroup from tenancy.models import *
from utilities.testing import APITestCase, APIViewTestCases from utilities.testing import APITestCase, APIViewTestCases
@ -92,3 +92,112 @@ class TenantTest(APIViewTestCases.APIViewTestCase):
'group': tenant_groups[1].pk, '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 django.test import TestCase
from tenancy.filtersets import * from tenancy.filtersets import *
from tenancy.models import Tenant, TenantGroup from tenancy.models import *
from utilities.testing import ChangeLoggedFilterSetTests from utilities.testing import ChangeLoggedFilterSetTests
@ -84,3 +84,103 @@ class TenantTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'group': [group[0].slug, group[1].slug]} params = {'group': [group[0].slug, group[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) 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 from utilities.testing import ViewTestCases, create_tags
@ -74,3 +74,105 @@ class TenantTestCase(ViewTestCases.PrimaryObjectViewTestCase):
cls.bulk_edit_data = { cls.bulk_edit_data = {
'group': tenant_groups[1].pk, '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 extras.views import ObjectChangeLogView, ObjectJournalView
from utilities.views import SlugRedirectView from utilities.views import SlugRedirectView
from . import views from . import views
from .models import Tenant, TenantGroup from .models import *
app_name = 'tenancy' app_name = 'tenancy'
urlpatterns = [ urlpatterns = [
@ -32,4 +32,39 @@ urlpatterns = [
path('tenants/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='tenant_changelog', kwargs={'model': Tenant}), 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}), 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 ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF
from netbox.views import generic from netbox.views import generic
from utilities.tables import paginate_table from utilities.tables import paginate_table
from utilities.utils import count_related
from virtualization.models import VirtualMachine, Cluster from virtualization.models import VirtualMachine, Cluster
from . import filtersets, forms, tables 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') queryset = Tenant.objects.prefetch_related('group')
filterset = filtersets.TenantFilterSet filterset = filtersets.TenantFilterSet
table = tables.TenantTable 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