mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 01:41:22 -06:00
17170 Add ability to add contacts to multiple contact groups (#18885)
* 17170 Allow multiple Group assignments for Contacts * 17170 update docs * 17170 update api, detail view, graphql * 17170 fixes * 17170 fixes * 17170 fixes * 17170 fixes * 17170 fixes * 17170 fixes * 17170 fix bulk import * 17170 test fixes * 17170 test fixes * 17170 test fixes * 17178 review changes * 17178 review changes * 17178 review changes * 17178 review changes * 17178 review changes * 17178 review changes * 17170 update migration * 17170 bulk edit form
This commit is contained in:
parent
d4f8cb72aa
commit
af5ec19430
@ -4,9 +4,11 @@ A contact represents an individual or group that has been associated with an obj
|
|||||||
|
|
||||||
## Fields
|
## Fields
|
||||||
|
|
||||||
### Group
|
### Groups
|
||||||
|
|
||||||
The [contact group](./contactgroup.md) to which this contact is assigned (if any).
|
The [contact groups](./contactgroup.md) to which this contact is assigned (if any).
|
||||||
|
|
||||||
|
!!! info "This field was renamed from `group` to `groups` in NetBox v4.3, and now supports the assignment of a contact to more than one group."
|
||||||
|
|
||||||
### Name
|
### Name
|
||||||
|
|
||||||
|
@ -18,8 +18,18 @@
|
|||||||
<h2 class="card-header">{% trans "Contact" %}</h2>
|
<h2 class="card-header">{% trans "Contact" %}</h2>
|
||||||
<table class="table table-hover attr-table">
|
<table class="table table-hover attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Group" %}</th>
|
<th scope="row">{% trans "Groups" %}</th>
|
||||||
<td>{{ object.group|linkify|placeholder }}</td>
|
<td>
|
||||||
|
{% if object.groups.all|length > 0 %}
|
||||||
|
<ol class="list-unstyled mb-0">
|
||||||
|
{% for group in object.groups.all %}
|
||||||
|
<li>{{ group|linkify|placeholder }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
|
{% else %}
|
||||||
|
{{ ''|placeholder }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Name" %}</th>
|
<th scope="row">{% trans "Name" %}</th>
|
||||||
|
@ -3,7 +3,7 @@ from drf_spectacular.types import OpenApiTypes
|
|||||||
from drf_spectacular.utils import extend_schema_field
|
from drf_spectacular.utils import extend_schema_field
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from netbox.api.fields import ChoiceField, ContentTypeField
|
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
||||||
from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer
|
from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer
|
||||||
from tenancy.choices import ContactPriorityChoices
|
from tenancy.choices import ContactPriorityChoices
|
||||||
from tenancy.models import ContactAssignment, Contact, ContactGroup, ContactRole
|
from tenancy.models import ContactAssignment, Contact, ContactGroup, ContactRole
|
||||||
@ -43,12 +43,17 @@ class ContactRoleSerializer(NetBoxModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class ContactSerializer(NetBoxModelSerializer):
|
class ContactSerializer(NetBoxModelSerializer):
|
||||||
group = ContactGroupSerializer(nested=True, required=False, allow_null=True, default=None)
|
groups = SerializedPKRelatedField(
|
||||||
|
queryset=ContactGroup.objects.all(),
|
||||||
|
serializer=ContactGroupSerializer,
|
||||||
|
required=False,
|
||||||
|
many=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Contact
|
model = Contact
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'group', 'name', 'title', 'phone', 'email', 'address', 'link',
|
'id', 'url', 'display_url', 'display', 'groups', 'name', 'title', 'phone', 'email', 'address', 'link',
|
||||||
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||||
|
@ -44,7 +44,7 @@ class ContactGroupViewSet(MPTTLockedMixin, NetBoxModelViewSet):
|
|||||||
queryset = ContactGroup.objects.add_related_count(
|
queryset = ContactGroup.objects.add_related_count(
|
||||||
ContactGroup.objects.all(),
|
ContactGroup.objects.all(),
|
||||||
Contact,
|
Contact,
|
||||||
'group',
|
'groups',
|
||||||
'contact_count',
|
'contact_count',
|
||||||
cumulative=True
|
cumulative=True
|
||||||
)
|
)
|
||||||
|
@ -46,6 +46,11 @@ class ContactGroupFilterSet(OrganizationalModelFilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label=_('Contact group (slug)'),
|
label=_('Contact group (slug)'),
|
||||||
)
|
)
|
||||||
|
contact_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='contact',
|
||||||
|
queryset=Contact.objects.all(),
|
||||||
|
label=_('Contact (ID)'),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ContactGroup
|
model = ContactGroup
|
||||||
@ -62,15 +67,15 @@ class ContactRoleFilterSet(OrganizationalModelFilterSet):
|
|||||||
class ContactFilterSet(NetBoxModelFilterSet):
|
class ContactFilterSet(NetBoxModelFilterSet):
|
||||||
group_id = TreeNodeMultipleChoiceFilter(
|
group_id = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=ContactGroup.objects.all(),
|
queryset=ContactGroup.objects.all(),
|
||||||
field_name='group',
|
field_name='groups',
|
||||||
lookup_expr='in',
|
lookup_expr='in',
|
||||||
label=_('Contact group (ID)'),
|
label=_('Contact group (ID)'),
|
||||||
)
|
)
|
||||||
group = TreeNodeMultipleChoiceFilter(
|
group = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=ContactGroup.objects.all(),
|
queryset=ContactGroup.objects.all(),
|
||||||
field_name='group',
|
field_name='groups',
|
||||||
lookup_expr='in',
|
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
|
lookup_expr='in',
|
||||||
label=_('Contact group (slug)'),
|
label=_('Contact group (slug)'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -105,13 +110,13 @@ class ContactAssignmentFilterSet(NetBoxModelFilterSet):
|
|||||||
)
|
)
|
||||||
group_id = TreeNodeMultipleChoiceFilter(
|
group_id = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=ContactGroup.objects.all(),
|
queryset=ContactGroup.objects.all(),
|
||||||
field_name='contact__group',
|
field_name='contact__groups',
|
||||||
lookup_expr='in',
|
lookup_expr='in',
|
||||||
label=_('Contact group (ID)'),
|
label=_('Contact group (ID)'),
|
||||||
)
|
)
|
||||||
group = TreeNodeMultipleChoiceFilter(
|
group = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=ContactGroup.objects.all(),
|
queryset=ContactGroup.objects.all(),
|
||||||
field_name='contact__group',
|
field_name='contact__groups',
|
||||||
lookup_expr='in',
|
lookup_expr='in',
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label=_('Contact group (slug)'),
|
label=_('Contact group (slug)'),
|
||||||
@ -153,7 +158,7 @@ class ContactModelFilterSet(django_filters.FilterSet):
|
|||||||
)
|
)
|
||||||
contact_group = TreeNodeMultipleChoiceFilter(
|
contact_group = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=ContactGroup.objects.all(),
|
queryset=ContactGroup.objects.all(),
|
||||||
field_name='contacts__contact__group',
|
field_name='contacts__contact__groups',
|
||||||
lookup_expr='in',
|
lookup_expr='in',
|
||||||
label=_('Contact group'),
|
label=_('Contact group'),
|
||||||
)
|
)
|
||||||
|
@ -5,7 +5,7 @@ from netbox.forms import NetBoxModelBulkEditForm
|
|||||||
from tenancy.choices import ContactPriorityChoices
|
from tenancy.choices import ContactPriorityChoices
|
||||||
from tenancy.models import *
|
from tenancy.models import *
|
||||||
from utilities.forms import add_blank_choice
|
from utilities.forms import add_blank_choice
|
||||||
from utilities.forms.fields import CommentField, DynamicModelChoiceField
|
from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
||||||
from utilities.forms.rendering import FieldSet
|
from utilities.forms.rendering import FieldSet
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -90,8 +90,13 @@ class ContactRoleBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
|
|
||||||
|
|
||||||
class ContactBulkEditForm(NetBoxModelBulkEditForm):
|
class ContactBulkEditForm(NetBoxModelBulkEditForm):
|
||||||
group = DynamicModelChoiceField(
|
add_groups = DynamicModelMultipleChoiceField(
|
||||||
label=_('Group'),
|
label=_('Add groups'),
|
||||||
|
queryset=ContactGroup.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
remove_groups = DynamicModelMultipleChoiceField(
|
||||||
|
label=_('Remove groups'),
|
||||||
queryset=ContactGroup.objects.all(),
|
queryset=ContactGroup.objects.all(),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
@ -127,9 +132,13 @@ class ContactBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
|
|
||||||
model = Contact
|
model = Contact
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('group', 'title', 'phone', 'email', 'address', 'link', 'description'),
|
FieldSet('title', 'phone', 'email', 'address', 'link', 'description'),
|
||||||
|
FieldSet('add_groups', 'remove_groups', name=_('Groups')),
|
||||||
|
)
|
||||||
|
|
||||||
|
nullable_fields = (
|
||||||
|
'add_groups', 'remove_groups', 'title', 'phone', 'email', 'address', 'link', 'description', 'comments'
|
||||||
)
|
)
|
||||||
nullable_fields = ('group', 'title', 'phone', 'email', 'address', 'link', 'description', 'comments')
|
|
||||||
|
|
||||||
|
|
||||||
class ContactAssignmentBulkEditForm(NetBoxModelBulkEditForm):
|
class ContactAssignmentBulkEditForm(NetBoxModelBulkEditForm):
|
||||||
|
@ -3,7 +3,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
from netbox.forms import NetBoxModelImportForm
|
from netbox.forms import NetBoxModelImportForm
|
||||||
from tenancy.models import *
|
from tenancy.models import *
|
||||||
from utilities.forms.fields import CSVContentTypeField, CSVModelChoiceField, SlugField
|
from utilities.forms.fields import CSVContentTypeField, CSVModelChoiceField, CSVModelMultipleChoiceField, SlugField
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ContactAssignmentImportForm',
|
'ContactAssignmentImportForm',
|
||||||
@ -77,17 +77,16 @@ class ContactRoleImportForm(NetBoxModelImportForm):
|
|||||||
|
|
||||||
|
|
||||||
class ContactImportForm(NetBoxModelImportForm):
|
class ContactImportForm(NetBoxModelImportForm):
|
||||||
group = CSVModelChoiceField(
|
groups = CSVModelMultipleChoiceField(
|
||||||
label=_('Group'),
|
|
||||||
queryset=ContactGroup.objects.all(),
|
queryset=ContactGroup.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
help_text=_('Assigned group')
|
help_text=_('Group names separated by commas, encased with double quotes (e.g. "Group 1,Group 2")')
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Contact
|
model = Contact
|
||||||
fields = ('name', 'title', 'phone', 'email', 'address', 'link', 'group', 'description', 'comments', 'tags')
|
fields = ('name', 'title', 'phone', 'email', 'address', 'link', 'groups', 'description', 'comments', 'tags')
|
||||||
|
|
||||||
|
|
||||||
class ContactAssignmentImportForm(NetBoxModelImportForm):
|
class ContactAssignmentImportForm(NetBoxModelImportForm):
|
||||||
|
@ -75,7 +75,7 @@ class ContactFilterForm(NetBoxModelFilterSetForm):
|
|||||||
queryset=ContactGroup.objects.all(),
|
queryset=ContactGroup.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
null_option='None',
|
null_option='None',
|
||||||
label=_('Group')
|
label=_('Groups')
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
from netbox.forms import NetBoxModelForm
|
from netbox.forms import NetBoxModelForm
|
||||||
from tenancy.models import *
|
from tenancy.models import *
|
||||||
from utilities.forms.fields import CommentField, DynamicModelChoiceField, SlugField
|
from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
|
||||||
from utilities.forms.rendering import FieldSet, ObjectAttribute
|
from utilities.forms.rendering import FieldSet, ObjectAttribute
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -93,8 +93,8 @@ class ContactRoleForm(NetBoxModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class ContactForm(NetBoxModelForm):
|
class ContactForm(NetBoxModelForm):
|
||||||
group = DynamicModelChoiceField(
|
groups = DynamicModelMultipleChoiceField(
|
||||||
label=_('Group'),
|
label=_('Groups'),
|
||||||
queryset=ContactGroup.objects.all(),
|
queryset=ContactGroup.objects.all(),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
@ -102,7 +102,7 @@ class ContactForm(NetBoxModelForm):
|
|||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'group', 'name', 'title', 'phone', 'email', 'address', 'link', 'description', 'tags',
|
'groups', 'name', 'title', 'phone', 'email', 'address', 'link', 'description', 'tags',
|
||||||
name=_('Contact')
|
name=_('Contact')
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -110,7 +110,7 @@ class ContactForm(NetBoxModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Contact
|
model = Contact
|
||||||
fields = (
|
fields = (
|
||||||
'group', 'name', 'title', 'phone', 'email', 'address', 'link', 'description', 'comments', 'tags',
|
'groups', 'name', 'title', 'phone', 'email', 'address', 'link', 'description', 'comments', 'tags',
|
||||||
)
|
)
|
||||||
widgets = {
|
widgets = {
|
||||||
'address': forms.Textarea(attrs={'rows': 3}),
|
'address': forms.Textarea(attrs={'rows': 3}),
|
||||||
@ -123,7 +123,7 @@ class ContactAssignmentForm(NetBoxModelForm):
|
|||||||
queryset=ContactGroup.objects.all(),
|
queryset=ContactGroup.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
initial_params={
|
initial_params={
|
||||||
'contacts': '$contact'
|
'contact': '$contact'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
contact = DynamicModelChoiceField(
|
contact = DynamicModelChoiceField(
|
||||||
|
@ -97,7 +97,7 @@ class TenantGroupType(OrganizationalObjectType):
|
|||||||
|
|
||||||
@strawberry_django.type(models.Contact, fields='__all__', filters=ContactFilter)
|
@strawberry_django.type(models.Contact, fields='__all__', filters=ContactFilter)
|
||||||
class ContactType(ContactAssignmentsMixin, NetBoxObjectType):
|
class ContactType(ContactAssignmentsMixin, NetBoxObjectType):
|
||||||
group: Annotated['ContactGroupType', strawberry.lazy('tenancy.graphql.types')] | None
|
groups: List[Annotated['ContactGroupType', strawberry.lazy('tenancy.graphql.types')]]
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.type(models.ContactRole, fields='__all__', filters=ContactRoleFilter)
|
@strawberry_django.type(models.ContactRole, fields='__all__', filters=ContactRoleFilter)
|
||||||
|
68
netbox/tenancy/migrations/0018_contact_groups.py
Normal file
68
netbox/tenancy/migrations/0018_contact_groups.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_contact_groups(apps, schema_editor):
|
||||||
|
Contacts = apps.get_model('tenancy', 'Contact')
|
||||||
|
|
||||||
|
qs = Contacts.objects.filter(group__isnull=False)
|
||||||
|
for contact in qs:
|
||||||
|
contact.groups.add(contact.group)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('tenancy', '0017_natural_ordering'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ContactGroupMembership',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'contact group membership',
|
||||||
|
'verbose_name_plural': 'contact group memberships',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.RemoveConstraint(
|
||||||
|
model_name='contact',
|
||||||
|
name='tenancy_contact_unique_group_name',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='contactgroupmembership',
|
||||||
|
name='contact',
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, related_name='+', to='tenancy.contact'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='contactgroupmembership',
|
||||||
|
name='group',
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, related_name='+', to='tenancy.contactgroup'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='contact',
|
||||||
|
name='groups',
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name='contacts',
|
||||||
|
related_query_name='contact',
|
||||||
|
through='tenancy.ContactGroupMembership',
|
||||||
|
to='tenancy.contactgroup',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='contactgroupmembership',
|
||||||
|
constraint=models.UniqueConstraint(fields=('group', 'contact'), name='unique_group_name'),
|
||||||
|
),
|
||||||
|
migrations.RunPython(code=migrate_contact_groups, reverse_code=migrations.RunPython.noop),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='contact',
|
||||||
|
name='group',
|
||||||
|
),
|
||||||
|
]
|
@ -13,6 +13,7 @@ __all__ = (
|
|||||||
'ContactAssignment',
|
'ContactAssignment',
|
||||||
'Contact',
|
'Contact',
|
||||||
'ContactGroup',
|
'ContactGroup',
|
||||||
|
'ContactGroupMembership',
|
||||||
'ContactRole',
|
'ContactRole',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -47,12 +48,12 @@ class Contact(PrimaryModel):
|
|||||||
"""
|
"""
|
||||||
Contact information for a particular object(s) in NetBox.
|
Contact information for a particular object(s) in NetBox.
|
||||||
"""
|
"""
|
||||||
group = models.ForeignKey(
|
groups = models.ManyToManyField(
|
||||||
to='tenancy.ContactGroup',
|
to='tenancy.ContactGroup',
|
||||||
on_delete=models.SET_NULL,
|
|
||||||
related_name='contacts',
|
related_name='contacts',
|
||||||
blank=True,
|
through='tenancy.ContactGroupMembership',
|
||||||
null=True
|
related_query_name='contact',
|
||||||
|
blank=True
|
||||||
)
|
)
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
@ -84,17 +85,11 @@ class Contact(PrimaryModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
clone_fields = (
|
clone_fields = (
|
||||||
'group', 'name', 'title', 'phone', 'email', 'address', 'link',
|
'groups', 'name', 'title', 'phone', 'email', 'address', 'link',
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
constraints = (
|
|
||||||
models.UniqueConstraint(
|
|
||||||
fields=('group', 'name'),
|
|
||||||
name='%(app_label)s_%(class)s_unique_group_name'
|
|
||||||
),
|
|
||||||
)
|
|
||||||
verbose_name = _('contact')
|
verbose_name = _('contact')
|
||||||
verbose_name_plural = _('contacts')
|
verbose_name_plural = _('contacts')
|
||||||
|
|
||||||
@ -102,6 +97,18 @@ class Contact(PrimaryModel):
|
|||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class ContactGroupMembership(models.Model):
|
||||||
|
group = models.ForeignKey(ContactGroup, related_name="+", on_delete=models.CASCADE)
|
||||||
|
contact = models.ForeignKey(Contact, related_name="+", on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(fields=['group', 'contact'], name='unique_group_name')
|
||||||
|
]
|
||||||
|
verbose_name = _('contact group membership')
|
||||||
|
verbose_name_plural = _('contact group memberships')
|
||||||
|
|
||||||
|
|
||||||
class ContactAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
|
class ContactAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
|
||||||
object_type = models.ForeignKey(
|
object_type = models.ForeignKey(
|
||||||
to='contenttypes.ContentType',
|
to='contenttypes.ContentType',
|
||||||
|
@ -56,9 +56,9 @@ class ContactTable(NetBoxTable):
|
|||||||
verbose_name=_('Name'),
|
verbose_name=_('Name'),
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
group = tables.Column(
|
groups = columns.ManyToManyColumn(
|
||||||
verbose_name=_('Group'),
|
verbose_name=_('Groups'),
|
||||||
linkify=True
|
linkify_item=('tenancy:contactgroup', {'pk': tables.A('pk')})
|
||||||
)
|
)
|
||||||
phone = tables.Column(
|
phone = tables.Column(
|
||||||
verbose_name=_('Phone'),
|
verbose_name=_('Phone'),
|
||||||
@ -79,10 +79,10 @@ class ContactTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = Contact
|
model = Contact
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'name', 'group', 'title', 'phone', 'email', 'address', 'link', 'description', 'comments',
|
'pk', 'name', 'groups', 'title', 'phone', 'email', 'address', 'link', 'description', 'comments',
|
||||||
'assignment_count', 'tags', 'created', 'last_updated',
|
'assignment_count', 'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'group', 'assignment_count', 'title', 'phone', 'email')
|
default_columns = ('pk', 'name', 'groups', 'assignment_count', 'title', 'phone', 'email')
|
||||||
|
|
||||||
|
|
||||||
class ContactAssignmentTable(NetBoxTable):
|
class ContactAssignmentTable(NetBoxTable):
|
||||||
|
@ -170,7 +170,7 @@ class ContactTest(APIViewTestCases.APIViewTestCase):
|
|||||||
model = Contact
|
model = Contact
|
||||||
brief_fields = ['description', 'display', 'id', 'name', 'url']
|
brief_fields = ['description', 'display', 'id', 'name', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'group': None,
|
'groups': [],
|
||||||
'comments': 'New comments',
|
'comments': 'New comments',
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,20 +183,22 @@ class ContactTest(APIViewTestCases.APIViewTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
contacts = (
|
contacts = (
|
||||||
Contact(name='Contact 1', group=contact_groups[0]),
|
Contact(name='Contact 1'),
|
||||||
Contact(name='Contact 2', group=contact_groups[0]),
|
Contact(name='Contact 2'),
|
||||||
Contact(name='Contact 3', group=contact_groups[0]),
|
Contact(name='Contact 3'),
|
||||||
)
|
)
|
||||||
Contact.objects.bulk_create(contacts)
|
Contact.objects.bulk_create(contacts)
|
||||||
|
contacts[0].groups.add(contact_groups[0])
|
||||||
|
contacts[1].groups.add(contact_groups[0])
|
||||||
|
contacts[2].groups.add(contact_groups[0])
|
||||||
|
|
||||||
cls.create_data = [
|
cls.create_data = [
|
||||||
{
|
{
|
||||||
'name': 'Contact 4',
|
'name': 'Contact 4',
|
||||||
'group': contact_groups[1].pk,
|
'groups': [contact_groups[1].pk],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Contact 5',
|
'name': 'Contact 5',
|
||||||
'group': contact_groups[1].pk,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Contact 6',
|
'name': 'Contact 6',
|
||||||
|
@ -241,6 +241,7 @@ class ContactRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
class ContactTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class ContactTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = Contact.objects.all()
|
queryset = Contact.objects.all()
|
||||||
filterset = ContactFilterSet
|
filterset = ContactFilterSet
|
||||||
|
ignore_fields = ('groups',)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
@ -254,11 +255,14 @@ class ContactTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
contactgroup.save()
|
contactgroup.save()
|
||||||
|
|
||||||
contacts = (
|
contacts = (
|
||||||
Contact(name='Contact 1', group=contact_groups[0], description='foobar1'),
|
Contact(name='Contact 1', description='foobar1'),
|
||||||
Contact(name='Contact 2', group=contact_groups[1], description='foobar2'),
|
Contact(name='Contact 2', description='foobar2'),
|
||||||
Contact(name='Contact 3', group=contact_groups[2], description='foobar3'),
|
Contact(name='Contact 3', description='foobar3'),
|
||||||
)
|
)
|
||||||
Contact.objects.bulk_create(contacts)
|
Contact.objects.bulk_create(contacts)
|
||||||
|
contacts[0].groups.add(contact_groups[0])
|
||||||
|
contacts[1].groups.add(contact_groups[1])
|
||||||
|
contacts[2].groups.add(contact_groups[2])
|
||||||
|
|
||||||
def test_q(self):
|
def test_q(self):
|
||||||
params = {'q': 'foobar1'}
|
params = {'q': 'foobar1'}
|
||||||
@ -311,11 +315,14 @@ class ContactAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
ContactRole.objects.bulk_create(contact_roles)
|
ContactRole.objects.bulk_create(contact_roles)
|
||||||
|
|
||||||
contacts = (
|
contacts = (
|
||||||
Contact(name='Contact 1', group=contact_groups[0]),
|
Contact(name='Contact 1'),
|
||||||
Contact(name='Contact 2', group=contact_groups[1]),
|
Contact(name='Contact 2'),
|
||||||
Contact(name='Contact 3', group=contact_groups[2]),
|
Contact(name='Contact 3'),
|
||||||
)
|
)
|
||||||
Contact.objects.bulk_create(contacts)
|
Contact.objects.bulk_create(contacts)
|
||||||
|
contacts[0].groups.add(contact_groups[0])
|
||||||
|
contacts[1].groups.add(contact_groups[1])
|
||||||
|
contacts[2].groups.add(contact_groups[2])
|
||||||
|
|
||||||
assignments = (
|
assignments = (
|
||||||
ContactAssignment(object=sites[0], contact=contacts[0], role=contact_roles[0]),
|
ContactAssignment(object=sites[0], contact=contacts[0], role=contact_roles[0]),
|
||||||
|
@ -196,37 +196,40 @@ class ContactTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
contactgroup.save()
|
contactgroup.save()
|
||||||
|
|
||||||
contacts = (
|
contacts = (
|
||||||
Contact(name='Contact 1', group=contact_groups[0]),
|
Contact(name='Contact 1'),
|
||||||
Contact(name='Contact 2', group=contact_groups[0]),
|
Contact(name='Contact 2'),
|
||||||
Contact(name='Contact 3', group=contact_groups[0]),
|
Contact(name='Contact 3'),
|
||||||
)
|
)
|
||||||
Contact.objects.bulk_create(contacts)
|
Contact.objects.bulk_create(contacts)
|
||||||
|
contacts[0].groups.add(contact_groups[0])
|
||||||
|
contacts[1].groups.add(contact_groups[1])
|
||||||
|
|
||||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'Contact X',
|
'name': 'Contact X',
|
||||||
'group': contact_groups[1].pk,
|
'groups': [contact_groups[1].pk],
|
||||||
'comments': 'Some comments',
|
'comments': 'Some comments',
|
||||||
'tags': [t.pk for t in tags],
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
"group,name",
|
"name",
|
||||||
"Contact Group 1,Contact 4",
|
"groups",
|
||||||
"Contact Group 1,Contact 5",
|
"Contact 4",
|
||||||
"Contact Group 1,Contact 6",
|
"Contact 5",
|
||||||
|
"Contact 6",
|
||||||
)
|
)
|
||||||
|
|
||||||
cls.csv_update_data = (
|
cls.csv_update_data = (
|
||||||
"id,name,comments",
|
"id,name,groups,comments",
|
||||||
f"{contacts[0].pk},Contact Group 7,New comments 7",
|
f'{contacts[0].pk},Contact 7,"Contact Group 1,Contact Group 2",New comments 7',
|
||||||
f"{contacts[1].pk},Contact Group 8,New comments 8",
|
f'{contacts[1].pk},Contact 8,"Contact Group 1",New comments 8',
|
||||||
f"{contacts[2].pk},Contact Group 9,New comments 9",
|
f'{contacts[2].pk},Contact 9,"Contact Group 1",New comments 9',
|
||||||
)
|
)
|
||||||
|
|
||||||
cls.bulk_edit_data = {
|
cls.bulk_edit_data = {
|
||||||
'group': contact_groups[1].pk,
|
'description': "New description",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -170,7 +170,7 @@ class ContactGroupListView(generic.ObjectListView):
|
|||||||
queryset = ContactGroup.objects.add_related_count(
|
queryset = ContactGroup.objects.add_related_count(
|
||||||
ContactGroup.objects.all(),
|
ContactGroup.objects.all(),
|
||||||
Contact,
|
Contact,
|
||||||
'group',
|
'groups',
|
||||||
'contact_count',
|
'contact_count',
|
||||||
cumulative=True
|
cumulative=True
|
||||||
)
|
)
|
||||||
@ -214,7 +214,7 @@ class ContactGroupBulkEditView(generic.BulkEditView):
|
|||||||
queryset = ContactGroup.objects.add_related_count(
|
queryset = ContactGroup.objects.add_related_count(
|
||||||
ContactGroup.objects.all(),
|
ContactGroup.objects.all(),
|
||||||
Contact,
|
Contact,
|
||||||
'group',
|
'groups',
|
||||||
'contact_count',
|
'contact_count',
|
||||||
cumulative=True
|
cumulative=True
|
||||||
)
|
)
|
||||||
@ -228,7 +228,7 @@ class ContactGroupBulkDeleteView(generic.BulkDeleteView):
|
|||||||
queryset = ContactGroup.objects.add_related_count(
|
queryset = ContactGroup.objects.add_related_count(
|
||||||
ContactGroup.objects.all(),
|
ContactGroup.objects.all(),
|
||||||
Contact,
|
Contact,
|
||||||
'group',
|
'groups',
|
||||||
'contact_count',
|
'contact_count',
|
||||||
cumulative=True
|
cumulative=True
|
||||||
)
|
)
|
||||||
@ -337,6 +337,15 @@ class ContactBulkEditView(generic.BulkEditView):
|
|||||||
table = tables.ContactTable
|
table = tables.ContactTable
|
||||||
form = forms.ContactBulkEditForm
|
form = forms.ContactBulkEditForm
|
||||||
|
|
||||||
|
def post_save_operations(self, form, obj):
|
||||||
|
super().post_save_operations(form, obj)
|
||||||
|
|
||||||
|
# Add/remove groups
|
||||||
|
if form.cleaned_data.get('add_groups', None):
|
||||||
|
obj.groups.add(*form.cleaned_data['add_groups'])
|
||||||
|
if form.cleaned_data.get('remove_groups', None):
|
||||||
|
obj.groups.remove(*form.cleaned_data['remove_groups'])
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Contact, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(Contact, 'bulk_delete', path='delete', detail=False)
|
||||||
class ContactBulkDeleteView(generic.BulkDeleteView):
|
class ContactBulkDeleteView(generic.BulkDeleteView):
|
||||||
|
@ -144,8 +144,8 @@ class BaseFilterSetTests:
|
|||||||
# Check that the filter class is correct
|
# Check that the filter class is correct
|
||||||
filter = filters[filter_name]
|
filter = filters[filter_name]
|
||||||
if filter_class is not None:
|
if filter_class is not None:
|
||||||
self.assertIs(
|
self.assertIsInstance(
|
||||||
type(filter),
|
filter,
|
||||||
filter_class,
|
filter_class,
|
||||||
f"Invalid filter class {type(filter)} for {filter_name} (should be {filter_class})!"
|
f"Invalid filter class {type(filter)} for {filter_name} (should be {filter_class})!"
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user