17170 Allow multiple Group assignments for Contacts

This commit is contained in:
Arthur 2025-03-11 15:43:33 -07:00
parent b5d970f7bb
commit ea03358752
7 changed files with 104 additions and 30 deletions

View File

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

View File

@ -62,13 +62,13 @@ 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__slug',
lookup_expr='in', lookup_expr='in',
to_field_name='slug', to_field_name='slug',
label=_('Contact group (slug)'), label=_('Contact group (slug)'),
@ -105,13 +105,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__slug',
lookup_expr='in', lookup_expr='in',
to_field_name='slug', to_field_name='slug',
label=_('Contact group (slug)'), label=_('Contact group (slug)'),
@ -153,7 +153,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'),
) )

View File

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

View File

@ -0,0 +1,69 @@
# Generated by Django 5.1.5 on 2025-03-11 20:27
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)
contact.save()
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)),
],
),
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, to='tenancy.contact'),
),
migrations.AddField(
model_name='contactgroupmembership',
name='group',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tenancy.contactgroup'),
),
migrations.AddField(
model_name='contact',
name='groups',
field=models.ManyToManyField(
blank=True,
related_name='group_contacts',
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',
),
migrations.AlterField(
model_name='contact',
name='groups',
field=models.ManyToManyField(
blank=True, related_name='contacts', through='tenancy.ContactGroupMembership', to='tenancy.contactgroup'
),
),
]

View File

@ -47,12 +47,11 @@ 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 blank=True
) )
name = models.CharField( name = models.CharField(
verbose_name=_('name'), verbose_name=_('name'),
@ -84,17 +83,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 +95,16 @@ class Contact(PrimaryModel):
return self.name return self.name
class ContactGroupMembership(models.Model):
group = models.ForeignKey(ContactGroup, on_delete=models.CASCADE)
contact = models.ForeignKey(Contact, on_delete=models.CASCADE)
class Meta:
constraints = [
models.UniqueConstraint(fields=['group', 'contact'], name='unique_group_name')
]
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',

View File

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

View File

@ -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
) )
@ -183,12 +183,14 @@ class ContactGroupListView(generic.ObjectListView):
class ContactGroupView(GetRelatedModelsMixin, generic.ObjectView): class ContactGroupView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ContactGroup.objects.all() queryset = ContactGroup.objects.all()
"""
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
groups = instance.get_descendants(include_self=True) groups = instance.get_descendants(include_self=True)
return { return {
'related_models': self.get_related_models(request, groups), 'related_models': self.get_related_models(request, groups),
} }
"""
@register_model_view(ContactGroup, 'add', detail=False) @register_model_view(ContactGroup, 'add', detail=False)
@ -214,7 +216,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 +230,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
) )