mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-13 16:47:34 -06:00
7025 circuit redundancy (#16945)
* 7025 CircuitRedundancyGroups * 7025 CircuitRedundancyGroups api * 7025 CircuitRedundancyGroups api * 7025 CircuitRedundancyGroups tests * 7025 CircuitRedundancyGroup -> CircuitGroup * 7025 add tenancy * 7025 linkify name * 7025 missing file * 7025 circuitgroupassignment * 7025 base group assignment working * 7025 assignments * 7025 fix forms/tests for CircuitGroup * 7025 fix api tests * 7025 view tests * 7025 CircuitGroupAssignment tests * 7025 fix typo * 7025 fix typo * 7025 fix tests * 7025 remove m2m * 7025 add count to serializer * 7025 fix test * 7025 documentation * 7025 review comments * 7025 review comments * 7025 add search index * Make CircuitPriorityChoices extensible * Add group assignment table to circuit view * Misc cleanup --------- Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
This commit is contained in:
parent
f7fdfdd925
commit
8237c6accc
13
docs/models/circuits/circuitgroup.md
Normal file
13
docs/models/circuits/circuitgroup.md
Normal file
@ -0,0 +1,13 @@
|
||||
# Circuit Groups
|
||||
|
||||
[Circuits](./circuit.md) can be arranged into administrative groups for organization. The assignment of a circuit to a group is optional.
|
||||
|
||||
## Fields
|
||||
|
||||
### Name
|
||||
|
||||
A unique human-friendly name.
|
||||
|
||||
### Slug
|
||||
|
||||
A unique URL-friendly identifier. (This value can be used for filtering.)
|
25
docs/models/circuits/circuitgroupassignment.md
Normal file
25
docs/models/circuits/circuitgroupassignment.md
Normal file
@ -0,0 +1,25 @@
|
||||
# Circuit Group Assignments
|
||||
|
||||
Circuits can be assigned to [circuit groups](./circuitgroup.md) for correlation purposes. For instance, three circuits, each belonging to a different provider, may each be assigned to the same circuit group. Each assignment may optionally include a priority designation.
|
||||
|
||||
## Fields
|
||||
|
||||
### Group
|
||||
|
||||
The [circuit group](./circuitgroup.md) being assigned.
|
||||
|
||||
### Circuit
|
||||
|
||||
The [circuit](./circuit.md) that is being assigned to the group.
|
||||
|
||||
### Priority
|
||||
|
||||
The circuit's operation priority relative to its peers within the group. The assignment of a priority is optional. Choices include:
|
||||
|
||||
* Primary
|
||||
* Secondary
|
||||
* Tertiary
|
||||
* Inactive
|
||||
|
||||
!!! tip
|
||||
Additional priority choices may be defined by setting `CircuitGroupAssignment.priority` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
|
@ -164,6 +164,8 @@ nav:
|
||||
- Data Model:
|
||||
- Circuits:
|
||||
- Circuit: 'models/circuits/circuit.md'
|
||||
- CircuitGroup: 'models/circuits/circuitgroup.md'
|
||||
- CircuitGroupAssignment: 'models/circuits/circuitgroupassignment.md'
|
||||
- Circuit Termination: 'models/circuits/circuittermination.md'
|
||||
- Circuit Type: 'models/circuits/circuittype.md'
|
||||
- Provider: 'models/circuits/provider.md'
|
||||
|
@ -1,7 +1,7 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from circuits.choices import CircuitStatusChoices
|
||||
from circuits.models import Circuit, CircuitTermination, CircuitType
|
||||
from circuits.choices import CircuitPriorityChoices, CircuitStatusChoices
|
||||
from circuits.models import Circuit, CircuitGroup, CircuitGroupAssignment, CircuitTermination, CircuitType
|
||||
from dcim.api.serializers_.cables import CabledObjectSerializer
|
||||
from dcim.api.serializers_.sites import SiteSerializer
|
||||
from netbox.api.fields import ChoiceField, RelatedObjectCountField
|
||||
@ -12,6 +12,8 @@ from .providers import ProviderAccountSerializer, ProviderNetworkSerializer, Pro
|
||||
|
||||
__all__ = (
|
||||
'CircuitSerializer',
|
||||
'CircuitGroupAssignmentSerializer',
|
||||
'CircuitGroupSerializer',
|
||||
'CircuitTerminationSerializer',
|
||||
'CircuitTypeSerializer',
|
||||
)
|
||||
@ -43,6 +45,34 @@ class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
|
||||
]
|
||||
|
||||
|
||||
class CircuitGroupSerializer(NetBoxModelSerializer):
|
||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||
circuit_count = RelatedObjectCountField('assignments')
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroup
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'tenant',
|
||||
'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count'
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name')
|
||||
|
||||
|
||||
class CircuitGroupAssignmentSerializer_(NetBoxModelSerializer):
|
||||
"""
|
||||
Base serializer for group assignments under CircuitSerializer.
|
||||
"""
|
||||
group = CircuitGroupSerializer(nested=True)
|
||||
priority = ChoiceField(choices=CircuitPriorityChoices, allow_blank=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroupAssignment
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'group', 'priority', 'tags', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'group', 'priority')
|
||||
|
||||
|
||||
class CircuitSerializer(NetBoxModelSerializer):
|
||||
provider = ProviderSerializer(nested=True)
|
||||
provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True, default=None)
|
||||
@ -51,13 +81,14 @@ class CircuitSerializer(NetBoxModelSerializer):
|
||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||
termination_a = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
|
||||
termination_z = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
|
||||
assignments = CircuitGroupAssignmentSerializer_(nested=True, many=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant',
|
||||
'install_date', 'termination_date', 'commit_rate', 'description', 'termination_a', 'termination_z',
|
||||
'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'assignments',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'cid', 'description')
|
||||
|
||||
@ -75,3 +106,14 @@ class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer
|
||||
'link_peers', 'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'circuit', 'term_side', 'description', 'cable', '_occupied')
|
||||
|
||||
|
||||
class CircuitGroupAssignmentSerializer(CircuitGroupAssignmentSerializer_):
|
||||
circuit = CircuitSerializer(nested=True)
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroupAssignment
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'group', 'circuit', 'priority', 'tags', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'group', 'circuit', 'priority')
|
||||
|
@ -14,6 +14,8 @@ router.register('provider-networks', views.ProviderNetworkViewSet)
|
||||
router.register('circuit-types', views.CircuitTypeViewSet)
|
||||
router.register('circuits', views.CircuitViewSet)
|
||||
router.register('circuit-terminations', views.CircuitTerminationViewSet)
|
||||
router.register('circuit-groups', views.CircuitGroupViewSet)
|
||||
router.register('circuit-group-assignments', views.CircuitGroupAssignmentViewSet)
|
||||
|
||||
app_name = 'circuits-api'
|
||||
urlpatterns = router.urls
|
||||
|
@ -55,6 +55,26 @@ class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
||||
filterset_class = filtersets.CircuitTerminationFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Circuit Groups
|
||||
#
|
||||
|
||||
class CircuitGroupViewSet(NetBoxModelViewSet):
|
||||
queryset = CircuitGroup.objects.all()
|
||||
serializer_class = serializers.CircuitGroupSerializer
|
||||
filterset_class = filtersets.CircuitGroupFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Circuit Group Assignments
|
||||
#
|
||||
|
||||
class CircuitGroupAssignmentViewSet(NetBoxModelViewSet):
|
||||
queryset = CircuitGroupAssignment.objects.all()
|
||||
serializer_class = serializers.CircuitGroupAssignmentSerializer
|
||||
filterset_class = filtersets.CircuitGroupAssignmentFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Provider accounts
|
||||
#
|
||||
|
@ -76,3 +76,19 @@ class CircuitTerminationPortSpeedChoices(ChoiceSet):
|
||||
(1544, 'T1 (1.544 Mbps)'),
|
||||
(2048, 'E1 (2.048 Mbps)'),
|
||||
]
|
||||
|
||||
|
||||
class CircuitPriorityChoices(ChoiceSet):
|
||||
key = 'CircuitGroupAssignment.priority'
|
||||
|
||||
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')),
|
||||
]
|
||||
|
@ -13,6 +13,8 @@ from .models import *
|
||||
|
||||
__all__ = (
|
||||
'CircuitFilterSet',
|
||||
'CircuitGroupAssignmentFilterSet',
|
||||
'CircuitGroupFilterSet',
|
||||
'CircuitTerminationFilterSet',
|
||||
'CircuitTypeFilterSet',
|
||||
'ProviderNetworkFilterSet',
|
||||
@ -303,3 +305,43 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
||||
Q(pp_info__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
).distinct()
|
||||
|
||||
|
||||
class CircuitGroupFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroup
|
||||
fields = ('id', 'name', 'slug', 'description')
|
||||
|
||||
|
||||
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)'),
|
||||
)
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='group__slug',
|
||||
queryset=CircuitGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
label=_('Circuit group (slug)'),
|
||||
)
|
||||
|
||||
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)
|
||||
)
|
||||
|
@ -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,8 @@ from utilities.forms.widgets import BulkEditNullBooleanSelect, DatePicker, Numbe
|
||||
|
||||
__all__ = (
|
||||
'CircuitBulkEditForm',
|
||||
'CircuitGroupAssignmentBulkEditForm',
|
||||
'CircuitGroupBulkEditForm',
|
||||
'CircuitTerminationBulkEditForm',
|
||||
'CircuitTypeBulkEditForm',
|
||||
'ProviderBulkEditForm',
|
||||
@ -219,3 +221,40 @@ class CircuitTerminationBulkEditForm(NetBoxModelBulkEditForm):
|
||||
FieldSet('port_speed', 'upstream_speed', name=_('Termination Details')),
|
||||
)
|
||||
nullable_fields = ('description')
|
||||
|
||||
|
||||
class CircuitGroupBulkEditForm(NetBoxModelBulkEditForm):
|
||||
description = forms.CharField(
|
||||
label=_('Description'),
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
tenant = DynamicModelChoiceField(
|
||||
label=_('Tenant'),
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
model = CircuitGroup
|
||||
nullable_fields = (
|
||||
'description', 'tenant',
|
||||
)
|
||||
|
||||
|
||||
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',)
|
||||
|
@ -11,6 +11,8 @@ from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, SlugFiel
|
||||
|
||||
__all__ = (
|
||||
'CircuitImportForm',
|
||||
'CircuitGroupAssignmentImportForm',
|
||||
'CircuitGroupImportForm',
|
||||
'CircuitTerminationImportForm',
|
||||
'CircuitTerminationImportRelatedForm',
|
||||
'CircuitTypeImportForm',
|
||||
@ -150,3 +152,24 @@ class CircuitTerminationImportForm(NetBoxModelImportForm, BaseCircuitTermination
|
||||
'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
|
||||
'pp_info', 'description', 'tags'
|
||||
]
|
||||
|
||||
|
||||
class CircuitGroupImportForm(NetBoxModelImportForm):
|
||||
tenant = CSVModelChoiceField(
|
||||
label=_('Tenant'),
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text=_('Assigned tenant')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroup
|
||||
fields = ('name', 'slug', 'description', 'tenant', 'tags')
|
||||
|
||||
|
||||
class CircuitGroupAssignmentImportForm(NetBoxModelImportForm):
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroupAssignment
|
||||
fields = ('circuit', 'group', 'priority')
|
||||
|
@ -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,8 @@ from utilities.forms.widgets import DatePicker, NumberWithOptions
|
||||
|
||||
__all__ = (
|
||||
'CircuitFilterForm',
|
||||
'CircuitGroupAssignmentFilterForm',
|
||||
'CircuitGroupFilterForm',
|
||||
'CircuitTerminationFilterForm',
|
||||
'CircuitTypeFilterForm',
|
||||
'ProviderFilterForm',
|
||||
@ -230,3 +232,36 @@ class CircuitTerminationFilterForm(NetBoxModelFilterSetForm):
|
||||
label=_('Provider')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class CircuitGroupFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
model = CircuitGroup
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
)
|
||||
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)
|
||||
|
@ -12,6 +12,8 @@ from utilities.forms.widgets import DatePicker, NumberWithOptions
|
||||
|
||||
__all__ = (
|
||||
'CircuitForm',
|
||||
'CircuitGroupAssignmentForm',
|
||||
'CircuitGroupForm',
|
||||
'CircuitTerminationForm',
|
||||
'CircuitTypeForm',
|
||||
'ProviderForm',
|
||||
@ -171,3 +173,35 @@ class CircuitTerminationForm(NetBoxModelForm):
|
||||
options=CircuitTerminationPortSpeedChoices
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class CircuitGroupForm(TenancyForm, NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('name', 'slug', 'description', 'tags', name=_('Circuit Group')),
|
||||
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroup
|
||||
fields = [
|
||||
'name', 'slug', 'description', 'tenant_group', 'tenant', 'tags',
|
||||
]
|
||||
|
||||
|
||||
class CircuitGroupAssignmentForm(NetBoxModelForm):
|
||||
group = DynamicModelChoiceField(
|
||||
label=_('Group'),
|
||||
queryset=CircuitGroup.objects.all(),
|
||||
)
|
||||
circuit = DynamicModelChoiceField(
|
||||
label=_('Circuit'),
|
||||
queryset=Circuit.objects.all(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroupAssignment
|
||||
fields = [
|
||||
'group', 'circuit', 'priority', 'tags',
|
||||
]
|
||||
|
@ -7,6 +7,8 @@ from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
|
||||
__all__ = (
|
||||
'CircuitTerminationFilter',
|
||||
'CircuitFilter',
|
||||
'CircuitGroupAssignmentFilter',
|
||||
'CircuitGroupFilter',
|
||||
'CircuitTypeFilter',
|
||||
'ProviderFilter',
|
||||
'ProviderAccountFilter',
|
||||
@ -32,6 +34,18 @@ class CircuitTypeFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.CircuitGroup, lookups=True)
|
||||
@autotype_decorator(filtersets.CircuitGroupFilterSet)
|
||||
class CircuitGroupFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.CircuitGroupAssignment, lookups=True)
|
||||
@autotype_decorator(filtersets.CircuitGroupAssignmentFilterSet)
|
||||
class CircuitGroupAssignmentFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Provider, lookups=True)
|
||||
@autotype_decorator(filtersets.ProviderFilterSet)
|
||||
class ProviderFilter(BaseFilterMixin):
|
||||
|
@ -24,6 +24,16 @@ class CircuitsQuery:
|
||||
return models.CircuitType.objects.get(pk=id)
|
||||
circuit_type_list: List[CircuitTypeType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def circuit_group(self, id: int) -> CircuitGroupType:
|
||||
return models.CircuitGroup.objects.get(pk=id)
|
||||
circuit_group_list: List[CircuitGroupType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def circuit_group_assignment(self, id: int) -> CircuitGroupAssignmentType:
|
||||
return models.CircuitGroupAssignment.objects.get(pk=id)
|
||||
circuit_group_assignment_list: List[CircuitGroupAssignmentType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def provider(self, id: int) -> ProviderType:
|
||||
return models.Provider.objects.get(pk=id)
|
||||
|
@ -6,13 +6,15 @@ import strawberry_django
|
||||
from circuits import models
|
||||
from dcim.graphql.mixins import CabledObjectMixin
|
||||
from extras.graphql.mixins import ContactsMixin, CustomFieldsMixin, TagsMixin
|
||||
from netbox.graphql.types import NetBoxObjectType, ObjectType, OrganizationalObjectType
|
||||
from netbox.graphql.types import BaseObjectType, NetBoxObjectType, ObjectType, OrganizationalObjectType
|
||||
from tenancy.graphql.types import TenantType
|
||||
from .filters import *
|
||||
|
||||
__all__ = (
|
||||
'CircuitTerminationType',
|
||||
'CircuitType',
|
||||
'CircuitGroupAssignmentType',
|
||||
'CircuitGroupType',
|
||||
'CircuitTypeType',
|
||||
'ProviderType',
|
||||
'ProviderAccountType',
|
||||
@ -91,3 +93,22 @@ class CircuitType(NetBoxObjectType, ContactsMixin):
|
||||
tenant: TenantType | None
|
||||
|
||||
terminations: List[CircuitTerminationType]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.CircuitGroup,
|
||||
fields='__all__',
|
||||
filters=CircuitGroupFilter
|
||||
)
|
||||
class CircuitGroupType(OrganizationalObjectType):
|
||||
tenant: TenantType | None
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.CircuitGroupAssignment,
|
||||
fields='__all__',
|
||||
filters=CircuitGroupAssignmentFilter
|
||||
)
|
||||
class CircuitGroupAssignmentType(TagsMixin, BaseObjectType):
|
||||
group: Annotated["CircuitGroupType", strawberry.lazy('circuits.graphql.types')]
|
||||
circuit: Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]
|
||||
|
@ -0,0 +1,90 @@
|
||||
# Generated by Django 5.0.7 on 2024-07-22 06:27
|
||||
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
import utilities.json
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0043_circuittype_color'),
|
||||
('extras', '0118_notifications'),
|
||||
('tenancy', '0015_contactassignment_rename_content_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CircuitGroup',
|
||||
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),
|
||||
),
|
||||
('name', models.CharField(max_length=100, unique=True)),
|
||||
('slug', models.SlugField(max_length=100, unique=True)),
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
(
|
||||
'tenant',
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='circuit_groups',
|
||||
to='tenancy.tenant',
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Circuit group',
|
||||
'verbose_name_plural': 'Circuit group',
|
||||
'ordering': ('name',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
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,
|
||||
related_name='assignments',
|
||||
to='circuits.circuit',
|
||||
),
|
||||
),
|
||||
(
|
||||
'group',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='assignments',
|
||||
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.AddConstraint(
|
||||
model_name='circuitgroupassignment',
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=('circuit', 'group'), name='circuits_circuitgroupassignment_unique_circuit_group'
|
||||
),
|
||||
),
|
||||
]
|
@ -6,11 +6,13 @@ 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__ = (
|
||||
'Circuit',
|
||||
'CircuitGroup',
|
||||
'CircuitGroupAssignment',
|
||||
'CircuitTermination',
|
||||
'CircuitType',
|
||||
)
|
||||
@ -151,6 +153,75 @@ class Circuit(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
|
||||
raise ValidationError({'provider_account': "The assigned account must belong to the assigned provider."})
|
||||
|
||||
|
||||
class CircuitGroup(OrganizationalModel):
|
||||
"""
|
||||
An administrative grouping of Circuits.
|
||||
"""
|
||||
tenant = models.ForeignKey(
|
||||
to='tenancy.Tenant',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='circuit_groups',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ('name',)
|
||||
verbose_name = _('circuit group')
|
||||
verbose_name_plural = _('circuit groups')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:circuitgroup', args=[self.pk])
|
||||
|
||||
|
||||
class CircuitGroupAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
|
||||
"""
|
||||
Assignment of a Circuit to a CircuitGroup with an optional priority.
|
||||
"""
|
||||
circuit = models.ForeignKey(
|
||||
Circuit,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='assignments'
|
||||
)
|
||||
group = models.ForeignKey(
|
||||
CircuitGroup,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='assignments'
|
||||
)
|
||||
priority = models.CharField(
|
||||
verbose_name=_('priority'),
|
||||
max_length=50,
|
||||
choices=CircuitPriorityChoices,
|
||||
blank=True
|
||||
)
|
||||
prerequisite_models = (
|
||||
'circuits.Circuit',
|
||||
'circuits.CircuitGroup',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ('circuit', 'priority', 'pk')
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('circuit', 'group'),
|
||||
name='%(app_label)s_%(class)s_unique_circuit_group'
|
||||
),
|
||||
)
|
||||
verbose_name = _('Circuit group assignment')
|
||||
verbose_name_plural = _('Circuit group assignments')
|
||||
|
||||
def __str__(self):
|
||||
if self.priority:
|
||||
return f"{self.group} ({self.get_priority_display()})"
|
||||
return str(self.group)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:circuitgroupassignment', args=[self.pk])
|
||||
|
||||
|
||||
class CircuitTermination(
|
||||
CustomFieldsMixin,
|
||||
CustomLinksMixin,
|
||||
|
@ -13,6 +13,17 @@ class CircuitIndex(SearchIndex):
|
||||
display_attrs = ('provider', 'provider_account', 'type', 'status', 'tenant', 'description')
|
||||
|
||||
|
||||
@register_search
|
||||
class CircuitGroupIndex(SearchIndex):
|
||||
model = models.CircuitGroup
|
||||
fields = (
|
||||
('name', 100),
|
||||
('slug', 110),
|
||||
('description', 500),
|
||||
)
|
||||
display_attrs = ('description',)
|
||||
|
||||
|
||||
@register_search
|
||||
class CircuitTerminationIndex(SearchIndex):
|
||||
model = models.CircuitTermination
|
||||
|
@ -9,6 +9,8 @@ from netbox.tables import NetBoxTable, columns
|
||||
from .columns import CommitRateColumn
|
||||
|
||||
__all__ = (
|
||||
'CircuitGroupAssignmentTable',
|
||||
'CircuitGroupTable',
|
||||
'CircuitTable',
|
||||
'CircuitTerminationTable',
|
||||
'CircuitTypeTable',
|
||||
@ -119,3 +121,50 @@ class CircuitTerminationTable(NetBoxTable):
|
||||
'xconnect_id', 'pp_info', 'description', 'created', 'last_updated', 'actions',
|
||||
)
|
||||
default_columns = ('pk', 'id', 'circuit', 'provider', 'term_side', 'description')
|
||||
|
||||
|
||||
class CircuitGroupTable(NetBoxTable):
|
||||
name = tables.Column(
|
||||
verbose_name=_('Name'),
|
||||
linkify=True
|
||||
)
|
||||
circuit_group_assignment_count = columns.LinkedCountColumn(
|
||||
viewname='circuits:circuitgroupassignment_list',
|
||||
url_params={'group_id': 'pk'},
|
||||
verbose_name=_('Circuits')
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='circuits:circuitgroup_list'
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = CircuitGroup
|
||||
fields = (
|
||||
'pk', 'name', 'description', 'circuit_group_assignment_count', 'tags',
|
||||
'created', 'last_updated', 'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'description', 'circuit_group_assignment_count')
|
||||
|
||||
|
||||
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'),
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='circuits:circuitgroupassignment_list'
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = CircuitGroupAssignment
|
||||
fields = (
|
||||
'pk', 'id', 'group', 'circuit', 'priority', 'created', 'last_updated', 'actions', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'group', 'circuit', 'priority')
|
||||
|
@ -206,6 +206,38 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
|
||||
}
|
||||
|
||||
|
||||
class CircuitGroupTest(APIViewTestCases.APIViewTestCase):
|
||||
model = CircuitGroup
|
||||
brief_fields = ['display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
circuit_groups = (
|
||||
CircuitGroup(name="Circuit Group 1", slug='circuit-group-1'),
|
||||
CircuitGroup(name="Circuit Group 2", slug='circuit-group-2'),
|
||||
CircuitGroup(name="Circuit Group 3", slug='circuit-group-3'),
|
||||
)
|
||||
CircuitGroup.objects.bulk_create(circuit_groups)
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
'name': 'Circuit Group 4',
|
||||
'slug': 'circuit-group-4',
|
||||
},
|
||||
{
|
||||
'name': 'Circuit Group 5',
|
||||
'slug': 'circuit-group-5',
|
||||
},
|
||||
{
|
||||
'name': 'Circuit Group 6',
|
||||
'slug': 'circuit-group-6',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class ProviderAccountTest(APIViewTestCases.APIViewTestCase):
|
||||
model = ProviderAccount
|
||||
brief_fields = ['account', 'description', 'display', 'id', 'name', 'url']
|
||||
@ -249,6 +281,77 @@ class ProviderAccountTest(APIViewTestCases.APIViewTestCase):
|
||||
}
|
||||
|
||||
|
||||
class CircuitGroupAssignmentTest(APIViewTestCases.APIViewTestCase):
|
||||
model = CircuitGroupAssignment
|
||||
brief_fields = ['circuit', 'display', 'group', 'id', 'priority', 'url']
|
||||
bulk_update_data = {
|
||||
'priority': CircuitPriorityChoices.PRIORITY_INACTIVE,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
circuit_groups = (
|
||||
CircuitGroup(name='Circuit Group 1', slug='circuit-group-1'),
|
||||
CircuitGroup(name='Circuit Group 2', slug='circuit-group-2'),
|
||||
CircuitGroup(name='Circuit Group 3', slug='circuit-group-3'),
|
||||
CircuitGroup(name='Circuit Group 4', slug='circuit-group-4'),
|
||||
CircuitGroup(name='Circuit Group 5', slug='circuit-group-5'),
|
||||
CircuitGroup(name='Circuit Group 6', slug='circuit-group-6'),
|
||||
)
|
||||
CircuitGroup.objects.bulk_create(circuit_groups)
|
||||
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
|
||||
|
||||
circuits = (
|
||||
Circuit(cid='Circuit 1', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 2', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 3', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 4', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 5', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 6', provider=provider, type=circuittype),
|
||||
)
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
assignments = (
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[0],
|
||||
circuit=circuits[0],
|
||||
priority=CircuitPriorityChoices.PRIORITY_PRIMARY
|
||||
),
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[1],
|
||||
circuit=circuits[1],
|
||||
priority=CircuitPriorityChoices.PRIORITY_SECONDARY
|
||||
),
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[2],
|
||||
circuit=circuits[2],
|
||||
priority=CircuitPriorityChoices.PRIORITY_TERTIARY
|
||||
),
|
||||
)
|
||||
CircuitGroupAssignment.objects.bulk_create(assignments)
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
'group': circuit_groups[3].pk,
|
||||
'circuit': circuits[3].pk,
|
||||
'priority': CircuitPriorityChoices.PRIORITY_PRIMARY,
|
||||
},
|
||||
{
|
||||
'group': circuit_groups[4].pk,
|
||||
'circuit': circuits[4].pk,
|
||||
'priority': CircuitPriorityChoices.PRIORITY_SECONDARY,
|
||||
},
|
||||
{
|
||||
'group': circuit_groups[5].pk,
|
||||
'circuit': circuits[5].pk,
|
||||
'priority': CircuitPriorityChoices.PRIORITY_TERTIARY,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class ProviderNetworkTest(APIViewTestCases.APIViewTestCase):
|
||||
model = ProviderNetwork
|
||||
brief_fields = ['description', 'display', 'id', 'name', 'url']
|
||||
|
@ -451,6 +451,122 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7)
|
||||
|
||||
|
||||
class CircuitGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = CircuitGroup.objects.all()
|
||||
filterset = CircuitGroupFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
tenant_groups = (
|
||||
TenantGroup(name='Tenant group 1', slug='tenant-group-1'),
|
||||
TenantGroup(name='Tenant group 2', slug='tenant-group-2'),
|
||||
TenantGroup(name='Tenant group 3', slug='tenant-group-3'),
|
||||
)
|
||||
for tenantgroup in tenant_groups:
|
||||
tenantgroup.save()
|
||||
|
||||
tenants = (
|
||||
Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]),
|
||||
Tenant(name='Tenant 2', slug='tenant-2', group=tenant_groups[1]),
|
||||
Tenant(name='Tenant 3', slug='tenant-3', group=tenant_groups[2]),
|
||||
)
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
CircuitGroup.objects.bulk_create((
|
||||
CircuitGroup(name='Circuit Group 1', slug='circuit-group-1', description='foobar1', tenant=tenants[0]),
|
||||
CircuitGroup(name='Circuit Group 2', slug='circuit-group-2', description='foobar2', tenant=tenants[1]),
|
||||
CircuitGroup(name='Circuit Group 3', slug='circuit-group-3', tenant=tenants[1]),
|
||||
))
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Circuit Group 1']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_slug(self):
|
||||
params = {'slug': ['circuit-group-1']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_tenant(self):
|
||||
tenants = Tenant.objects.all()[:2]
|
||||
params = {'tenant_id': [tenants[0].pk, tenants[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
params = {'tenant': [tenants[0].slug, tenants[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_tenant_group(self):
|
||||
tenant_groups = TenantGroup.objects.all()[:2]
|
||||
params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
|
||||
class CircuitGroupAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = CircuitGroupAssignment.objects.all()
|
||||
filterset = CircuitGroupAssignmentFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
circuit_groups = (
|
||||
CircuitGroup(name='Circuit Group 1', slug='circuit-group-1'),
|
||||
CircuitGroup(name='Circuit Group 2', slug='circuit-group-2'),
|
||||
CircuitGroup(name='Circuit Group 3', slug='circuit-group-3'),
|
||||
CircuitGroup(name='Circuit Group 4', slug='circuit-group-4'),
|
||||
)
|
||||
CircuitGroup.objects.bulk_create(circuit_groups)
|
||||
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
|
||||
|
||||
circuits = (
|
||||
Circuit(cid='Circuit 1', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 2', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 3', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 4', provider=provider, type=circuittype),
|
||||
)
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
assignments = (
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[0],
|
||||
circuit=circuits[0],
|
||||
priority=CircuitPriorityChoices.PRIORITY_PRIMARY
|
||||
),
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[1],
|
||||
circuit=circuits[1],
|
||||
priority=CircuitPriorityChoices.PRIORITY_SECONDARY
|
||||
),
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[2],
|
||||
circuit=circuits[2],
|
||||
priority=CircuitPriorityChoices.PRIORITY_TERTIARY
|
||||
),
|
||||
)
|
||||
CircuitGroupAssignment.objects.bulk_create(assignments)
|
||||
|
||||
def test_group_id(self):
|
||||
groups = CircuitGroup.objects.filter(name__in=['Circuit Group 1', 'Circuit Group 2'])
|
||||
params = {'group_id': [groups[0].pk, groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'group': [groups[0].slug, groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_circuit_id(self):
|
||||
circuits = Circuit.objects.filter(cid__in=['Circuit 1', 'Circuit 2'])
|
||||
params = {'circuit_id': [circuits[0].pk, circuits[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class ProviderNetworkTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
filterset = ProviderNetworkFilterSet
|
||||
|
@ -404,3 +404,109 @@ class CircuitTerminationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
|
||||
response = self.client.get(reverse('circuits:circuittermination_trace', kwargs={'pk': circuittermination.pk}))
|
||||
self.assertHttpStatus(response, 200)
|
||||
|
||||
|
||||
class CircuitGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = CircuitGroup
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
circuit_groups = (
|
||||
CircuitGroup(name='Circuit Group 1', slug='circuit-group-1'),
|
||||
CircuitGroup(name='Circuit Group 2', slug='circuit-group-2'),
|
||||
CircuitGroup(name='Circuit Group 3', slug='circuit-group-3'),
|
||||
)
|
||||
CircuitGroup.objects.bulk_create(circuit_groups)
|
||||
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'name': 'Circuit Group X',
|
||||
'slug': 'circuit-group-x',
|
||||
'description': 'A new Circuit Group',
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"name,slug",
|
||||
"Circuit Group 4,circuit-group-4",
|
||||
"Circuit Group 5,circuit-group-5",
|
||||
"Circuit Group 6,circuit-group-6",
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
"id,name,description",
|
||||
f"{circuit_groups[0].pk},Circuit Group 7,New description7",
|
||||
f"{circuit_groups[1].pk},Circuit Group 8,New description8",
|
||||
f"{circuit_groups[2].pk},Circuit Group 9,New description9",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'description': 'Foo',
|
||||
}
|
||||
|
||||
|
||||
class CircuitGroupAssignmentTestCase(
|
||||
ViewTestCases.CreateObjectViewTestCase,
|
||||
ViewTestCases.EditObjectViewTestCase,
|
||||
ViewTestCases.DeleteObjectViewTestCase,
|
||||
ViewTestCases.ListObjectsViewTestCase,
|
||||
ViewTestCases.BulkEditObjectsViewTestCase,
|
||||
ViewTestCases.BulkDeleteObjectsViewTestCase
|
||||
):
|
||||
model = CircuitGroupAssignment
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
circuit_groups = (
|
||||
CircuitGroup(name='Circuit Group 1', slug='circuit-group-1'),
|
||||
CircuitGroup(name='Circuit Group 2', slug='circuit-group-2'),
|
||||
CircuitGroup(name='Circuit Group 3', slug='circuit-group-3'),
|
||||
CircuitGroup(name='Circuit Group 4', slug='circuit-group-4'),
|
||||
)
|
||||
CircuitGroup.objects.bulk_create(circuit_groups)
|
||||
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
|
||||
|
||||
circuits = (
|
||||
Circuit(cid='Circuit 1', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 2', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 3', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 4', provider=provider, type=circuittype),
|
||||
)
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
assignments = (
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[0],
|
||||
circuit=circuits[0],
|
||||
priority=CircuitPriorityChoices.PRIORITY_PRIMARY
|
||||
),
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[1],
|
||||
circuit=circuits[1],
|
||||
priority=CircuitPriorityChoices.PRIORITY_SECONDARY
|
||||
),
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[2],
|
||||
circuit=circuits[2],
|
||||
priority=CircuitPriorityChoices.PRIORITY_TERTIARY
|
||||
),
|
||||
)
|
||||
CircuitGroupAssignment.objects.bulk_create(assignments)
|
||||
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'group': circuit_groups[3].pk,
|
||||
'circuit': circuits[3].pk,
|
||||
'priority': CircuitPriorityChoices.PRIORITY_INACTIVE,
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'priority': CircuitPriorityChoices.PRIORITY_INACTIVE,
|
||||
}
|
||||
|
@ -55,4 +55,19 @@ urlpatterns = [
|
||||
path('circuit-terminations/delete/', views.CircuitTerminationBulkDeleteView.as_view(), name='circuittermination_bulk_delete'),
|
||||
path('circuit-terminations/<int:pk>/', include(get_model_urls('circuits', 'circuittermination'))),
|
||||
|
||||
# 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/<int:pk>/', 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/<int:pk>/', include(get_model_urls('circuits', 'circuitgroupassignment'))),
|
||||
]
|
||||
|
@ -440,3 +440,100 @@ class CircuitTerminationBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
# Trace view
|
||||
register_model_view(CircuitTermination, 'trace', kwargs={'model': CircuitTermination})(PathTraceView)
|
||||
|
||||
|
||||
#
|
||||
# Circuit Groups
|
||||
#
|
||||
|
||||
class CircuitGroupListView(generic.ObjectListView):
|
||||
queryset = CircuitGroup.objects.annotate(
|
||||
circuit_group_assignment_count=count_related(CircuitGroupAssignment, 'group')
|
||||
)
|
||||
filterset = filtersets.CircuitGroupFilterSet
|
||||
filterset_form = forms.CircuitGroupFilterForm
|
||||
table = tables.CircuitGroupTable
|
||||
|
||||
|
||||
@register_model_view(CircuitGroup)
|
||||
class CircuitGroupView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = CircuitGroup.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
'related_models': self.get_related_models(request, instance),
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(CircuitGroup, 'edit')
|
||||
class CircuitGroupEditView(generic.ObjectEditView):
|
||||
queryset = CircuitGroup.objects.all()
|
||||
form = forms.CircuitGroupForm
|
||||
|
||||
|
||||
@register_model_view(CircuitGroup, 'delete')
|
||||
class CircuitGroupDeleteView(generic.ObjectDeleteView):
|
||||
queryset = CircuitGroup.objects.all()
|
||||
|
||||
|
||||
class CircuitGroupBulkImportView(generic.BulkImportView):
|
||||
queryset = CircuitGroup.objects.all()
|
||||
model_form = forms.CircuitGroupImportForm
|
||||
|
||||
|
||||
class CircuitGroupBulkEditView(generic.BulkEditView):
|
||||
queryset = CircuitGroup.objects.all()
|
||||
filterset = filtersets.CircuitGroupFilterSet
|
||||
table = tables.CircuitGroupTable
|
||||
form = forms.CircuitGroupBulkEditForm
|
||||
|
||||
|
||||
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(CircuitGroupAssignment)
|
||||
class CircuitGroupAssignmentView(generic.ObjectView):
|
||||
queryset = CircuitGroupAssignment.objects.all()
|
||||
|
||||
|
||||
@register_model_view(CircuitGroupAssignment, 'edit')
|
||||
class CircuitGroupAssignmentEditView(generic.ObjectEditView):
|
||||
queryset = CircuitGroupAssignment.objects.all()
|
||||
form = forms.CircuitGroupAssignmentForm
|
||||
|
||||
|
||||
@register_model_view(CircuitGroupAssignment, '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
|
||||
|
@ -1098,6 +1098,8 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
'asnrange',
|
||||
'cable',
|
||||
'circuit',
|
||||
'circuitgroup',
|
||||
'circuitgroupassignment',
|
||||
'circuittermination',
|
||||
'circuittype',
|
||||
'cluster',
|
||||
|
@ -259,6 +259,8 @@ CIRCUITS_MENU = Menu(
|
||||
items=(
|
||||
get_model_item('circuits', 'circuit', _('Circuits')),
|
||||
get_model_item('circuits', 'circuittype', _('Circuit Types')),
|
||||
get_model_item('circuits', 'circuitgroup', _('Circuit Groups')),
|
||||
get_model_item('circuits', 'circuitgroupassignment', _('Group Assignments')),
|
||||
get_model_item('circuits', 'circuittermination', _('Circuit Terminations')),
|
||||
),
|
||||
),
|
||||
|
@ -61,6 +61,19 @@
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
{% trans "Group Assignments" %}
|
||||
{% if perms.circuits.add_circuitgroupassignment %}
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'circuits:circuitgroupassignment_add' %}?circuit={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Assign Group" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h5>
|
||||
{% htmx_table 'circuits:circuitgroupassignment_list' circuit_id=object.pk %}
|
||||
</div>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
|
56
netbox/templates/circuits/circuitgroup.html
Normal file
56
netbox/templates/circuits/circuitgroup.html
Normal file
@ -0,0 +1,56 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load static %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
<li class="breadcrumb-item"><a href="{% url 'circuits:circuitgroup_list' %}?circuitgroup_id={{ object.id }}">{{ object.name }}</a></li>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_controls %}
|
||||
{% if perms.circuit.add_circuitgroupassignment %}
|
||||
<a href="{% url 'circuits:circuitgroupassignment_add' %}?group={{ object.pk }}" class="btn btn-primary">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Assign Circuit" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock extra_controls %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">{% trans "Circuit Group" %}</h5>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tenant" %}</th>
|
||||
<td>
|
||||
{% if object.tenant.group %}
|
||||
{{ object.tenant.group|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.tenant|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
48
netbox/templates/circuits/circuitgroupassignment.html
Normal file
48
netbox/templates/circuits/circuitgroupassignment.html
Normal file
@ -0,0 +1,48 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load static %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'circuits:circuitgroupassignment_list' %}?group_id={{ object.group_id }}">{{ object.group }}</a>
|
||||
</li>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">{% trans "Circuit Group Assignment" %}</h5>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Group" %}</th>
|
||||
<td>{{ object.group }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Circuit" %}</th>
|
||||
<td>{{ object.circuit }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Priority" %}</th>
|
||||
<td>{{ object.priority }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
Loading…
Reference in New Issue
Block a user