From b78d4428e161b9da33bda48184a5169fc424e7a2 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 3 Jan 2025 16:32:30 -0500 Subject: [PATCH] Closes #18153: Introduce virtual circuit types --- docs/models/circuits/virtualcircuit.md | 4 + docs/models/circuits/virtualcircuittype.md | 13 +++ netbox/circuits/api/serializers_/circuits.py | 22 +++- netbox/circuits/api/urls.py | 1 + netbox/circuits/api/views.py | 10 ++ netbox/circuits/filtersets.py | 18 +++ netbox/circuits/forms/bulk_edit.py | 24 ++++ netbox/circuits/forms/bulk_import.py | 18 ++- netbox/circuits/forms/filtersets.py | 22 +++- netbox/circuits/forms/model_forms.py | 24 +++- netbox/circuits/graphql/filters.py | 7 ++ netbox/circuits/graphql/schema.py | 3 + netbox/circuits/graphql/types.py | 13 +++ .../migrations/0050_virtual_circuits.py | 32 ++++++ netbox/circuits/models/base.py | 23 ++++ netbox/circuits/models/circuits.py | 9 +- netbox/circuits/models/virtual_circuits.py | 19 +++ netbox/circuits/search.py | 11 ++ netbox/circuits/tables/circuits.py | 6 +- netbox/circuits/tables/virtual_circuits.py | 37 +++++- netbox/circuits/tests/test_api.py | 58 +++++++++- netbox/circuits/tests/test_filtersets.py | 81 +++++++++++-- netbox/circuits/tests/test_views.py | 108 +++++++++++++++--- netbox/circuits/urls.py | 3 + netbox/circuits/views.py | 61 ++++++++++ netbox/netbox/navigation/menu.py | 1 + netbox/templates/circuits/virtualcircuit.html | 4 + .../circuits/virtualcircuittype.html | 55 +++++++++ 28 files changed, 639 insertions(+), 48 deletions(-) create mode 100644 docs/models/circuits/virtualcircuittype.md create mode 100644 netbox/circuits/models/base.py create mode 100644 netbox/templates/circuits/virtualcircuittype.html diff --git a/docs/models/circuits/virtualcircuit.md b/docs/models/circuits/virtualcircuit.md index c81c654c88d..17328b87a2a 100644 --- a/docs/models/circuits/virtualcircuit.md +++ b/docs/models/circuits/virtualcircuit.md @@ -18,6 +18,10 @@ The [provider account](./provideraccount.md) with which the virtual circuit is a The unique identifier assigned to the virtual circuit by its [provider](./provider.md). +### Type + +The assigned [virtual circuit type](./virtualcircuittype.md). + ### Status The operational status of the virtual circuit. By default, the following statuses are available: diff --git a/docs/models/circuits/virtualcircuittype.md b/docs/models/circuits/virtualcircuittype.md new file mode 100644 index 00000000000..69cb0c027f8 --- /dev/null +++ b/docs/models/circuits/virtualcircuittype.md @@ -0,0 +1,13 @@ +# Virtual Circuit Types + +Like physical [circuits](./circuit.md), [virtual circuits](./virtualcircuit.md) are classified by functional type. These types are completely customizable, and can help categorize circuits by function or technology. + +## Fields + +### Name + +A unique human-friendly name. + +### Slug + +A unique URL-friendly identifier. (This value can be used for filtering.) diff --git a/netbox/circuits/api/serializers_/circuits.py b/netbox/circuits/api/serializers_/circuits.py index 4f3dc5f3558..70b57a6885a 100644 --- a/netbox/circuits/api/serializers_/circuits.py +++ b/netbox/circuits/api/serializers_/circuits.py @@ -6,7 +6,7 @@ from circuits.constants import CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS, CIRCUIT_TERMINATION_TERMINATION_TYPES from circuits.models import ( Circuit, CircuitGroup, CircuitGroupAssignment, CircuitTermination, CircuitType, VirtualCircuit, - VirtualCircuitTermination, + VirtualCircuitTermination, VirtualCircuitType, ) from dcim.api.serializers_.device_components import InterfaceSerializer from dcim.api.serializers_.cables import CabledObjectSerializer @@ -25,6 +25,7 @@ 'CircuitTypeSerializer', 'VirtualCircuitSerializer', 'VirtualCircuitTerminationSerializer', + 'VirtualCircuitTypeSerializer', ) @@ -175,17 +176,32 @@ def get_member(self, obj): return serializer(obj.member, nested=True, context=context).data +class VirtualCircuitTypeSerializer(NetBoxModelSerializer): + + # Related object counts + virtual_circuit_count = RelatedObjectCountField('virtual_circuits') + + class Meta: + model = VirtualCircuitType + fields = [ + 'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', + 'created', 'last_updated', 'virtual_circuit_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'virtual_circuit_count') + + class VirtualCircuitSerializer(NetBoxModelSerializer): provider_network = ProviderNetworkSerializer(nested=True) provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True, default=None) + type = VirtualCircuitTypeSerializer(nested=True) status = ChoiceField(choices=CircuitStatusChoices, required=False) tenant = TenantSerializer(nested=True, required=False, allow_null=True) class Meta: model = VirtualCircuit fields = [ - 'id', 'url', 'display_url', 'display', 'cid', 'provider_network', 'provider_account', 'status', 'tenant', - 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + 'id', 'url', 'display_url', 'display', 'cid', 'provider_network', 'provider_account', 'type', 'status', + 'tenant', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'provider_network', 'cid', 'description') diff --git a/netbox/circuits/api/urls.py b/netbox/circuits/api/urls.py index 6f257f694d1..3be620bd27d 100644 --- a/netbox/circuits/api/urls.py +++ b/netbox/circuits/api/urls.py @@ -19,6 +19,7 @@ # Virtual circuits router.register('virtual-circuits', views.VirtualCircuitViewSet) +router.register('virtual-circuit-types', views.VirtualCircuitTypeViewSet) router.register('virtual-circuit-terminations', views.VirtualCircuitTerminationViewSet) app_name = 'circuits-api' diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index 3b49075bea9..05540d9ad35 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -95,6 +95,16 @@ class ProviderNetworkViewSet(NetBoxModelViewSet): filterset_class = filtersets.ProviderNetworkFilterSet +# +# Virtual circuit types +# + +class VirtualCircuitTypeViewSet(NetBoxModelViewSet): + queryset = VirtualCircuitType.objects.all() + serializer_class = serializers.VirtualCircuitTypeSerializer + filterset_class = filtersets.VirtualCircuitTypeFilterSet + + # # Virtual circuits # diff --git a/netbox/circuits/filtersets.py b/netbox/circuits/filtersets.py index 956e91d76f7..964f69f83d5 100644 --- a/netbox/circuits/filtersets.py +++ b/netbox/circuits/filtersets.py @@ -25,6 +25,7 @@ 'ProviderFilterSet', 'VirtualCircuitFilterSet', 'VirtualCircuitTerminationFilterSet', + 'VirtualCircuitTypeFilterSet', ) @@ -462,6 +463,13 @@ def filter_provider(self, queryset, name, value): ) +class VirtualCircuitTypeFilterSet(OrganizationalModelFilterSet): + + class Meta: + model = VirtualCircuitType + fields = ('id', 'name', 'slug', 'color', 'description') + + class VirtualCircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet): provider_id = django_filters.ModelMultipleChoiceFilter( field_name='provider_network__provider', @@ -489,6 +497,16 @@ class VirtualCircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet): queryset=ProviderNetwork.objects.all(), label=_('Provider network (ID)'), ) + type_id = django_filters.ModelMultipleChoiceFilter( + queryset=VirtualCircuitType.objects.all(), + label=_('Virtual circuit type (ID)'), + ) + type = django_filters.ModelMultipleChoiceFilter( + field_name='type__slug', + queryset=VirtualCircuitType.objects.all(), + to_field_name='slug', + label=_('Virtual circuit type (slug)'), + ) status = django_filters.MultipleChoiceFilter( choices=CircuitStatusChoices, null_value=None diff --git a/netbox/circuits/forms/bulk_edit.py b/netbox/circuits/forms/bulk_edit.py index b8e6094f957..8d6e8dec19f 100644 --- a/netbox/circuits/forms/bulk_edit.py +++ b/netbox/circuits/forms/bulk_edit.py @@ -32,6 +32,7 @@ 'ProviderNetworkBulkEditForm', 'VirtualCircuitBulkEditForm', 'VirtualCircuitTerminationBulkEditForm', + 'VirtualCircuitTypeBulkEditForm', ) @@ -297,6 +298,24 @@ class CircuitGroupAssignmentBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('priority',) +class VirtualCircuitTypeBulkEditForm(NetBoxModelBulkEditForm): + color = ColorField( + label=_('Color'), + required=False + ) + description = forms.CharField( + label=_('Description'), + max_length=200, + required=False + ) + + model = VirtualCircuitType + fieldsets = ( + FieldSet('color', 'description'), + ) + nullable_fields = ('color', 'description') + + class VirtualCircuitBulkEditForm(NetBoxModelBulkEditForm): provider_network = DynamicModelChoiceField( label=_('Provider network'), @@ -308,6 +327,11 @@ class VirtualCircuitBulkEditForm(NetBoxModelBulkEditForm): queryset=ProviderAccount.objects.all(), required=False ) + type = DynamicModelChoiceField( + label=_('Type'), + queryset=VirtualCircuitType.objects.all(), + required=False + ) status = forms.ChoiceField( label=_('Status'), choices=add_blank_choice(CircuitStatusChoices), diff --git a/netbox/circuits/forms/bulk_import.py b/netbox/circuits/forms/bulk_import.py index 428c636b38b..43700d16b41 100644 --- a/netbox/circuits/forms/bulk_import.py +++ b/netbox/circuits/forms/bulk_import.py @@ -24,6 +24,7 @@ 'VirtualCircuitImportForm', 'VirtualCircuitTerminationImportForm', 'VirtualCircuitTerminationImportRelatedForm', + 'VirtualCircuitTypeImportForm', ) @@ -194,6 +195,14 @@ class Meta: fields = ('member_type', 'member_id', 'group', 'priority') +class VirtualCircuitTypeImportForm(NetBoxModelImportForm): + slug = SlugField() + + class Meta: + model = VirtualCircuitType + fields = ('name', 'slug', 'color', 'description', 'tags') + + class VirtualCircuitImportForm(NetBoxModelImportForm): provider_network = CSVModelChoiceField( label=_('Provider network'), @@ -208,6 +217,12 @@ class VirtualCircuitImportForm(NetBoxModelImportForm): help_text=_('Assigned provider account (if any)'), required=False ) + type = CSVModelChoiceField( + label=_('Type'), + queryset=VirtualCircuitType.objects.all(), + to_field_name='name', + help_text=_('Type of virtual circuit') + ) status = CSVChoiceField( label=_('Status'), choices=CircuitStatusChoices, @@ -224,7 +239,8 @@ class VirtualCircuitImportForm(NetBoxModelImportForm): class Meta: model = VirtualCircuit fields = [ - 'cid', 'provider_network', 'provider_account', 'status', 'tenant', 'description', 'comments', 'tags', + 'cid', 'provider_network', 'provider_account', 'type', 'status', 'tenant', 'description', 'comments', + 'tags', ] diff --git a/netbox/circuits/forms/filtersets.py b/netbox/circuits/forms/filtersets.py index 1359b7a6a59..aefc626557e 100644 --- a/netbox/circuits/forms/filtersets.py +++ b/netbox/circuits/forms/filtersets.py @@ -27,6 +27,7 @@ 'ProviderNetworkFilterForm', 'VirtualCircuitFilterForm', 'VirtualCircuitTerminationFilterForm', + 'VirtualCircuitTypeFilterForm', ) @@ -302,12 +303,26 @@ class CircuitGroupAssignmentFilterForm(NetBoxModelFilterSetForm): tag = TagFilterField(model) +class VirtualCircuitTypeFilterForm(NetBoxModelFilterSetForm): + model = VirtualCircuitType + fieldsets = ( + FieldSet('q', 'filter_id', 'tag'), + FieldSet('color', name=_('Attributes')), + ) + tag = TagFilterField(model) + + color = ColorField( + label=_('Color'), + required=False + ) + + class VirtualCircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm): model = VirtualCircuit fieldsets = ( FieldSet('q', 'filter_id', 'tag'), FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')), - FieldSet('status', name=_('Attributes')), + FieldSet('type', 'status', name=_('Attributes')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), ) selector_fields = ('filter_id', 'q', 'provider_id', 'provider_network_id') @@ -332,6 +347,11 @@ class VirtualCircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBox }, label=_('Provider network') ) + type_id = DynamicModelMultipleChoiceField( + queryset=VirtualCircuitType.objects.all(), + required=False, + label=_('Type') + ) status = forms.MultipleChoiceField( label=_('Status'), choices=CircuitStatusChoices, diff --git a/netbox/circuits/forms/model_forms.py b/netbox/circuits/forms/model_forms.py index 94ab4db78f3..6f8ab783d92 100644 --- a/netbox/circuits/forms/model_forms.py +++ b/netbox/circuits/forms/model_forms.py @@ -31,6 +31,7 @@ 'ProviderNetworkForm', 'VirtualCircuitForm', 'VirtualCircuitTerminationForm', + 'VirtualCircuitTypeForm', ) @@ -305,6 +306,20 @@ def clean(self): self.instance.member = self.cleaned_data.get('member') +class VirtualCircuitTypeForm(NetBoxModelForm): + slug = SlugField() + + fieldsets = ( + FieldSet('name', 'slug', 'color', 'description', 'tags'), + ) + + class Meta: + model = VirtualCircuitType + fields = [ + 'name', 'slug', 'color', 'description', 'tags', + ] + + class VirtualCircuitForm(TenancyForm, NetBoxModelForm): provider_network = DynamicModelChoiceField( label=_('Provider network'), @@ -316,11 +331,16 @@ class VirtualCircuitForm(TenancyForm, NetBoxModelForm): queryset=ProviderAccount.objects.all(), required=False ) + type = DynamicModelChoiceField( + queryset=VirtualCircuitType.objects.all(), + quick_add=True + ) comments = CommentField() fieldsets = ( FieldSet( - 'provider_network', 'provider_account', 'cid', 'status', 'description', 'tags', name=_('Virtual circuit'), + 'provider_network', 'provider_account', 'cid', 'type', 'status', 'description', 'tags', + name=_('Virtual circuit'), ), FieldSet('tenant_group', 'tenant', name=_('Tenancy')), ) @@ -328,7 +348,7 @@ class VirtualCircuitForm(TenancyForm, NetBoxModelForm): class Meta: model = VirtualCircuit fields = [ - 'cid', 'provider_network', 'provider_account', 'status', 'description', 'tenant_group', 'tenant', + 'cid', 'provider_network', 'provider_account', 'type', 'status', 'description', 'tenant_group', 'tenant', 'comments', 'tags', ] diff --git a/netbox/circuits/graphql/filters.py b/netbox/circuits/graphql/filters.py index 36ddc25b255..7d066f42860 100644 --- a/netbox/circuits/graphql/filters.py +++ b/netbox/circuits/graphql/filters.py @@ -14,6 +14,7 @@ 'ProviderNetworkFilter', 'VirtualCircuitFilter', 'VirtualCircuitTerminationFilter', + 'VirtualCircuitTypeFilter', ) @@ -65,6 +66,12 @@ class ProviderNetworkFilter(BaseFilterMixin): pass +@strawberry_django.filter(models.VirtualCircuitType, lookups=True) +@autotype_decorator(filtersets.VirtualCircuitTypeFilterSet) +class VirtualCircuitTypeFilter(BaseFilterMixin): + pass + + @strawberry_django.filter(models.VirtualCircuit, lookups=True) @autotype_decorator(filtersets.VirtualCircuitFilterSet) class VirtualCircuitFilter(BaseFilterMixin): diff --git a/netbox/circuits/graphql/schema.py b/netbox/circuits/graphql/schema.py index 9c683ce450a..63bd7bba635 100644 --- a/netbox/circuits/graphql/schema.py +++ b/netbox/circuits/graphql/schema.py @@ -37,3 +37,6 @@ class CircuitsQuery: virtual_circuit_termination: VirtualCircuitTerminationType = strawberry_django.field() virtual_circuit_termination_list: List[VirtualCircuitTerminationType] = strawberry_django.field() + + virtual_circuit_type: VirtualCircuitTypeType = strawberry_django.field() + virtual_circuit_type_list: List[VirtualCircuitTypeType] = strawberry_django.field() diff --git a/netbox/circuits/graphql/types.py b/netbox/circuits/graphql/types.py index 1cffc8cd40c..81422070677 100644 --- a/netbox/circuits/graphql/types.py +++ b/netbox/circuits/graphql/types.py @@ -21,6 +21,7 @@ 'ProviderNetworkType', 'VirtualCircuitTerminationType', 'VirtualCircuitType', + 'VirtualCircuitTypeType', ) @@ -130,6 +131,17 @@ def member(self) -> Annotated[Union[ return self.member +@strawberry_django.type( + models.VirtualCircuitType, + fields='__all__', + filters=VirtualCircuitTypeFilter +) +class VirtualCircuitTypeType(OrganizationalObjectType): + color: str + + virtual_circuits: List[Annotated["VirtualCircuitType", strawberry.lazy('circuits.graphql.types')]] + + @strawberry_django.type( models.VirtualCircuitTermination, fields='__all__', @@ -154,6 +166,7 @@ class VirtualCircuitTerminationType(CustomFieldsMixin, TagsMixin, ObjectType): class VirtualCircuitType(NetBoxObjectType): provider_network: ProviderNetworkType = strawberry_django.field(select_related=["provider_network"]) provider_account: ProviderAccountType | None + type: VirtualCircuitTypeType tenant: TenantType | None terminations: List[VirtualCircuitTerminationType] diff --git a/netbox/circuits/migrations/0050_virtual_circuits.py b/netbox/circuits/migrations/0050_virtual_circuits.py index eb451b4ecdc..9987b95ac23 100644 --- a/netbox/circuits/migrations/0050_virtual_circuits.py +++ b/netbox/circuits/migrations/0050_virtual_circuits.py @@ -2,6 +2,7 @@ import taggit.managers from django.db import migrations, models +import utilities.fields import utilities.json @@ -14,6 +15,29 @@ class Migration(migrations.Migration): ] operations = [ + migrations.CreateModel( + name='VirtualCircuitType', + 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)), + ('color', utilities.fields.ColorField(blank=True, max_length=6)), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ], + options={ + 'verbose_name': 'virtual circuit type', + 'verbose_name_plural': 'virtual circuit types', + 'ordering': ('name',), + }, + ), migrations.CreateModel( name='VirtualCircuit', fields=[ @@ -47,6 +71,14 @@ class Migration(migrations.Migration): ), ), ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ( + 'type', + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name='virtual_circuits', + to='circuits.virtualcircuittype' + ) + ), ( 'tenant', models.ForeignKey( diff --git a/netbox/circuits/models/base.py b/netbox/circuits/models/base.py new file mode 100644 index 00000000000..5b2a3c1b8de --- /dev/null +++ b/netbox/circuits/models/base.py @@ -0,0 +1,23 @@ +from django.utils.translation import gettext_lazy as _ + +from netbox.models import OrganizationalModel +from utilities.fields import ColorField + +__all__ = ( + 'BaseCircuitType', +) + + +class BaseCircuitType(OrganizationalModel): + """ + Abstract base model to represent a type of physical or virtual circuit. + Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named + "Long Haul," "Metro," or "Out-of-Band". + """ + color = ColorField( + verbose_name=_('color'), + blank=True + ) + + class Meta: + abstract = True diff --git a/netbox/circuits/models/circuits.py b/netbox/circuits/models/circuits.py index 8a8e4bdf92e..f661c047246 100644 --- a/netbox/circuits/models/circuits.py +++ b/netbox/circuits/models/circuits.py @@ -13,7 +13,7 @@ from netbox.models.features import ( ContactsMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, ImageAttachmentsMixin, TagsMixin, ) -from utilities.fields import ColorField +from .base import BaseCircuitType __all__ = ( 'Circuit', @@ -24,16 +24,11 @@ ) -class CircuitType(OrganizationalModel): +class CircuitType(BaseCircuitType): """ Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named "Long Haul," "Metro," or "Out-of-Band". """ - color = ColorField( - verbose_name=_('color'), - blank=True - ) - class Meta: ordering = ('name',) verbose_name = _('circuit type') diff --git a/netbox/circuits/models/virtual_circuits.py b/netbox/circuits/models/virtual_circuits.py index 04255cd0d36..47259eaa02a 100644 --- a/netbox/circuits/models/virtual_circuits.py +++ b/netbox/circuits/models/virtual_circuits.py @@ -9,13 +9,26 @@ from circuits.choices import * from netbox.models import ChangeLoggedModel, PrimaryModel from netbox.models.features import CustomFieldsMixin, CustomLinksMixin, TagsMixin +from .base import BaseCircuitType __all__ = ( 'VirtualCircuit', 'VirtualCircuitTermination', + 'VirtualCircuitType', ) +class VirtualCircuitType(BaseCircuitType): + """ + Like physical circuits, virtual circuits can be organized by their functional role. For example, a user might wish + to categorize virtual circuits by their technological nature or by product name. + """ + class Meta: + ordering = ('name',) + verbose_name = _('virtual circuit type') + verbose_name_plural = _('virtual circuit types') + + class VirtualCircuit(PrimaryModel): """ A virtual connection between two or more endpoints, delivered across one or more physical circuits. @@ -37,6 +50,11 @@ class VirtualCircuit(PrimaryModel): blank=True, null=True ) + type = models.ForeignKey( + to='VirtualCircuitType', + on_delete=models.PROTECT, + related_name='virtual_circuits' + ) status = models.CharField( verbose_name=_('status'), max_length=50, @@ -63,6 +81,7 @@ class VirtualCircuit(PrimaryModel): ) prerequisite_models = ( 'circuits.ProviderNetwork', + 'circuits.VirtualCircuitType', ) class Meta: diff --git a/netbox/circuits/search.py b/netbox/circuits/search.py index 80725c1b811..2ea11b7fd72 100644 --- a/netbox/circuits/search.py +++ b/netbox/circuits/search.py @@ -100,3 +100,14 @@ class VirtualCircuitTerminationIndex(SearchIndex): ('description', 500), ) display_attrs = ('virtual_circuit', 'role', 'description') + + +@register_search +class VirtualCircuitTypeIndex(SearchIndex): + model = models.VirtualCircuitType + fields = ( + ('name', 100), + ('slug', 110), + ('description', 500), + ) + display_attrs = ('description',) diff --git a/netbox/circuits/tables/circuits.py b/netbox/circuits/tables/circuits.py index ed9ecde2eff..9e59ec0197a 100644 --- a/netbox/circuits/tables/circuits.py +++ b/netbox/circuits/tables/circuits.py @@ -45,7 +45,7 @@ class Meta(NetBoxTable.Meta): 'pk', 'id', 'name', 'circuit_count', 'color', 'description', 'slug', 'tags', 'created', 'last_updated', 'actions', ) - default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug') + default_columns = ('pk', 'name', 'circuit_count', 'color', 'description') class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): @@ -61,6 +61,10 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): linkify=True, verbose_name=_('Account') ) + type = tables.Column( + verbose_name=_('Type'), + linkify=True + ) status = columns.ChoiceFieldColumn() termination_a = columns.TemplateColumn( template_code=CIRCUITTERMINATION_LINK, diff --git a/netbox/circuits/tables/virtual_circuits.py b/netbox/circuits/tables/virtual_circuits.py index b7617f29769..67ac03d59aa 100644 --- a/netbox/circuits/tables/virtual_circuits.py +++ b/netbox/circuits/tables/virtual_circuits.py @@ -8,9 +8,34 @@ __all__ = ( 'VirtualCircuitTable', 'VirtualCircuitTerminationTable', + 'VirtualCircuitTypeTable', ) +class VirtualCircuitTypeTable(NetBoxTable): + name = tables.Column( + linkify=True, + verbose_name=_('Name'), + ) + color = columns.ColorColumn() + tags = columns.TagColumn( + url_name='circuits:virtualcircuittype_list' + ) + virtual_circuit_count = columns.LinkedCountColumn( + viewname='circuits:virtualcircuit_list', + url_params={'type_id': 'pk'}, + verbose_name=_('Circuits') + ) + + class Meta(NetBoxTable.Meta): + model = VirtualCircuitType + fields = ( + 'pk', 'id', 'name', 'virtual_circuit_count', 'color', 'description', 'slug', 'tags', 'created', + 'last_updated', 'actions', + ) + default_columns = ('pk', 'name', 'virtual_circuit_count', 'color', 'description') + + class VirtualCircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): cid = tables.Column( linkify=True, @@ -29,6 +54,10 @@ class VirtualCircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable) linkify=True, verbose_name=_('Account') ) + type = tables.Column( + verbose_name=_('Type'), + linkify=True + ) status = columns.ChoiceFieldColumn() termination_count = columns.LinkedCountColumn( viewname='circuits:virtualcircuittermination_list', @@ -45,12 +74,12 @@ class VirtualCircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable) class Meta(NetBoxTable.Meta): model = VirtualCircuit fields = ( - 'pk', 'id', 'cid', 'provider', 'provider_account', 'provider_network', 'status', 'tenant', 'tenant_group', - 'description', 'comments', 'tags', 'created', 'last_updated', + 'pk', 'id', 'cid', 'provider', 'provider_account', 'provider_network', 'type', 'status', 'tenant', + 'tenant_group', 'description', 'comments', 'tags', 'created', 'last_updated', ) default_columns = ( - 'pk', 'cid', 'provider', 'provider_account', 'provider_network', 'status', 'tenant', 'termination_count', - 'description', + 'pk', 'cid', 'provider', 'provider_account', 'provider_network', 'type', 'status', 'tenant', + 'termination_count', 'description', ) diff --git a/netbox/circuits/tests/test_api.py b/netbox/circuits/tests/test_api.py index 291a2596cb9..3b828023619 100644 --- a/netbox/circuits/tests/test_api.py +++ b/netbox/circuits/tests/test_api.py @@ -409,6 +409,38 @@ def setUpTestData(cls): } +class VirtualCircuitTypeTest(APIViewTestCases.APIViewTestCase): + model = VirtualCircuitType + brief_fields = ['description', 'display', 'id', 'name', 'slug', 'url', 'virtual_circuit_count'] + create_data = ( + { + 'name': 'Virtual Circuit Type 4', + 'slug': 'virtual-circuit-type-4', + }, + { + 'name': 'Virtual Circuit Type 5', + 'slug': 'virtual-circuit-type-5', + }, + { + 'name': 'Virtual Circuit Type 6', + 'slug': 'virtual-circuit-type-6', + }, + ) + bulk_update_data = { + 'description': 'New description', + } + + @classmethod + def setUpTestData(cls): + + virtual_circuit_types = ( + VirtualCircuitType(name='Virtual Circuit Type 1', slug='virtual-circuit-type-1'), + VirtualCircuitType(name='Virtual Circuit Type 2', slug='virtual-circuit-type-2'), + VirtualCircuitType(name='Virtual Circuit Type 3', slug='virtual-circuit-type-3'), + ) + VirtualCircuitType.objects.bulk_create(virtual_circuit_types) + + class VirtualCircuitTest(APIViewTestCases.APIViewTestCase): model = VirtualCircuit brief_fields = ['cid', 'description', 'display', 'id', 'provider_network', 'url'] @@ -421,21 +453,28 @@ def setUpTestData(cls): provider = Provider.objects.create(name='Provider 1', slug='provider-1') provider_network = ProviderNetwork.objects.create(provider=provider, name='Provider Network 1') provider_account = ProviderAccount.objects.create(provider=provider, account='Provider Account 1') + virtual_circuit_type = VirtualCircuitType.objects.create( + name='Virtual Circuit Type 1', + slug='virtual-circuit-type-1' + ) virtual_circuits = ( VirtualCircuit( provider_network=provider_network, provider_account=provider_account, + type=virtual_circuit_type, cid='Virtual Circuit 1' ), VirtualCircuit( provider_network=provider_network, provider_account=provider_account, + type=virtual_circuit_type, cid='Virtual Circuit 2' ), VirtualCircuit( provider_network=provider_network, provider_account=provider_account, + type=virtual_circuit_type, cid='Virtual Circuit 3' ), ) @@ -446,18 +485,21 @@ def setUpTestData(cls): 'cid': 'Virtual Circuit 4', 'provider_network': provider_network.pk, 'provider_account': provider_account.pk, + 'type': virtual_circuit_type.pk, 'status': CircuitStatusChoices.STATUS_PLANNED, }, { 'cid': 'Virtual Circuit 5', 'provider_network': provider_network.pk, 'provider_account': provider_account.pk, + 'type': virtual_circuit_type.pk, 'status': CircuitStatusChoices.STATUS_PLANNED, }, { 'cid': 'Virtual Circuit 6', 'provider_network': provider_network.pk, 'provider_account': provider_account.pk, + 'type': virtual_circuit_type.pk, 'status': CircuitStatusChoices.STATUS_PLANNED, }, ] @@ -563,27 +605,35 @@ def setUpTestData(cls): provider = Provider.objects.create(name='Provider 1', slug='provider-1') provider_network = ProviderNetwork.objects.create(provider=provider, name='Provider Network 1') provider_account = ProviderAccount.objects.create(provider=provider, account='Provider Account 1') + virtual_circuit_type = VirtualCircuitType.objects.create( + name='Virtual Circuit Type 1', + slug='virtual-circuit-type-1' + ) virtual_circuits = ( VirtualCircuit( provider_network=provider_network, provider_account=provider_account, - cid='Virtual Circuit 1' + cid='Virtual Circuit 1', + type=virtual_circuit_type ), VirtualCircuit( provider_network=provider_network, provider_account=provider_account, - cid='Virtual Circuit 2' + cid='Virtual Circuit 2', + type=virtual_circuit_type ), VirtualCircuit( provider_network=provider_network, provider_account=provider_account, - cid='Virtual Circuit 3' + cid='Virtual Circuit 3', + type=virtual_circuit_type ), VirtualCircuit( provider_network=provider_network, provider_account=provider_account, - cid='Virtual Circuit 4' + cid='Virtual Circuit 4', + type=virtual_circuit_type ), ) VirtualCircuit.objects.bulk_create(virtual_circuits) diff --git a/netbox/circuits/tests/test_filtersets.py b/netbox/circuits/tests/test_filtersets.py index d9a5c1bc268..b32abd34eff 100644 --- a/netbox/circuits/tests/test_filtersets.py +++ b/netbox/circuits/tests/test_filtersets.py @@ -656,12 +656,12 @@ def setUpTestData(cls): Provider(name='Provider 2', slug='provider-2'), Provider(name='Provider 3', slug='provider-3'), )) - circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1') + circuit_type = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1') circuits = ( - Circuit(cid='Circuit 1', provider=providers[0], type=circuittype), - Circuit(cid='Circuit 2', provider=providers[1], type=circuittype), - Circuit(cid='Circuit 3', provider=providers[2], type=circuittype), + Circuit(cid='Circuit 1', provider=providers[0], type=circuit_type), + Circuit(cid='Circuit 2', provider=providers[1], type=circuit_type), + Circuit(cid='Circuit 3', provider=providers[2], type=circuit_type), ) Circuit.objects.bulk_create(circuits) @@ -672,18 +672,25 @@ def setUpTestData(cls): ) ProviderNetwork.objects.bulk_create(provider_networks) + virtual_circuit_type = VirtualCircuitType.objects.create( + name='Virtual Circuit Type 1', + slug='virtual-circuit-type-1' + ) virtual_circuits = ( VirtualCircuit( provider_network=provider_networks[0], - cid='Virtual Circuit 1' + cid='Virtual Circuit 1', + type=virtual_circuit_type ), VirtualCircuit( provider_network=provider_networks[1], - cid='Virtual Circuit 2' + cid='Virtual Circuit 2', + type=virtual_circuit_type ), VirtualCircuit( provider_network=provider_networks[2], - cid='Virtual Circuit 3' + cid='Virtual Circuit 3', + type=virtual_circuit_type ), ) VirtualCircuit.objects.bulk_create(virtual_circuits) @@ -837,6 +844,36 @@ def test_provider(self): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) +class VirtualCircuitTypeTestCase(TestCase, ChangeLoggedFilterSetTests): + queryset = VirtualCircuitType.objects.all() + filterset = VirtualCircuitTypeFilterSet + + @classmethod + def setUpTestData(cls): + + VirtualCircuitType.objects.bulk_create(( + VirtualCircuitType(name='Virtual Circuit Type 1', slug='virtual-circuit-type-1', description='foobar1'), + VirtualCircuitType(name='Virtual Circuit Type 2', slug='virtual-circuit-type-2', description='foobar2'), + VirtualCircuitType(name='Virtual Circuit Type 3', slug='virtual-circuit-type-3'), + )) + + def test_q(self): + params = {'q': 'foobar1'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_name(self): + params = {'name': ['Virtual Circuit Type 1']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_slug(self): + params = {'slug': ['virtual-circuit-type-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) + + class VirtualCircuitTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = VirtualCircuit.objects.all() filterset = VirtualCircuitFilterSet @@ -880,12 +917,20 @@ def setUpTestData(cls): ) ProviderNetwork.objects.bulk_create(provider_networks) + virtual_circuit_types = ( + VirtualCircuitType(name='Virtual Circuit Type 1', slug='virtual-circuit-type-1'), + VirtualCircuitType(name='Virtual Circuit Type 2', slug='virtual-circuit-type-2'), + VirtualCircuitType(name='Virtual Circuit Type 3', slug='virtual-circuit-type-3'), + ) + VirtualCircuitType.objects.bulk_create(virtual_circuit_types) + virutal_circuits = ( VirtualCircuit( provider_network=provider_networks[0], provider_account=provider_accounts[0], tenant=tenants[0], cid='Virtual Circuit 1', + type=virtual_circuit_types[0], status=CircuitStatusChoices.STATUS_PLANNED, description='virtualcircuit1', ), @@ -894,6 +939,7 @@ def setUpTestData(cls): provider_account=provider_accounts[1], tenant=tenants[1], cid='Virtual Circuit 2', + type=virtual_circuit_types[1], status=CircuitStatusChoices.STATUS_ACTIVE, description='virtualcircuit2', ), @@ -902,6 +948,7 @@ def setUpTestData(cls): provider_account=provider_accounts[2], tenant=tenants[2], cid='Virtual Circuit 3', + type=virtual_circuit_types[2], status=CircuitStatusChoices.STATUS_DEPROVISIONING, description='virtualcircuit3', ), @@ -933,6 +980,13 @@ def test_provider_network(self): params = {'provider_network_id': [provider_networks[0].pk, provider_networks[1].pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_type(self): + virtual_circuit_types = VirtualCircuitType.objects.all()[:2] + params = {'type_id': [virtual_circuit_types[0].pk, virtual_circuit_types[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'type': [virtual_circuit_types[0].slug, virtual_circuit_types[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_status(self): params = {'status': [CircuitStatusChoices.STATUS_ACTIVE, CircuitStatusChoices.STATUS_PLANNED]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -1029,22 +1083,29 @@ def setUpTestData(cls): ProviderAccount(provider=providers[2], account='Provider Account 3'), ) ProviderAccount.objects.bulk_create(provider_accounts) + virtual_circuit_type = VirtualCircuitType.objects.create( + name='Virtual Circuit Type 1', + slug='virtual-circuit-type-1' + ) virtual_circuits = ( VirtualCircuit( provider_network=provider_networks[0], provider_account=provider_accounts[0], - cid='Virtual Circuit 1' + cid='Virtual Circuit 1', + type=virtual_circuit_type ), VirtualCircuit( provider_network=provider_networks[1], provider_account=provider_accounts[1], - cid='Virtual Circuit 2' + cid='Virtual Circuit 2', + type=virtual_circuit_type ), VirtualCircuit( provider_network=provider_networks[2], provider_account=provider_accounts[2], - cid='Virtual Circuit 3' + cid='Virtual Circuit 3', + type=virtual_circuit_type ), ) VirtualCircuit.objects.bulk_create(virtual_circuits) diff --git a/netbox/circuits/tests/test_views.py b/netbox/circuits/tests/test_views.py index 56f874c1ae5..6ced9a95813 100644 --- a/netbox/circuits/tests/test_views.py +++ b/netbox/circuits/tests/test_views.py @@ -543,6 +543,47 @@ def setUpTestData(cls): } +class VirtualCircuitTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase): + model = VirtualCircuitType + + @classmethod + def setUpTestData(cls): + + virtual_circuit_types = ( + VirtualCircuitType(name='Virtual Circuit Type 1', slug='circuit-type-1'), + VirtualCircuitType(name='Virtual Circuit Type 2', slug='circuit-type-2'), + VirtualCircuitType(name='Virtual Circuit Type 3', slug='circuit-type-3'), + ) + VirtualCircuitType.objects.bulk_create(virtual_circuit_types) + + tags = create_tags('Alpha', 'Bravo', 'Charlie') + + cls.form_data = { + 'name': 'Virtual Circuit Type X', + 'slug': 'virtual-circuit-type-x', + 'description': 'A new virtual circuit type', + 'tags': [t.pk for t in tags], + } + + cls.csv_data = ( + "name,slug", + "Virtual Circuit Type 4,circuit-type-4", + "Virtual Circuit Type 5,circuit-type-5", + "Virtual Circuit Type 6,circuit-type-6", + ) + + cls.csv_update_data = ( + "id,name,description", + f"{virtual_circuit_types[0].pk},Virtual Circuit Type 7,New description7", + f"{virtual_circuit_types[1].pk},Virtual Circuit Type 8,New description8", + f"{virtual_circuit_types[2].pk},Virtual Circuit Type 9,New description9", + ) + + cls.bulk_edit_data = { + 'description': 'Foo', + } + + class VirtualCircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase): model = VirtualCircuit @@ -566,22 +607,30 @@ def setUpTestData(cls): ProviderAccount(provider=provider, account='Provider Account 2'), ) ProviderAccount.objects.bulk_create(provider_accounts) + virtual_circuit_types = ( + VirtualCircuitType(name='Virtual Circuit Type 1', slug='virtual-circuit-type-1'), + VirtualCircuitType(name='Virtual Circuit Type 2', slug='virtual-circuit-type-2'), + ) + VirtualCircuitType.objects.bulk_create(virtual_circuit_types) virtual_circuits = ( VirtualCircuit( provider_network=provider_networks[0], provider_account=provider_accounts[0], - cid='Virtual Circuit 1' + cid='Virtual Circuit 1', + type=virtual_circuit_types[0] ), VirtualCircuit( provider_network=provider_networks[0], provider_account=provider_accounts[0], - cid='Virtual Circuit 2' + cid='Virtual Circuit 2', + type=virtual_circuit_types[0] ), VirtualCircuit( provider_network=provider_networks[0], provider_account=provider_accounts[0], - cid='Virtual Circuit 3' + cid='Virtual Circuit 3', + type=virtual_circuit_types[0] ), ) VirtualCircuit.objects.bulk_create(virtual_circuits) @@ -600,6 +649,7 @@ def setUpTestData(cls): 'cid': 'Virtual Circuit X', 'provider_network': provider_networks[1].pk, 'provider_account': provider_accounts[1].pk, + 'type': virtual_circuit_types[1].pk, 'status': CircuitStatusChoices.STATUS_PLANNED, 'description': 'A new virtual circuit', 'comments': 'Some comments', @@ -607,22 +657,41 @@ def setUpTestData(cls): } cls.csv_data = ( - "cid,provider_network,provider_account,status", - f"Virtual Circuit 4,Provider Network 1,Provider Account 1,{CircuitStatusChoices.STATUS_PLANNED}", - f"Virtual Circuit 5,Provider Network 1,Provider Account 1,{CircuitStatusChoices.STATUS_PLANNED}", - f"Virtual Circuit 6,Provider Network 1,Provider Account 1,{CircuitStatusChoices.STATUS_PLANNED}", + "cid,provider_network,provider_account,type,status", + ( + f"Virtual Circuit 4,Provider Network 1,Provider Account 1,{virtual_circuit_types[0].name}," + f"{CircuitStatusChoices.STATUS_PLANNED}" + ), + ( + f"Virtual Circuit 5,Provider Network 1,Provider Account 1,{virtual_circuit_types[0].name}," + f"{CircuitStatusChoices.STATUS_PLANNED}" + ), + ( + f"Virtual Circuit 6,Provider Network 1,Provider Account 1,{virtual_circuit_types[0].name}," + f"{CircuitStatusChoices.STATUS_PLANNED}" + ), ) cls.csv_update_data = ( - "id,cid,description,status", - f"{virtual_circuits[0].pk},Virtual Circuit A,New description,{CircuitStatusChoices.STATUS_DECOMMISSIONED}", - f"{virtual_circuits[1].pk},Virtual Circuit B,New description,{CircuitStatusChoices.STATUS_DECOMMISSIONED}", - f"{virtual_circuits[2].pk},Virtual Circuit C,New description,{CircuitStatusChoices.STATUS_DECOMMISSIONED}", + "id,cid,description,type,status", + ( + f"{virtual_circuits[0].pk},Virtual Circuit A,New description,{virtual_circuit_types[1].name}," + f"{CircuitStatusChoices.STATUS_DECOMMISSIONED}" + ), + ( + f"{virtual_circuits[1].pk},Virtual Circuit B,New description,{virtual_circuit_types[1].name}," + f"{CircuitStatusChoices.STATUS_DECOMMISSIONED}" + ), + ( + f"{virtual_circuits[2].pk},Virtual Circuit C,New description,{virtual_circuit_types[1].name}," + f"{CircuitStatusChoices.STATUS_DECOMMISSIONED}" + ), ) cls.bulk_edit_data = { 'provider_network': provider_networks[1].pk, 'provider_account': provider_accounts[1].pk, + 'type': virtual_circuit_types[1].pk, 'status': CircuitStatusChoices.STATUS_DECOMMISSIONED, 'description': 'New description', 'comments': 'New comments', @@ -636,6 +705,7 @@ def test_bulk_import_objects_with_terminations(self): {{ "cid": "Virtual Circuit 7", "provider_network": "Provider Network 1", + "type": "Virtual Circuit Type 1", "status": "active", "terminations": [ {{ @@ -774,27 +844,35 @@ def setUpTestData(cls): provider = Provider.objects.create(name='Provider 1', slug='provider-1') provider_network = ProviderNetwork.objects.create(provider=provider, name='Provider Network 1') provider_account = ProviderAccount.objects.create(provider=provider, account='Provider Account 1') + virtual_circuit_type = VirtualCircuitType.objects.create( + name='Virtual Circuit Type 1', + slug='virtual-circuit-type-1' + ) virtual_circuits = ( VirtualCircuit( provider_network=provider_network, provider_account=provider_account, - cid='Virtual Circuit 1' + cid='Virtual Circuit 1', + type=virtual_circuit_type ), VirtualCircuit( provider_network=provider_network, provider_account=provider_account, - cid='Virtual Circuit 2' + cid='Virtual Circuit 2', + type=virtual_circuit_type ), VirtualCircuit( provider_network=provider_network, provider_account=provider_account, - cid='Virtual Circuit 3' + cid='Virtual Circuit 3', + type=virtual_circuit_type ), VirtualCircuit( provider_network=provider_network, provider_account=provider_account, - cid='Virtual Circuit 4' + cid='Virtual Circuit 4', + type=virtual_circuit_type ), ) VirtualCircuit.objects.bulk_create(virtual_circuits) diff --git a/netbox/circuits/urls.py b/netbox/circuits/urls.py index 49eaa39108c..90e9e511f30 100644 --- a/netbox/circuits/urls.py +++ b/netbox/circuits/urls.py @@ -42,6 +42,9 @@ path('virtual-circuits/delete/', views.VirtualCircuitBulkDeleteView.as_view(), name='virtualcircuit_bulk_delete'), path('virtual-circuits//', include(get_model_urls('circuits', 'virtualcircuit'))), + path('virtual-circuit-types/', include(get_model_urls('circuits', 'virtualcircuittype', detail=False))), + path('virtual-circuit-types//', include(get_model_urls('circuits', 'virtualcircuittype'))), + # Virtual circuit terminations path( 'virtual-circuit-terminations/', diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 09f79789eb6..3bd81c33a37 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -579,6 +579,67 @@ class CircuitGroupAssignmentBulkDeleteView(generic.BulkDeleteView): table = tables.CircuitGroupAssignmentTable +# +# Virtual circuit Types +# + +@register_model_view(VirtualCircuitType, 'list', path='', detail=False) +class VirtualCircuitTypeListView(generic.ObjectListView): + queryset = VirtualCircuitType.objects.annotate( + virtual_circuit_count=count_related(VirtualCircuit, 'type') + ) + filterset = filtersets.VirtualCircuitTypeFilterSet + filterset_form = forms.VirtualCircuitTypeFilterForm + table = tables.VirtualCircuitTypeTable + + +@register_model_view(VirtualCircuitType) +class VirtualCircuitTypeView(GetRelatedModelsMixin, generic.ObjectView): + queryset = VirtualCircuitType.objects.all() + + def get_extra_context(self, request, instance): + return { + 'related_models': self.get_related_models(request, instance), + } + + +@register_model_view(VirtualCircuitType, 'add', detail=False) +@register_model_view(VirtualCircuitType, 'edit') +class VirtualCircuitTypeEditView(generic.ObjectEditView): + queryset = VirtualCircuitType.objects.all() + form = forms.VirtualCircuitTypeForm + + +@register_model_view(VirtualCircuitType, 'delete') +class VirtualCircuitTypeDeleteView(generic.ObjectDeleteView): + queryset = VirtualCircuitType.objects.all() + + +@register_model_view(VirtualCircuitType, 'bulk_import', detail=False) +class VirtualCircuitTypeBulkImportView(generic.BulkImportView): + queryset = VirtualCircuitType.objects.all() + model_form = forms.VirtualCircuitTypeImportForm + + +@register_model_view(VirtualCircuitType, 'bulk_edit', path='edit', detail=False) +class VirtualCircuitTypeBulkEditView(generic.BulkEditView): + queryset = VirtualCircuitType.objects.annotate( + circuit_count=count_related(Circuit, 'type') + ) + filterset = filtersets.VirtualCircuitTypeFilterSet + table = tables.VirtualCircuitTypeTable + form = forms.VirtualCircuitTypeBulkEditForm + + +@register_model_view(VirtualCircuitType, 'bulk_delete', path='delete', detail=False) +class VirtualCircuitTypeBulkDeleteView(generic.BulkDeleteView): + queryset = VirtualCircuitType.objects.annotate( + circuit_count=count_related(Circuit, 'type') + ) + filterset = filtersets.VirtualCircuitTypeFilterSet + table = tables.VirtualCircuitTypeTable + + # # Virtual circuits # diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py index 559c2860b51..9148caa8e8c 100644 --- a/netbox/netbox/navigation/menu.py +++ b/netbox/netbox/navigation/menu.py @@ -286,6 +286,7 @@ label=_('Virtual Circuits'), items=( get_model_item('circuits', 'virtualcircuit', _('Virtual Circuits')), + get_model_item('circuits', 'virtualcircuittype', _('Virtual Circuit Types')), get_model_item('circuits', 'virtualcircuittermination', _('Virtual Circuit Terminations')), ), ), diff --git a/netbox/templates/circuits/virtualcircuit.html b/netbox/templates/circuits/virtualcircuit.html index 20fad6af2a9..8fac4a04efb 100644 --- a/netbox/templates/circuits/virtualcircuit.html +++ b/netbox/templates/circuits/virtualcircuit.html @@ -35,6 +35,10 @@

{% trans "Virtual circuit" %}

{% trans "Circuit ID" %} {{ object.cid }} + + {% trans "Type" %} + {{ object.type|linkify }} + {% trans "Status" %} {% badge object.get_status_display bg_color=object.get_status_color %} diff --git a/netbox/templates/circuits/virtualcircuittype.html b/netbox/templates/circuits/virtualcircuittype.html new file mode 100644 index 00000000000..594d9ef22f5 --- /dev/null +++ b/netbox/templates/circuits/virtualcircuittype.html @@ -0,0 +1,55 @@ +{% extends 'generic/object.html' %} +{% load helpers %} +{% load plugins %} +{% load render_table from django_tables2 %} +{% load i18n %} + +{% block extra_controls %} + {% if perms.circuits.add_virtualcircuit %} + + {% trans "Add Virtual Circuit" %} + + {% endif %} +{% endblock extra_controls %} + +{% block content %} +
+
+
+

{% trans "Virtual Circuit Type" %}

+ + + + + + + + + + + + + +
{% trans "Name" %}{{ object.name }}
{% trans "Description" %}{{ object.description|placeholder }}
{% trans "Color" %} + {% if object.color %} +   + {% else %} + {{ ''|placeholder }} + {% endif %} +
+
+ {% include 'inc/panels/tags.html' %} + {% plugin_left_page object %} +
+
+ {% include 'inc/panels/related_objects.html' %} + {% include 'inc/panels/custom_fields.html' %} + {% plugin_right_page object %} +
+
+
+
+ {% plugin_full_width_page object %} +
+
+{% endblock %}