diff --git a/netbox/circuits/filtersets.py b/netbox/circuits/filtersets.py index 993959c51..e22d864f9 100644 --- a/netbox/circuits/filtersets.py +++ b/netbox/circuits/filtersets.py @@ -13,6 +13,7 @@ from .models import * __all__ = ( 'CircuitFilterSet', + 'CircuitGroupAssignmentFilterSet', 'CircuitGroupFilterSet', 'CircuitTerminationFilterSet', 'CircuitTypeFilterSet', @@ -319,3 +320,30 @@ class CircuitGroupFilterSet(NetBoxModelFilterSet, TenancyFilterSet): Q(name__icontains=value) | Q(comments__icontains=value) ).distinct() + + +class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet): + q = django_filters.CharFilter( + method='search', + label=_('Search'), + ) + circuit_id = django_filters.ModelMultipleChoiceFilter( + queryset=Circuit.objects.all(), + label=_('Circuit'), + ) + group_id = django_filters.ModelMultipleChoiceFilter( + queryset=CircuitGroup.objects.all(), + label=_('Circuit group (ID)'), + ) + + class Meta: + model = CircuitGroupAssignment + fields = ('id', 'circuit', 'group', 'priority') + + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(circuit__cid__icontains=value) | + Q(group__name__icontains=value) + ) diff --git a/netbox/circuits/forms/bulk_edit.py b/netbox/circuits/forms/bulk_edit.py index 798faf961..8c05d4435 100644 --- a/netbox/circuits/forms/bulk_edit.py +++ b/netbox/circuits/forms/bulk_edit.py @@ -1,7 +1,7 @@ from django import forms from django.utils.translation import gettext_lazy as _ -from circuits.choices import CircuitCommitRateChoices, CircuitStatusChoices +from circuits.choices import CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices from circuits.models import * from dcim.models import Site from ipam.models import ASN @@ -14,6 +14,7 @@ from utilities.forms.widgets import BulkEditNullBooleanSelect, DatePicker, Numbe __all__ = ( 'CircuitBulkEditForm', + 'CircuitGroupAssignmentBulkEditForm', 'CircuitGroupBulkEditForm', 'CircuitTerminationBulkEditForm', 'CircuitTypeBulkEditForm', @@ -237,3 +238,22 @@ class CircuitGroupBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ( 'tenant', 'comments', ) + + +class CircuitGroupAssignmentBulkEditForm(NetBoxModelBulkEditForm): + circuit = DynamicModelChoiceField( + label=_('Circuit'), + queryset=Circuit.objects.all(), + required=False + ) + priority = forms.ChoiceField( + label=_('Priority'), + choices=add_blank_choice(CircuitPriorityChoices), + required=False + ) + + model = CircuitGroupAssignment + fieldsets = ( + FieldSet('circuit', 'priority'), + ) + nullable_fields = ('priority',) diff --git a/netbox/circuits/forms/bulk_import.py b/netbox/circuits/forms/bulk_import.py index a0c99eff3..44655a4ac 100644 --- a/netbox/circuits/forms/bulk_import.py +++ b/netbox/circuits/forms/bulk_import.py @@ -11,6 +11,7 @@ from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, SlugFiel __all__ = ( 'CircuitImportForm', + 'CircuitGroupAssignmentImportForm', 'CircuitGroupImportForm', 'CircuitTerminationImportForm', 'CircuitTerminationImportRelatedForm', @@ -165,3 +166,10 @@ class CircuitGroupImportForm(NetBoxModelImportForm): class Meta: model = CircuitGroup fields = ('name', 'tenant', 'tags') + + +class CircuitGroupAssignmentImportForm(NetBoxModelImportForm): + + class Meta: + model = CircuitGroupAssignment + fields = ('circuit', 'group', 'priority') diff --git a/netbox/circuits/forms/filtersets.py b/netbox/circuits/forms/filtersets.py index dd11743c6..b1c0a87b6 100644 --- a/netbox/circuits/forms/filtersets.py +++ b/netbox/circuits/forms/filtersets.py @@ -1,7 +1,7 @@ from django import forms from django.utils.translation import gettext as _ -from circuits.choices import CircuitCommitRateChoices, CircuitStatusChoices, CircuitTerminationSideChoices +from circuits.choices import CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices, CircuitTerminationSideChoices from circuits.models import * from dcim.models import Region, Site, SiteGroup from ipam.models import ASN @@ -13,6 +13,7 @@ from utilities.forms.widgets import DatePicker, NumberWithOptions __all__ = ( 'CircuitFilterForm', + 'CircuitGroupAssignmentFilterForm', 'CircuitGroupFilterForm', 'CircuitTerminationFilterForm', 'CircuitTypeFilterForm', @@ -241,3 +242,27 @@ class CircuitGroupFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): ) selector_fields = ('filter_id', 'q', ) tag = TagFilterField(model) + + +class CircuitGroupAssignmentFilterForm(NetBoxModelFilterSetForm): + model = CircuitGroupAssignment + fieldsets = ( + FieldSet('q', 'filter_id', 'tag'), + FieldSet('circuit_id', 'group_id', 'priority', name=_('Assignment')), + ) + circuit_id = DynamicModelMultipleChoiceField( + queryset=Circuit.objects.all(), + required=False, + label=_('Circuit') + ) + group_id = DynamicModelMultipleChoiceField( + queryset=CircuitGroup.objects.all(), + required=False, + label=_('Group') + ) + priority = forms.MultipleChoiceField( + label=_('Priority'), + choices=CircuitPriorityChoices, + required=False + ) + tag = TagFilterField(model) diff --git a/netbox/circuits/forms/model_forms.py b/netbox/circuits/forms/model_forms.py index 4d1890b66..538148421 100644 --- a/netbox/circuits/forms/model_forms.py +++ b/netbox/circuits/forms/model_forms.py @@ -12,6 +12,7 @@ from utilities.forms.widgets import DatePicker, NumberWithOptions __all__ = ( 'CircuitForm', + 'CircuitGroupAssignmentForm', 'CircuitGroupForm', 'CircuitTerminationForm', 'CircuitTypeForm', @@ -183,3 +184,29 @@ class CircuitGroupForm(TenancyForm, NetBoxModelForm): 'name', 'tenant_group', 'tenant', 'comments', 'tags', ] + + +class CircuitGroupAssignmentForm(NetBoxModelForm): + group = DynamicModelChoiceField( + label=_('Group'), + queryset=CircuitGroup.objects.all(), + required=False, + initial_params={ + 'groups': '$group' + } + ) + circuit = DynamicModelChoiceField( + label=_('Circuit'), + queryset=Circuit.objects.all(), + required=False, + initial_params={ + 'circuits': '$circuit' + } + ) + + class Meta: + model = CircuitGroupAssignment + fields = [ + 'group', 'circuit', 'priority', + 'tags', + ] diff --git a/netbox/circuits/migrations/0044_circuitgroup_circuitgroupassignment_and_more.py b/netbox/circuits/migrations/0044_circuitgroup_circuitgroupassignment_and_more.py index e393ffcbd..7b888c7c3 100644 --- a/netbox/circuits/migrations/0044_circuitgroup_circuitgroupassignment_and_more.py +++ b/netbox/circuits/migrations/0044_circuitgroup_circuitgroupassignment_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.7 on 2024-07-17 14:08 +# Generated by Django 5.0.7 on 2024-07-19 04:18 import django.db.models.deletion import taggit.managers @@ -51,14 +51,39 @@ class Migration(migrations.Migration): name='CircuitGroupAssignment', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('created', models.DateTimeField(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=utilities.json.CustomFieldJSONEncoder), + ), ('priority', models.CharField(blank=True, max_length=50)), - ('circuit', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='circuits.circuit')), + ( + 'circuit', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='circuit_group_assignments', + to='circuits.circuit', + ), + ), ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='circuits.circuitgroup')), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), ], + options={ + 'verbose_name': 'Circuit group assignment', + 'verbose_name_plural': 'Circuit group assignments', + 'ordering': ('circuit', 'priority', 'pk'), + }, ), migrations.AddField( model_name='circuitgroup', name='circuits', field=models.ManyToManyField(through='circuits.CircuitGroupAssignment', to='circuits.circuit'), ), + migrations.AddConstraint( + model_name='circuitgroupassignment', + constraint=models.UniqueConstraint( + fields=('circuit', 'group'), name='circuits_circuitgroupassignment_unique_circuit_assignment' + ), + ), ] diff --git a/netbox/circuits/models/circuits.py b/netbox/circuits/models/circuits.py index bbb3cf1dc..305edb070 100644 --- a/netbox/circuits/models/circuits.py +++ b/netbox/circuits/models/circuits.py @@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _ from circuits.choices import * from dcim.models import CabledObjectModel from netbox.models import ChangeLoggedModel, OrganizationalModel, PrimaryModel -from netbox.models.features import ContactsMixin, CustomFieldsMixin, CustomLinksMixin, ImageAttachmentsMixin, TagsMixin +from netbox.models.features import ContactsMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, ImageAttachmentsMixin, TagsMixin from utilities.fields import ColorField __all__ = ( @@ -182,9 +182,16 @@ class CircuitGroup(PrimaryModel): return reverse('circuits:circuitgroup', args=[self.pk]) -class CircuitGroupAssignment(models.Model): - circuit = models.ForeignKey(Circuit, on_delete=models.CASCADE) - group = models.ForeignKey(CircuitGroup, on_delete=models.CASCADE) +class CircuitGroupAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel): + circuit = models.ForeignKey( + Circuit, + on_delete=models.CASCADE, + related_name='circuit_group_assignments' + ) + group = models.ForeignKey( + CircuitGroup, + on_delete=models.CASCADE, + ) priority = models.CharField( verbose_name=_('priority'), max_length=50, @@ -192,6 +199,25 @@ class CircuitGroupAssignment(models.Model): blank=True ) + class Meta: + ordering = ('circuit', 'priority', 'pk') + constraints = ( + models.UniqueConstraint( + fields=('circuit', 'group',), + name='%(app_label)s_%(class)s_unique_circuit_assignment' + ), + ) + verbose_name = _('Circuit group assignment') + verbose_name_plural = _('Circuit group assignments') + + def __str__(self): + if self.priority: + return f"{self.group} ({self.priority}) -> {self.circuit}" + return str(f"{self.group} -> {self.circuit}") + + def get_absolute_url(self): + return reverse('circuits:circuitgroupassignment', args=[self.pk]) + class CircuitTermination( CustomFieldsMixin, diff --git a/netbox/circuits/tables/circuits.py b/netbox/circuits/tables/circuits.py index 322bfd5b0..8e46e293e 100644 --- a/netbox/circuits/tables/circuits.py +++ b/netbox/circuits/tables/circuits.py @@ -9,6 +9,7 @@ from netbox.tables import NetBoxTable, columns from .columns import CommitRateColumn __all__ = ( + 'CircuitGroupAssignmentTable', 'CircuitGroupTable', 'CircuitTable', 'CircuitTerminationTable', @@ -134,3 +135,24 @@ class CircuitGroupTable(NetBoxTable): 'pk', 'name', 'created', 'last_updated', 'actions', ) default_columns = ('pk', 'name',) + + +class CircuitGroupAssignmentTable(NetBoxTable): + group = tables.Column( + verbose_name=_('Group'), + linkify=True + ) + circuit = tables.Column( + verbose_name=_('Circuit'), + linkify=True + ) + priority = tables.Column( + verbose_name=_('Priority'), + ) + + class Meta(NetBoxTable.Meta): + model = CircuitGroupAssignment + fields = ( + 'pk', 'id', 'group', 'circuit', 'priority', 'created', 'last_updated', 'actions', + ) + default_columns = ('pk', 'group', 'circuit', 'priority') diff --git a/netbox/circuits/urls.py b/netbox/circuits/urls.py index b9387aa9b..2171d49be 100644 --- a/netbox/circuits/urls.py +++ b/netbox/circuits/urls.py @@ -55,11 +55,19 @@ urlpatterns = [ path('circuit-terminations/delete/', views.CircuitTerminationBulkDeleteView.as_view(), name='circuittermination_bulk_delete'), path('circuit-terminations//', include(get_model_urls('circuits', 'circuittermination'))), - # Circuit Redundacy Groups - path('circuit-redundancy-groups/', views.CircuitGroupListView.as_view(), name='circuitgroup_list'), - path('circuit-redundancy-groups/add/', views.CircuitGroupEditView.as_view(), name='circuitgroup_add'), - path('circuit-redundancy-groups/import/', views.CircuitGroupBulkImportView.as_view(), name='circuitgroup_import'), - path('circuit-redundancy-groups/edit/', views.CircuitGroupBulkEditView.as_view(), name='circuitgroup_bulk_edit'), - path('circuit-redundancy-groups/delete/', views.CircuitGroupBulkDeleteView.as_view(), name='circuitgroup_bulk_delete'), - path('circuit-redundancy-groups//', include(get_model_urls('circuits', 'circuitgroup'))), + # Circuit Groups + path('circuit-groups/', views.CircuitGroupListView.as_view(), name='circuitgroup_list'), + path('circuit-groups/add/', views.CircuitGroupEditView.as_view(), name='circuitgroup_add'), + path('circuit-groups/import/', views.CircuitGroupBulkImportView.as_view(), name='circuitgroup_import'), + path('circuit-groups/edit/', views.CircuitGroupBulkEditView.as_view(), name='circuitgroup_bulk_edit'), + path('circuit-groups/delete/', views.CircuitGroupBulkDeleteView.as_view(), name='circuitgroup_bulk_delete'), + path('circuit-groups//', include(get_model_urls('circuits', 'circuitgroup'))), + + # Circuit Group Assignments + path('circuit-group-assignments/', views.CircuitGroupAssignmentListView.as_view(), name='circuitgroupassignment_list'), + path('circuit-group-assignments/add/', views.CircuitGroupAssignmentEditView.as_view(), name='circuitgroupassignment_add'), + path('circuit-group-assignments/import/', views.CircuitGroupAssignmentBulkImportView.as_view(), name='circuitgroupassignment_import'), + path('circuit-group-assignments/edit/', views.CircuitGroupAssignmentBulkEditView.as_view(), name='circuitgroupassignment_bulk_edit'), + path('circuit-group-assignments/delete/', views.CircuitGroupAssignmentBulkDeleteView.as_view(), name='circuitgroupassignment_bulk_delete'), + path('circuit-group-assignments//', include(get_model_urls('circuits', 'circuitgroupassignment'))), ] diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index f9a5db4a9..925bb2fbd 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -443,7 +443,7 @@ register_model_view(CircuitTermination, 'trace', kwargs={'model': CircuitTermina # -# Circuit Redundacy Groups +# Circuit Groups # class CircuitGroupListView(generic.ObjectListView): @@ -485,3 +485,48 @@ class CircuitGroupBulkDeleteView(generic.BulkDeleteView): queryset = CircuitGroup.objects.all() filterset = filtersets.CircuitGroupFilterSet table = tables.CircuitGroupTable + + +# +# Circuit Groups +# + +class CircuitGroupAssignmentListView(generic.ObjectListView): + queryset = CircuitGroupAssignment.objects.all() + filterset = filtersets.CircuitGroupAssignmentFilterSet + filterset_form = forms.CircuitGroupAssignmentFilterForm + table = tables.CircuitGroupAssignmentTable + + +@register_model_view(CircuitGroup) +class CircuitGroupAssignmentView(generic.ObjectView): + queryset = CircuitGroupAssignment.objects.all() + + +@register_model_view(CircuitGroup, 'edit') +class CircuitGroupAssignmentEditView(generic.ObjectEditView): + queryset = CircuitGroupAssignment.objects.all() + form = forms.CircuitGroupAssignmentForm + + +@register_model_view(CircuitGroup, 'delete') +class CircuitGroupAssignmentDeleteView(generic.ObjectDeleteView): + queryset = CircuitGroupAssignment.objects.all() + + +class CircuitGroupAssignmentBulkImportView(generic.BulkImportView): + queryset = CircuitGroupAssignment.objects.all() + model_form = forms.CircuitGroupAssignmentImportForm + + +class CircuitGroupAssignmentBulkEditView(generic.BulkEditView): + queryset = CircuitGroupAssignment.objects.all() + filterset = filtersets.CircuitGroupAssignmentFilterSet + table = tables.CircuitGroupAssignmentTable + form = forms.CircuitGroupAssignmentBulkEditForm + + +class CircuitGroupAssignmentBulkDeleteView(generic.BulkDeleteView): + queryset = CircuitGroupAssignment.objects.all() + filterset = filtersets.CircuitGroupAssignmentFilterSet + table = tables.CircuitGroupAssignmentTable diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py index b07cd7f0f..73b681bab 100644 --- a/netbox/netbox/navigation/menu.py +++ b/netbox/netbox/navigation/menu.py @@ -261,6 +261,7 @@ CIRCUITS_MENU = Menu( get_model_item('circuits', 'circuittype', _('Circuit Types')), get_model_item('circuits', 'circuittermination', _('Circuit Terminations')), get_model_item('circuits', 'circuitgroup', _('Circuit Groups')), + get_model_item('circuits', 'circuitgroupassignment', _('Circuit Group Assignments')), ), ), MenuGroup(