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:
@ -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'),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
66
netbox/templates/tenancy/contact.html
Normal file
66
netbox/templates/tenancy/contact.html
Normal 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 %}
|
76
netbox/templates/tenancy/contactgroup.html
Normal file
76
netbox/templates/tenancy/contactgroup.html
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
{% extends 'generic/object.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load plugins %}
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% for contactgroup in object.get_ancestors %}
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'tenancy:contactgroup_list' %}?parent_id={{ contactgroup.pk }}">{{ contactgroup }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">
|
||||||
|
Contact Group
|
||||||
|
</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Name</th>
|
||||||
|
<td>{{ object.name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Description</th>
|
||||||
|
<td>{{ object.description|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Parent</th>
|
||||||
|
<td>
|
||||||
|
{% if object.parent %}
|
||||||
|
<a href="{{ object.parent.get_absolute_url }}">{{ object.parent }}</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Contacts</th>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'tenancy:contact_list' %}?group_id={{ object.pk }}">{{ contacts_table.rows|length }}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% plugin_left_page object %}
|
||||||
|
</div>
|
||||||
|
<div class="col col-md-6">
|
||||||
|
{% include 'inc/custom_fields_panel.html' %}
|
||||||
|
{% plugin_right_page object %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col col-md-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
Tenants
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% include 'inc/table.html' with table=contacts_table %}
|
||||||
|
</div>
|
||||||
|
{% if perms.tenancy.add_contact %}
|
||||||
|
<div class="card-footer text-end noprint">
|
||||||
|
<a href="{% url 'tenancy:contact_add' %}?group={{ object.pk }}" class="btn btn-sm btn-primary">
|
||||||
|
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Contact
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% include 'inc/paginator.html' with paginator=contacts_table.paginator page=contacts_table.page %}
|
||||||
|
{% plugin_full_width_page object %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
46
netbox/templates/tenancy/contactrole.html
Normal file
46
netbox/templates/tenancy/contactrole.html
Normal 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 %}
|
@ -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']
|
||||||
|
@ -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',
|
||||||
|
]
|
||||||
|
@ -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
|
||||||
|
@ -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
19
netbox/tenancy/choices.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
from utilities.choices import ChoiceSet
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Contacts
|
||||||
|
#
|
||||||
|
|
||||||
|
class ContactPriorityChoices(ChoiceSet):
|
||||||
|
PRIORITY_PRIMARY = 'primary'
|
||||||
|
PRIORITY_SECONDARY = 'secondary'
|
||||||
|
PRIORITY_TERTIARY = 'tertiary'
|
||||||
|
PRIORITY_INACTIVE = 'inactive'
|
||||||
|
|
||||||
|
CHOICES = (
|
||||||
|
(PRIORITY_PRIMARY, 'Primary'),
|
||||||
|
(PRIORITY_SECONDARY, 'Secondary'),
|
||||||
|
(PRIORITY_TERTIARY, 'Tertiary'),
|
||||||
|
(PRIORITY_INACTIVE, 'Inactive'),
|
||||||
|
)
|
@ -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']
|
||||||
|
@ -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']
|
||||||
|
@ -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')
|
||||||
|
@ -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)
|
||||||
|
@ -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}),
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
98
netbox/tenancy/migrations/0003_contacts.py
Normal file
98
netbox/tenancy/migrations/0003_contacts.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
@ -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')
|
||||||
|
@ -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')
|
||||||
|
@ -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,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
|
}
|
||||||
|
@ -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}),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
@ -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
|
||||||
|
Reference in New Issue
Block a user