mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-27 10:58:37 -06:00
17170 Allow multiple Group assignments for Contacts
This commit is contained in:
parent
b5d970f7bb
commit
ea03358752
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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'),
|
||||||
)
|
)
|
||||||
|
@ -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}),
|
||||||
|
69
netbox/tenancy/migrations/0018_contact_groups.py
Normal file
69
netbox/tenancy/migrations/0018_contact_groups.py
Normal 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'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -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',
|
||||||
|
@ -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 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
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user