1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Add tags to organizational & nested group models

This commit is contained in:
jeremystretch
2021-10-21 10:51:02 -04:00
parent 8c058dcd45
commit cfb3897047
52 changed files with 463 additions and 154 deletions

View File

@ -19,8 +19,8 @@ The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/
| Type | Change Logging | Webhooks | Custom Fields | Export Templates | Tags | Journaling | Nesting | | Type | Change Logging | Webhooks | Custom Fields | Export Templates | Tags | Journaling | Nesting |
| ------------------ | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | | ------------------ | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- |
| Primary | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | | | Primary | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | |
| Organizational | :material-check: | :material-check: | :material-check: | :material-check: | | | | | Organizational | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | | |
| Nested Group | :material-check: | :material-check: | :material-check: | :material-check: | | | :material-check: | | Nested Group | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | | :material-check: |
| Component | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | | | | Component | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | | |
| Component Template | :material-check: | :material-check: | :material-check: | | | | | | Component Template | :material-check: | :material-check: | :material-check: | | | | |

View File

@ -15,6 +15,3 @@ The `tag` filter can be specified multiple times to match only objects which hav
```no-highlight ```no-highlight
GET /api/dcim/devices/?tag=monitored&tag=deprecated GET /api/dcim/devices/?tag=monitored&tag=deprecated
``` ```
!!! note
Tags have changed substantially in NetBox v2.9. They are no longer created on-demand when editing an object, and their representation in the REST API now includes a complete depiction of the tag rather than only its label.

View File

@ -5,9 +5,7 @@ from circuits.models import *
from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
from dcim.api.serializers import CableTerminationSerializer from dcim.api.serializers import CableTerminationSerializer
from netbox.api import ChoiceField from netbox.api import ChoiceField
from netbox.api.serializers import ( from netbox.api.serializers import PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer
OrganizationalModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer
)
from tenancy.api.nested_serializers import NestedTenantSerializer from tenancy.api.nested_serializers import NestedTenantSerializer
from .nested_serializers import * from .nested_serializers import *
@ -48,14 +46,14 @@ class ProviderNetworkSerializer(PrimaryModelSerializer):
# Circuits # Circuits
# #
class CircuitTypeSerializer(OrganizationalModelSerializer): class CircuitTypeSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail') url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
circuit_count = serializers.IntegerField(read_only=True) circuit_count = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = CircuitType model = CircuitType
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'description', 'custom_fields', 'created', 'last_updated', 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
'circuit_count', 'circuit_count',
] ]

View File

@ -34,7 +34,7 @@ class ProviderViewSet(CustomFieldModelViewSet):
# #
class CircuitTypeViewSet(CustomFieldModelViewSet): class CircuitTypeViewSet(CustomFieldModelViewSet):
queryset = CircuitType.objects.annotate( queryset = CircuitType.objects.prefetch_related('tags').annotate(
circuit_count=count_related(Circuit, 'type') circuit_count=count_related(Circuit, 'type')
) )
serializer_class = serializers.CircuitTypeSerializer serializer_class = serializers.CircuitTypeSerializer

View File

@ -79,7 +79,7 @@ class ProviderNetworkBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomField
] ]
class CircuitTypeBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): class CircuitTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=CircuitType.objects.all(), queryset=CircuitType.objects.all(),
widget=forms.MultipleHiddenInput widget=forms.MultipleHiddenInput

View File

@ -75,11 +75,15 @@ class ProviderNetworkForm(BootstrapMixin, CustomFieldModelForm):
class CircuitTypeForm(BootstrapMixin, CustomFieldModelForm): class CircuitTypeForm(BootstrapMixin, CustomFieldModelForm):
slug = SlugField() slug = SlugField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta: class Meta:
model = CircuitType model = CircuitType
fields = [ fields = [
'name', 'slug', 'description', 'name', 'slug', 'description', 'tags',
] ]

View File

@ -0,0 +1,20 @@
# Generated by Django 3.2.8 on 2021-10-21 14:50
from django.db import migrations
import taggit.managers
class Migration(migrations.Migration):
dependencies = [
('extras', '0062_clear_secrets_changelog'),
('circuits', '0002_squashed_0029'),
]
operations = [
migrations.AddField(
model_name='circuittype',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
]

View File

@ -128,7 +128,7 @@ class ProviderNetwork(PrimaryModel):
return reverse('circuits:providernetwork', args=[self.pk]) return reverse('circuits:providernetwork', args=[self.pk])
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class CircuitType(OrganizationalModel): class CircuitType(OrganizationalModel):
""" """
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named

View File

@ -82,6 +82,9 @@ class CircuitTypeTable(BaseTable):
name = tables.Column( name = tables.Column(
linkify=True linkify=True
) )
tags = TagColumn(
url_name='circuits:circuittype_list'
)
circuit_count = tables.Column( circuit_count = tables.Column(
verbose_name='Circuits' verbose_name='Circuits'
) )
@ -89,7 +92,7 @@ class CircuitTypeTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = CircuitType model = CircuitType
fields = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions') fields = ('pk', 'name', 'circuit_count', 'description', 'slug', 'tags', 'actions')
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions') default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions')

View File

@ -64,10 +64,13 @@ class CircuitTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
CircuitType(name='Circuit Type 3', slug='circuit-type-3'), CircuitType(name='Circuit Type 3', slug='circuit-type-3'),
]) ])
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = { cls.form_data = {
'name': 'Circuit Type X', 'name': 'Circuit Type X',
'slug': 'circuit-type-x', 'slug': 'circuit-type-x',
'description': 'A new circuit type', 'description': 'A new circuit type',
'tags': [t.pk for t in tags],
} }
cls.csv_data = ( cls.csv_data = (

View File

@ -11,8 +11,7 @@ from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSer
from ipam.models import VLAN from ipam.models import VLAN
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
from netbox.api.serializers import ( from netbox.api.serializers import (
NestedGroupModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer, NestedGroupModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer,
WritableNestedSerializer,
) )
from tenancy.api.nested_serializers import NestedTenantSerializer from tenancy.api.nested_serializers import NestedTenantSerializer
from users.api.nested_serializers import NestedUserSerializer from users.api.nested_serializers import NestedUserSerializer
@ -87,8 +86,8 @@ class RegionSerializer(NestedGroupModelSerializer):
class Meta: class Meta:
model = Region model = Region
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'custom_fields', 'created', 'last_updated', 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created',
'site_count', '_depth', 'last_updated', 'site_count', '_depth',
] ]
@ -100,8 +99,8 @@ class SiteGroupSerializer(NestedGroupModelSerializer):
class Meta: class Meta:
model = SiteGroup model = SiteGroup
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'custom_fields', 'created', 'last_updated', 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created',
'site_count', '_depth', 'last_updated', 'site_count', '_depth',
] ]
@ -144,20 +143,20 @@ class LocationSerializer(NestedGroupModelSerializer):
class Meta: class Meta:
model = Location model = Location
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'tenant', 'description', 'custom_fields', 'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'tenant', 'description', 'tags', 'custom_fields',
'created', 'last_updated', 'rack_count', 'device_count', '_depth', 'created', 'last_updated', 'rack_count', 'device_count', '_depth',
] ]
class RackRoleSerializer(OrganizationalModelSerializer): class RackRoleSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
rack_count = serializers.IntegerField(read_only=True) rack_count = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = RackRole model = RackRole
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'custom_fields', 'created', 'last_updated', 'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created',
'rack_count', 'last_updated', 'rack_count',
] ]
@ -254,7 +253,7 @@ class RackElevationDetailFilterSerializer(serializers.Serializer):
# Device types # Device types
# #
class ManufacturerSerializer(OrganizationalModelSerializer): class ManufacturerSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
devicetype_count = serializers.IntegerField(read_only=True) devicetype_count = serializers.IntegerField(read_only=True)
inventoryitem_count = serializers.IntegerField(read_only=True) inventoryitem_count = serializers.IntegerField(read_only=True)
@ -263,7 +262,7 @@ class ManufacturerSerializer(OrganizationalModelSerializer):
class Meta: class Meta:
model = Manufacturer model = Manufacturer
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'description', 'custom_fields', 'created', 'last_updated', 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
'devicetype_count', 'inventoryitem_count', 'platform_count', 'devicetype_count', 'inventoryitem_count', 'platform_count',
] ]
@ -411,7 +410,7 @@ class DeviceBayTemplateSerializer(ValidatedModelSerializer):
# Devices # Devices
# #
class DeviceRoleSerializer(OrganizationalModelSerializer): class DeviceRoleSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
device_count = serializers.IntegerField(read_only=True) device_count = serializers.IntegerField(read_only=True)
virtualmachine_count = serializers.IntegerField(read_only=True) virtualmachine_count = serializers.IntegerField(read_only=True)
@ -419,12 +418,12 @@ class DeviceRoleSerializer(OrganizationalModelSerializer):
class Meta: class Meta:
model = DeviceRole model = DeviceRole
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'color', 'vm_role', 'description', 'custom_fields', 'created', 'id', 'url', 'display', 'name', 'slug', 'color', 'vm_role', 'description', 'tags', 'custom_fields',
'last_updated', 'device_count', 'virtualmachine_count', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
] ]
class PlatformSerializer(OrganizationalModelSerializer): class PlatformSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True) manufacturer = NestedManufacturerSerializer(required=False, allow_null=True)
device_count = serializers.IntegerField(read_only=True) device_count = serializers.IntegerField(read_only=True)
@ -434,7 +433,7 @@ class PlatformSerializer(OrganizationalModelSerializer):
model = Platform model = Platform
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'id', 'url', 'display', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description',
'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
] ]

View File

@ -110,7 +110,7 @@ class RegionViewSet(CustomFieldModelViewSet):
'region', 'region',
'site_count', 'site_count',
cumulative=True cumulative=True
) ).prefetch_related('tags')
serializer_class = serializers.RegionSerializer serializer_class = serializers.RegionSerializer
filterset_class = filtersets.RegionFilterSet filterset_class = filtersets.RegionFilterSet
@ -126,7 +126,7 @@ class SiteGroupViewSet(CustomFieldModelViewSet):
'group', 'group',
'site_count', 'site_count',
cumulative=True cumulative=True
) ).prefetch_related('tags')
serializer_class = serializers.SiteGroupSerializer serializer_class = serializers.SiteGroupSerializer
filterset_class = filtersets.SiteGroupFilterSet filterset_class = filtersets.SiteGroupFilterSet
@ -167,7 +167,7 @@ class LocationViewSet(CustomFieldModelViewSet):
'location', 'location',
'rack_count', 'rack_count',
cumulative=True cumulative=True
).prefetch_related('site') ).prefetch_related('site', 'tags')
serializer_class = serializers.LocationSerializer serializer_class = serializers.LocationSerializer
filterset_class = filtersets.LocationFilterSet filterset_class = filtersets.LocationFilterSet
@ -177,7 +177,7 @@ class LocationViewSet(CustomFieldModelViewSet):
# #
class RackRoleViewSet(CustomFieldModelViewSet): class RackRoleViewSet(CustomFieldModelViewSet):
queryset = RackRole.objects.annotate( queryset = RackRole.objects.prefetch_related('tags').annotate(
rack_count=count_related(Rack, 'role') rack_count=count_related(Rack, 'role')
) )
serializer_class = serializers.RackRoleSerializer serializer_class = serializers.RackRoleSerializer
@ -261,7 +261,7 @@ class RackReservationViewSet(ModelViewSet):
# #
class ManufacturerViewSet(CustomFieldModelViewSet): class ManufacturerViewSet(CustomFieldModelViewSet):
queryset = Manufacturer.objects.annotate( queryset = Manufacturer.objects.prefetch_related('tags').annotate(
devicetype_count=count_related(DeviceType, 'manufacturer'), devicetype_count=count_related(DeviceType, 'manufacturer'),
inventoryitem_count=count_related(InventoryItem, 'manufacturer'), inventoryitem_count=count_related(InventoryItem, 'manufacturer'),
platform_count=count_related(Platform, 'manufacturer') platform_count=count_related(Platform, 'manufacturer')
@ -340,7 +340,7 @@ class DeviceBayTemplateViewSet(ModelViewSet):
# #
class DeviceRoleViewSet(CustomFieldModelViewSet): class DeviceRoleViewSet(CustomFieldModelViewSet):
queryset = DeviceRole.objects.annotate( queryset = DeviceRole.objects.prefetch_related('tags').annotate(
device_count=count_related(Device, 'device_role'), device_count=count_related(Device, 'device_role'),
virtualmachine_count=count_related(VirtualMachine, 'role') virtualmachine_count=count_related(VirtualMachine, 'role')
) )
@ -353,7 +353,7 @@ class DeviceRoleViewSet(CustomFieldModelViewSet):
# #
class PlatformViewSet(CustomFieldModelViewSet): class PlatformViewSet(CustomFieldModelViewSet):
queryset = Platform.objects.annotate( queryset = Platform.objects.prefetch_related('tags').annotate(
device_count=count_related(Device, 'platform'), device_count=count_related(Device, 'platform'),
virtualmachine_count=count_related(VirtualMachine, 'platform') virtualmachine_count=count_related(VirtualMachine, 'platform')
) )

View File

@ -51,7 +51,7 @@ __all__ = (
) )
class RegionBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): class RegionBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
widget=forms.MultipleHiddenInput widget=forms.MultipleHiddenInput
@ -69,7 +69,7 @@ class RegionBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
nullable_fields = ['parent', 'description'] nullable_fields = ['parent', 'description']
class SiteGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): class SiteGroupBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=SiteGroup.objects.all(), queryset=SiteGroup.objects.all(),
widget=forms.MultipleHiddenInput widget=forms.MultipleHiddenInput
@ -132,7 +132,7 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEd
] ]
class LocationBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): class LocationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=Location.objects.all(), queryset=Location.objects.all(),
widget=forms.MultipleHiddenInput widget=forms.MultipleHiddenInput
@ -161,7 +161,7 @@ class LocationBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
nullable_fields = ['parent', 'tenant', 'description'] nullable_fields = ['parent', 'tenant', 'description']
class RackRoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): class RackRoleBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=RackRole.objects.all(), queryset=RackRole.objects.all(),
widget=forms.MultipleHiddenInput widget=forms.MultipleHiddenInput
@ -303,7 +303,7 @@ class RackReservationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomField
nullable_fields = [] nullable_fields = []
class ManufacturerBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): class ManufacturerBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
widget=forms.MultipleHiddenInput widget=forms.MultipleHiddenInput
@ -345,7 +345,7 @@ class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModel
nullable_fields = ['airflow'] nullable_fields = ['airflow']
class DeviceRoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): class DeviceRoleBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=DeviceRole.objects.all(), queryset=DeviceRole.objects.all(),
widget=forms.MultipleHiddenInput widget=forms.MultipleHiddenInput
@ -367,7 +367,7 @@ class DeviceRoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
nullable_fields = ['color', 'description'] nullable_fields = ['color', 'description']
class PlatformBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): class PlatformBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=Platform.objects.all(), queryset=Platform.objects.all(),
widget=forms.MultipleHiddenInput widget=forms.MultipleHiddenInput

View File

@ -70,11 +70,15 @@ class RegionForm(BootstrapMixin, CustomFieldModelForm):
required=False required=False
) )
slug = SlugField() slug = SlugField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta: class Meta:
model = Region model = Region
fields = ( fields = (
'parent', 'name', 'slug', 'description', 'parent', 'name', 'slug', 'description', 'tags',
) )
@ -84,11 +88,15 @@ class SiteGroupForm(BootstrapMixin, CustomFieldModelForm):
required=False required=False
) )
slug = SlugField() slug = SlugField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta: class Meta:
model = SiteGroup model = SiteGroup
fields = ( fields = (
'parent', 'name', 'slug', 'description', 'parent', 'name', 'slug', 'description', 'tags',
) )
@ -187,15 +195,19 @@ class LocationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
} }
) )
slug = SlugField() slug = SlugField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta: class Meta:
model = Location model = Location
fields = ( fields = (
'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description', 'tenant_group', 'tenant', 'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description', 'tenant_group', 'tenant', 'tags',
) )
fieldsets = ( fieldsets = (
('Location', ( ('Location', (
'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description', 'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description', 'tags',
)), )),
('Tenancy', ('tenant_group', 'tenant')), ('Tenancy', ('tenant_group', 'tenant')),
) )
@ -203,11 +215,15 @@ class LocationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
class RackRoleForm(BootstrapMixin, CustomFieldModelForm): class RackRoleForm(BootstrapMixin, CustomFieldModelForm):
slug = SlugField() slug = SlugField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta: class Meta:
model = RackRole model = RackRole
fields = [ fields = [
'name', 'slug', 'color', 'description', 'name', 'slug', 'color', 'description', 'tags',
] ]
@ -343,11 +359,15 @@ class RackReservationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
class ManufacturerForm(BootstrapMixin, CustomFieldModelForm): class ManufacturerForm(BootstrapMixin, CustomFieldModelForm):
slug = SlugField() slug = SlugField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta: class Meta:
model = Manufacturer model = Manufacturer
fields = [ fields = [
'name', 'slug', 'description', 'name', 'slug', 'description', 'tags',
] ]
@ -392,11 +412,15 @@ class DeviceTypeForm(BootstrapMixin, CustomFieldModelForm):
class DeviceRoleForm(BootstrapMixin, CustomFieldModelForm): class DeviceRoleForm(BootstrapMixin, CustomFieldModelForm):
slug = SlugField() slug = SlugField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta: class Meta:
model = DeviceRole model = DeviceRole
fields = [ fields = [
'name', 'slug', 'color', 'vm_role', 'description', 'name', 'slug', 'color', 'vm_role', 'description', 'tags',
] ]
@ -408,11 +432,15 @@ class PlatformForm(BootstrapMixin, CustomFieldModelForm):
slug = SlugField( slug = SlugField(
max_length=64 max_length=64
) )
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta: class Meta:
model = Platform model = Platform
fields = [ fields = [
'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'tags',
] ]
widgets = { widgets = {
'napalm_args': SmallTextarea(), 'napalm_args': SmallTextarea(),

View File

@ -0,0 +1,50 @@
# Generated by Django 3.2.8 on 2021-10-21 14:50
from django.db import migrations
import taggit.managers
class Migration(migrations.Migration):
dependencies = [
('extras', '0062_clear_secrets_changelog'),
('dcim', '0137_relax_uniqueness_constraints'),
]
operations = [
migrations.AddField(
model_name='devicerole',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
migrations.AddField(
model_name='location',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
migrations.AddField(
model_name='manufacturer',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
migrations.AddField(
model_name='platform',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
migrations.AddField(
model_name='rackrole',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
migrations.AddField(
model_name='region',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
migrations.AddField(
model_name='sitegroup',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
]

View File

@ -36,7 +36,7 @@ __all__ = (
# Device Types # Device Types
# #
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Manufacturer(OrganizationalModel): class Manufacturer(OrganizationalModel):
""" """
A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell. A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
@ -351,7 +351,7 @@ class DeviceType(PrimaryModel):
# Devices # Devices
# #
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class DeviceRole(OrganizationalModel): class DeviceRole(OrganizationalModel):
""" """
Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a
@ -391,7 +391,7 @@ class DeviceRole(OrganizationalModel):
return reverse('dcim:devicerole', args=[self.pk]) return reverse('dcim:devicerole', args=[self.pk])
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Platform(OrganizationalModel): class Platform(OrganizationalModel):
""" """
Platform refers to the software or firmware running on a Device. For example, "Cisco IOS-XR" or "Juniper Junos". Platform refers to the software or firmware running on a Device. For example, "Cisco IOS-XR" or "Juniper Junos".

View File

@ -35,7 +35,7 @@ __all__ = (
# Racks # Racks
# #
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class RackRole(OrganizationalModel): class RackRole(OrganizationalModel):
""" """
Racks can be organized by functional role, similar to Devices. Racks can be organized by functional role, similar to Devices.

View File

@ -25,7 +25,7 @@ __all__ = (
# Regions # Regions
# #
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Region(NestedGroupModel): class Region(NestedGroupModel):
""" """
A region represents a geographic collection of sites. For example, you might create regions representing countries, A region represents a geographic collection of sites. For example, you might create regions representing countries,
@ -82,7 +82,7 @@ class Region(NestedGroupModel):
# Site groups # Site groups
# #
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class SiteGroup(NestedGroupModel): class SiteGroup(NestedGroupModel):
""" """
A site group is an arbitrary grouping of sites. For example, you might have corporate sites and customer sites; and A site group is an arbitrary grouping of sites. For example, you might have corporate sites and customer sites; and
@ -278,7 +278,7 @@ class Site(PrimaryModel):
# Locations # Locations
# #
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Location(NestedGroupModel): class Location(NestedGroupModel):
""" """
A Location represents a subgroup of Racks and/or Devices within a Site. A Location may represent a building within a A Location represents a subgroup of Racks and/or Devices within a Site. A Location may represent a building within a

View File

@ -84,11 +84,16 @@ class DeviceRoleTable(BaseTable):
) )
color = ColorColumn() color = ColorColumn()
vm_role = BooleanColumn() vm_role = BooleanColumn()
tags = TagColumn(
url_name='dcim:devicerole_list'
)
actions = ButtonsColumn(DeviceRole) actions = ButtonsColumn(DeviceRole)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = DeviceRole model = DeviceRole
fields = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'actions') fields = (
'pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'tags', 'actions',
)
default_columns = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'actions') default_columns = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'actions')
@ -111,13 +116,16 @@ class PlatformTable(BaseTable):
url_params={'platform_id': 'pk'}, url_params={'platform_id': 'pk'},
verbose_name='VMs' verbose_name='VMs'
) )
tags = TagColumn(
url_name='dcim:platform_list'
)
actions = ButtonsColumn(Platform) actions = ButtonsColumn(Platform)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Platform model = Platform
fields = ( fields = (
'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'napalm_args', 'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'napalm_args',
'description', 'actions', 'description', 'tags', 'actions',
) )
default_columns = ( default_columns = (
'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'napalm_driver', 'description', 'actions', 'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'napalm_driver', 'description', 'actions',

View File

@ -41,12 +41,16 @@ class ManufacturerTable(BaseTable):
verbose_name='Platforms' verbose_name='Platforms'
) )
slug = tables.Column() slug = tables.Column()
tags = TagColumn(
url_name='dcim:manufacturer_list'
)
actions = ButtonsColumn(Manufacturer) actions = ButtonsColumn(Manufacturer)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Manufacturer model = Manufacturer
fields = ( fields = (
'pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug', 'actions', 'pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug', 'tags',
'actions',
) )

View File

@ -24,11 +24,14 @@ class RackRoleTable(BaseTable):
name = tables.Column(linkify=True) name = tables.Column(linkify=True)
rack_count = tables.Column(verbose_name='Racks') rack_count = tables.Column(verbose_name='Racks')
color = ColorColumn() color = ColorColumn()
tags = TagColumn(
url_name='dcim:rackrole_list'
)
actions = ButtonsColumn(RackRole) actions = ButtonsColumn(RackRole)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = RackRole model = RackRole
fields = ('pk', 'name', 'rack_count', 'color', 'description', 'slug', 'actions') fields = ('pk', 'name', 'rack_count', 'color', 'description', 'slug', 'tags', 'actions')
default_columns = ('pk', 'name', 'rack_count', 'color', 'description', 'actions') default_columns = ('pk', 'name', 'rack_count', 'color', 'description', 'actions')

View File

@ -29,11 +29,14 @@ class RegionTable(BaseTable):
url_params={'region_id': 'pk'}, url_params={'region_id': 'pk'},
verbose_name='Sites' verbose_name='Sites'
) )
tags = TagColumn(
url_name='dcim:region_list'
)
actions = ButtonsColumn(Region) actions = ButtonsColumn(Region)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Region model = Region
fields = ('pk', 'name', 'slug', 'site_count', 'description', 'actions') fields = ('pk', 'name', 'slug', 'site_count', 'description', 'tags', 'actions')
default_columns = ('pk', 'name', 'site_count', 'description', 'actions') default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
@ -51,11 +54,14 @@ class SiteGroupTable(BaseTable):
url_params={'group_id': 'pk'}, url_params={'group_id': 'pk'},
verbose_name='Sites' verbose_name='Sites'
) )
tags = TagColumn(
url_name='dcim:sitegroup_list'
)
actions = ButtonsColumn(SiteGroup) actions = ButtonsColumn(SiteGroup)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = SiteGroup model = SiteGroup
fields = ('pk', 'name', 'slug', 'site_count', 'description', 'actions') fields = ('pk', 'name', 'slug', 'site_count', 'description', 'tags', 'actions')
default_columns = ('pk', 'name', 'site_count', 'description', 'actions') default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
@ -114,6 +120,9 @@ class LocationTable(BaseTable):
url_params={'location_id': 'pk'}, url_params={'location_id': 'pk'},
verbose_name='Devices' verbose_name='Devices'
) )
tags = TagColumn(
url_name='dcim:location_list'
)
actions = ButtonsColumn( actions = ButtonsColumn(
model=Location, model=Location,
prepend_template=LOCATION_ELEVATIONS prepend_template=LOCATION_ELEVATIONS
@ -121,5 +130,7 @@ class LocationTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Location model = Location
fields = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'actions') fields = (
'pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'tags', 'actions',
)
default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'actions') default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'actions')

View File

@ -31,11 +31,14 @@ class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
for region in regions: for region in regions:
region.save() region.save()
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = { cls.form_data = {
'name': 'Region X', 'name': 'Region X',
'slug': 'region-x', 'slug': 'region-x',
'parent': regions[2].pk, 'parent': regions[2].pk,
'description': 'A new region', 'description': 'A new region',
'tags': [t.pk for t in tags],
} }
cls.csv_data = ( cls.csv_data = (
@ -65,11 +68,14 @@ class SiteGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
for sitegroup in sitegroups: for sitegroup in sitegroups:
sitegroup.save() sitegroup.save()
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = { cls.form_data = {
'name': 'Site Group X', 'name': 'Site Group X',
'slug': 'site-group-x', 'slug': 'site-group-x',
'parent': sitegroups[2].pk, 'parent': sitegroups[2].pk,
'description': 'A new site group', 'description': 'A new site group',
'tags': [t.pk for t in tags],
} }
cls.csv_data = ( cls.csv_data = (
@ -169,12 +175,15 @@ class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
for location in locations: for location in locations:
location.save() location.save()
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = { cls.form_data = {
'name': 'Location X', 'name': 'Location X',
'slug': 'location-x', 'slug': 'location-x',
'site': site.pk, 'site': site.pk,
'tenant': tenant.pk, 'tenant': tenant.pk,
'description': 'A new location', 'description': 'A new location',
'tags': [t.pk for t in tags],
} }
cls.csv_data = ( cls.csv_data = (
@ -201,11 +210,14 @@ class RackRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
RackRole(name='Rack Role 3', slug='rack-role-3'), RackRole(name='Rack Role 3', slug='rack-role-3'),
]) ])
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = { cls.form_data = {
'name': 'Rack Role X', 'name': 'Rack Role X',
'slug': 'rack-role-x', 'slug': 'rack-role-x',
'color': 'c0c0c0', 'color': 'c0c0c0',
'description': 'New role', 'description': 'New role',
'tags': [t.pk for t in tags],
} }
cls.csv_data = ( cls.csv_data = (
@ -368,10 +380,13 @@ class ManufacturerTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
Manufacturer(name='Manufacturer 3', slug='manufacturer-3'), Manufacturer(name='Manufacturer 3', slug='manufacturer-3'),
]) ])
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = { cls.form_data = {
'name': 'Manufacturer X', 'name': 'Manufacturer X',
'slug': 'manufacturer-x', 'slug': 'manufacturer-x',
'description': 'A new manufacturer', 'description': 'A new manufacturer',
'tags': [t.pk for t in tags],
} }
cls.csv_data = ( cls.csv_data = (
@ -1034,12 +1049,15 @@ class DeviceRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
DeviceRole(name='Device Role 3', slug='device-role-3'), DeviceRole(name='Device Role 3', slug='device-role-3'),
]) ])
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = { cls.form_data = {
'name': 'Devie Role X', 'name': 'Devie Role X',
'slug': 'device-role-x', 'slug': 'device-role-x',
'color': 'c0c0c0', 'color': 'c0c0c0',
'vm_role': False, 'vm_role': False,
'description': 'New device role', 'description': 'New device role',
'tags': [t.pk for t in tags],
} }
cls.csv_data = ( cls.csv_data = (
@ -1069,6 +1087,8 @@ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturer), Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturer),
]) ])
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = { cls.form_data = {
'name': 'Platform X', 'name': 'Platform X',
'slug': 'platform-x', 'slug': 'platform-x',
@ -1076,6 +1096,7 @@ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
'napalm_driver': 'junos', 'napalm_driver': 'junos',
'napalm_args': None, 'napalm_args': None,
'description': 'A new platform', 'description': 'A new platform',
'tags': [t.pk for t in tags],
} }
cls.csv_data = ( cls.csv_data = (

View File

@ -9,7 +9,6 @@ from ipam.choices import *
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES
from ipam.models import * from ipam.models import *
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
from netbox.api.serializers import OrganizationalModelSerializer
from netbox.api.serializers import PrimaryModelSerializer from netbox.api.serializers import PrimaryModelSerializer
from tenancy.api.nested_serializers import NestedTenantSerializer from tenancy.api.nested_serializers import NestedTenantSerializer
from utilities.api import get_serializer_for_model from utilities.api import get_serializer_for_model
@ -66,14 +65,14 @@ class RouteTargetSerializer(PrimaryModelSerializer):
# RIRs/aggregates # RIRs/aggregates
# #
class RIRSerializer(OrganizationalModelSerializer): class RIRSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail') url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
aggregate_count = serializers.IntegerField(read_only=True) aggregate_count = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = RIR model = RIR
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'is_private', 'description', 'custom_fields', 'created', 'id', 'url', 'display', 'name', 'slug', 'is_private', 'description', 'tags', 'custom_fields', 'created',
'last_updated', 'aggregate_count', 'last_updated', 'aggregate_count',
] ]
@ -97,7 +96,7 @@ class AggregateSerializer(PrimaryModelSerializer):
# VLANs # VLANs
# #
class RoleSerializer(OrganizationalModelSerializer): class RoleSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail') url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
prefix_count = serializers.IntegerField(read_only=True) prefix_count = serializers.IntegerField(read_only=True)
vlan_count = serializers.IntegerField(read_only=True) vlan_count = serializers.IntegerField(read_only=True)
@ -105,12 +104,12 @@ class RoleSerializer(OrganizationalModelSerializer):
class Meta: class Meta:
model = Role model = Role
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'weight', 'description', 'custom_fields', 'created', 'last_updated', 'id', 'url', 'display', 'name', 'slug', 'weight', 'description', 'tags', 'custom_fields', 'created',
'prefix_count', 'vlan_count', 'last_updated', 'prefix_count', 'vlan_count',
] ]
class VLANGroupSerializer(OrganizationalModelSerializer): class VLANGroupSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail') url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
scope_type = ContentTypeField( scope_type = ContentTypeField(
queryset=ContentType.objects.filter( queryset=ContentType.objects.filter(
@ -126,8 +125,8 @@ class VLANGroupSerializer(OrganizationalModelSerializer):
class Meta: class Meta:
model = VLANGroup model = VLANGroup
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'description', 'custom_fields', 'id', 'url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'description', 'tags',
'created', 'last_updated', 'vlan_count', 'custom_fields', 'created', 'last_updated', 'vlan_count',
] ]
validators = [] validators = []

View File

@ -48,7 +48,7 @@ class RouteTargetViewSet(CustomFieldModelViewSet):
class RIRViewSet(CustomFieldModelViewSet): class RIRViewSet(CustomFieldModelViewSet):
queryset = RIR.objects.annotate( queryset = RIR.objects.annotate(
aggregate_count=count_related(Aggregate, 'rir') aggregate_count=count_related(Aggregate, 'rir')
) ).prefetch_related('tags')
serializer_class = serializers.RIRSerializer serializer_class = serializers.RIRSerializer
filterset_class = filtersets.RIRFilterSet filterset_class = filtersets.RIRFilterSet
@ -71,7 +71,7 @@ class RoleViewSet(CustomFieldModelViewSet):
queryset = Role.objects.annotate( queryset = Role.objects.annotate(
prefix_count=count_related(Prefix, 'role'), prefix_count=count_related(Prefix, 'role'),
vlan_count=count_related(VLAN, 'role') vlan_count=count_related(VLAN, 'role')
) ).prefetch_related('tags')
serializer_class = serializers.RoleSerializer serializer_class = serializers.RoleSerializer
filterset_class = filtersets.RoleFilterSet filterset_class = filtersets.RoleFilterSet
@ -126,7 +126,7 @@ class IPAddressViewSet(CustomFieldModelViewSet):
class VLANGroupViewSet(CustomFieldModelViewSet): class VLANGroupViewSet(CustomFieldModelViewSet):
queryset = VLANGroup.objects.annotate( queryset = VLANGroup.objects.annotate(
vlan_count=count_related(VLAN, 'group') vlan_count=count_related(VLAN, 'group')
) ).prefetch_related('tags')
serializer_class = serializers.VLANGroupSerializer serializer_class = serializers.VLANGroupSerializer
filterset_class = filtersets.VLANGroupFilterSet filterset_class = filtersets.VLANGroupFilterSet

View File

@ -71,7 +71,7 @@ class RouteTargetBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldMode
] ]
class RIRBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): class RIRBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=RIR.objects.all(), queryset=RIR.objects.all(),
widget=forms.MultipleHiddenInput widget=forms.MultipleHiddenInput
@ -120,7 +120,7 @@ class AggregateBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelB
} }
class RoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): class RoleBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=Role.objects.all(), queryset=Role.objects.all(),
widget=forms.MultipleHiddenInput widget=forms.MultipleHiddenInput
@ -280,7 +280,7 @@ class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelB
] ]
class VLANGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): class VLANGroupBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=VLANGroup.objects.all(), queryset=VLANGroup.objects.all(),
widget=forms.MultipleHiddenInput widget=forms.MultipleHiddenInput

View File

@ -82,11 +82,15 @@ class RouteTargetForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
class RIRForm(BootstrapMixin, CustomFieldModelForm): class RIRForm(BootstrapMixin, CustomFieldModelForm):
slug = SlugField() slug = SlugField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta: class Meta:
model = RIR model = RIR
fields = [ fields = [
'name', 'slug', 'is_private', 'description', 'name', 'slug', 'is_private', 'description', 'tags',
] ]
@ -120,11 +124,15 @@ class AggregateForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
class RoleForm(BootstrapMixin, CustomFieldModelForm): class RoleForm(BootstrapMixin, CustomFieldModelForm):
slug = SlugField() slug = SlugField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta: class Meta:
model = Role model = Role
fields = [ fields = [
'name', 'slug', 'weight', 'description', 'name', 'slug', 'weight', 'description', 'tags',
] ]
@ -530,15 +538,19 @@ class VLANGroupForm(BootstrapMixin, CustomFieldModelForm):
} }
) )
slug = SlugField() slug = SlugField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta: class Meta:
model = VLANGroup model = VLANGroup
fields = [ fields = [
'name', 'slug', 'description', 'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'name', 'slug', 'description', 'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack',
'clustergroup', 'cluster', 'clustergroup', 'cluster', 'tags',
] ]
fieldsets = ( fieldsets = (
('VLAN Group', ('name', 'slug', 'description')), ('VLAN Group', ('name', 'slug', 'description', 'tags')),
('Scope', ('scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster')), ('Scope', ('scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster')),
) )
widgets = { widgets = {

View File

@ -0,0 +1,30 @@
# Generated by Django 3.2.8 on 2021-10-21 14:50
from django.db import migrations
import taggit.managers
class Migration(migrations.Migration):
dependencies = [
('extras', '0062_clear_secrets_changelog'),
('ipam', '0050_iprange'),
]
operations = [
migrations.AddField(
model_name='rir',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
migrations.AddField(
model_name='role',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
migrations.AddField(
model_name='vlangroup',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
]

View File

@ -31,7 +31,7 @@ __all__ = (
) )
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class RIR(OrganizationalModel): class RIR(OrganizationalModel):
""" """
A Regional Internet Registry (RIR) is responsible for the allocation of a large portion of the global IP address A Regional Internet Registry (RIR) is responsible for the allocation of a large portion of the global IP address
@ -168,7 +168,7 @@ class Aggregate(PrimaryModel):
return min(utilization, 100) return min(utilization, 100)
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Role(OrganizationalModel): class Role(OrganizationalModel):
""" """
A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or

View File

@ -21,7 +21,7 @@ __all__ = (
) )
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class VLANGroup(OrganizationalModel): class VLANGroup(OrganizationalModel):
""" """
A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique. A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.

View File

@ -85,11 +85,14 @@ class RIRTable(BaseTable):
url_params={'rir_id': 'pk'}, url_params={'rir_id': 'pk'},
verbose_name='Aggregates' verbose_name='Aggregates'
) )
tags = TagColumn(
url_name='ipam:rir_list'
)
actions = ButtonsColumn(RIR) actions = ButtonsColumn(RIR)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = RIR model = RIR
fields = ('pk', 'name', 'slug', 'is_private', 'aggregate_count', 'description', 'actions') fields = ('pk', 'name', 'slug', 'is_private', 'aggregate_count', 'description', 'tags', 'actions')
default_columns = ('pk', 'name', 'is_private', 'aggregate_count', 'description', 'actions') default_columns = ('pk', 'name', 'is_private', 'aggregate_count', 'description', 'actions')
@ -144,11 +147,14 @@ class RoleTable(BaseTable):
url_params={'role_id': 'pk'}, url_params={'role_id': 'pk'},
verbose_name='VLANs' verbose_name='VLANs'
) )
tags = TagColumn(
url_name='ipam:role_list'
)
actions = ButtonsColumn(Role) actions = ButtonsColumn(Role)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Role model = Role
fields = ('pk', 'name', 'slug', 'prefix_count', 'vlan_count', 'description', 'weight', 'actions') fields = ('pk', 'name', 'slug', 'prefix_count', 'vlan_count', 'description', 'weight', 'tags', 'actions')
default_columns = ('pk', 'name', 'prefix_count', 'vlan_count', 'description', 'actions') default_columns = ('pk', 'name', 'prefix_count', 'vlan_count', 'description', 'actions')

View File

@ -74,6 +74,9 @@ class VLANGroupTable(BaseTable):
url_params={'group_id': 'pk'}, url_params={'group_id': 'pk'},
verbose_name='VLANs' verbose_name='VLANs'
) )
tags = TagColumn(
url_name='ipam:vlangroup_list'
)
actions = ButtonsColumn( actions = ButtonsColumn(
model=VLANGroup, model=VLANGroup,
prepend_template=VLANGROUP_ADD_VLAN prepend_template=VLANGROUP_ADD_VLAN
@ -81,7 +84,7 @@ class VLANGroupTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = VLANGroup model = VLANGroup
fields = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'slug', 'description', 'actions') fields = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'slug', 'description', 'tags', 'actions')
default_columns = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'description', 'actions') default_columns = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'description', 'actions')

View File

@ -104,11 +104,14 @@ class RIRTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
RIR(name='RIR 3', slug='rir-3'), RIR(name='RIR 3', slug='rir-3'),
]) ])
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = { cls.form_data = {
'name': 'RIR X', 'name': 'RIR X',
'slug': 'rir-x', 'slug': 'rir-x',
'is_private': True, 'is_private': True,
'description': 'A new RIR', 'description': 'A new RIR',
'tags': [t.pk for t in tags],
} }
cls.csv_data = ( cls.csv_data = (
@ -177,11 +180,14 @@ class RoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
Role(name='Role 3', slug='role-3'), Role(name='Role 3', slug='role-3'),
]) ])
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = { cls.form_data = {
'name': 'Role X', 'name': 'Role X',
'slug': 'role-x', 'slug': 'role-x',
'weight': 200, 'weight': 200,
'description': 'A new role', 'description': 'A new role',
'tags': [t.pk for t in tags],
} }
cls.csv_data = ( cls.csv_data = (
@ -384,10 +390,13 @@ class VLANGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
VLANGroup(name='VLAN Group 3', slug='vlan-group-3', scope=sites[0]), VLANGroup(name='VLAN Group 3', slug='vlan-group-3', scope=sites[0]),
]) ])
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = { cls.form_data = {
'name': 'VLAN Group X', 'name': 'VLAN Group X',
'slug': 'vlan-group-x', 'slug': 'vlan-group-x',
'description': 'A new VLAN group', 'description': 'A new VLAN group',
'tags': [t.pk for t in tags],
} }
cls.csv_data = ( cls.csv_data = (

View File

@ -147,13 +147,6 @@ class NestedTagSerializer(WritableNestedSerializer):
# Base model serializers # Base model serializers
# #
class OrganizationalModelSerializer(CustomFieldModelSerializer):
"""
Adds support for custom fields.
"""
pass
class PrimaryModelSerializer(CustomFieldModelSerializer): class PrimaryModelSerializer(CustomFieldModelSerializer):
""" """
Adds support for custom fields and tags. Adds support for custom fields and tags.
@ -189,9 +182,9 @@ class PrimaryModelSerializer(CustomFieldModelSerializer):
return instance return instance
class NestedGroupModelSerializer(CustomFieldModelSerializer): class NestedGroupModelSerializer(PrimaryModelSerializer):
""" """
Extends OrganizationalModelSerializer to include MPTT support. Extends PrimaryModelSerializer to include MPTT support.
""" """
_depth = serializers.IntegerField(source='level', read_only=True) _depth = serializers.IntegerField(source='level', read_only=True)

View File

@ -41,6 +41,7 @@ class ObjectType(
class OrganizationalObjectType( class OrganizationalObjectType(
ChangelogMixin, ChangelogMixin,
CustomFieldsMixin, CustomFieldsMixin,
TagsMixin,
BaseObjectType BaseObjectType
): ):
""" """

View File

@ -143,6 +143,18 @@ class CustomValidationMixin(models.Model):
post_clean.send(sender=self.__class__, instance=self) post_clean.send(sender=self.__class__, instance=self)
class TagsMixin(models.Model):
"""
Enable the assignment of Tags.
"""
tags = TaggableManager(
through='extras.TaggedItem'
)
class Meta:
abstract = True
# #
# Base model classes # Base model classes
@ -166,7 +178,7 @@ class ChangeLoggedModel(ChangeLoggingMixin, CustomValidationMixin, BigIDModel):
abstract = True abstract = True
class PrimaryModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin, BigIDModel): class PrimaryModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin, TagsMixin, BigIDModel):
""" """
Primary models represent real objects within the infrastructure being modeled. Primary models represent real objects within the infrastructure being modeled.
""" """
@ -175,15 +187,12 @@ class PrimaryModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin,
object_id_field='assigned_object_id', object_id_field='assigned_object_id',
content_type_field='assigned_object_type' content_type_field='assigned_object_type'
) )
tags = TaggableManager(
through='extras.TaggedItem'
)
class Meta: class Meta:
abstract = True abstract = True
class NestedGroupModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin, BigIDModel, MPTTModel): class NestedGroupModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin, TagsMixin, BigIDModel, MPTTModel):
""" """
Base model for objects which are used to form a hierarchy (regions, locations, etc.). These models nest Base model for objects which are used to form a hierarchy (regions, locations, etc.). These models nest
recursively using MPTT. Within each parent, each child instance must have a unique name. recursively using MPTT. Within each parent, each child instance must have a unique name.
@ -225,7 +234,7 @@ class NestedGroupModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMi
}) })
class OrganizationalModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin, BigIDModel): class OrganizationalModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin, TagsMixin, BigIDModel):
""" """
Organizational models are those which are used solely to categorize and qualify other objects, and do not convey Organizational models are those which are used solely to categorize and qualify other objects, and do not convey
any real information about the infrastructure being modeled (for example, functional device roles). Organizational any real information about the infrastructure being modeled (for example, functional device roles). Organizational

View File

@ -2,7 +2,7 @@ from django.contrib.auth.models import ContentType
from rest_framework import serializers from rest_framework import serializers
from netbox.api import ChoiceField, ContentTypeField from netbox.api import ChoiceField, ContentTypeField
from netbox.api.serializers import NestedGroupModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer from netbox.api.serializers import NestedGroupModelSerializer, PrimaryModelSerializer
from tenancy.choices import ContactPriorityChoices from tenancy.choices import ContactPriorityChoices
from tenancy.models import * from tenancy.models import *
from .nested_serializers import * from .nested_serializers import *
@ -20,8 +20,8 @@ class TenantGroupSerializer(NestedGroupModelSerializer):
class Meta: class Meta:
model = TenantGroup model = TenantGroup
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'custom_fields', 'created', 'last_updated', 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created',
'tenant_count', '_depth', 'last_updated', 'tenant_count', '_depth',
] ]
@ -60,18 +60,18 @@ class ContactGroupSerializer(NestedGroupModelSerializer):
class Meta: class Meta:
model = ContactGroup model = ContactGroup
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'custom_fields', 'created', 'last_updated', 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created',
'contact_count', '_depth', 'last_updated', 'contact_count', '_depth',
] ]
class ContactRoleSerializer(OrganizationalModelSerializer): class ContactRoleSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactrole-detail') url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactrole-detail')
class Meta: class Meta:
model = ContactRole model = ContactRole
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'description', 'custom_fields', 'created', 'last_updated', 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
] ]

View File

@ -30,7 +30,7 @@ class TenantGroupViewSet(CustomFieldModelViewSet):
'group', 'group',
'tenant_count', 'tenant_count',
cumulative=True cumulative=True
) ).prefetch_related('tags')
serializer_class = serializers.TenantGroupSerializer serializer_class = serializers.TenantGroupSerializer
filterset_class = filtersets.TenantGroupFilterSet filterset_class = filtersets.TenantGroupFilterSet
@ -64,28 +64,24 @@ class ContactGroupViewSet(CustomFieldModelViewSet):
'group', 'group',
'contact_count', 'contact_count',
cumulative=True cumulative=True
) ).prefetch_related('tags')
serializer_class = serializers.ContactGroupSerializer serializer_class = serializers.ContactGroupSerializer
filterset_class = filtersets.ContactGroupFilterSet filterset_class = filtersets.ContactGroupFilterSet
class ContactRoleViewSet(CustomFieldModelViewSet): class ContactRoleViewSet(CustomFieldModelViewSet):
queryset = ContactRole.objects.all() queryset = ContactRole.objects.prefetch_related('tags')
serializer_class = serializers.ContactRoleSerializer serializer_class = serializers.ContactRoleSerializer
filterset_class = filtersets.ContactRoleFilterSet filterset_class = filtersets.ContactRoleFilterSet
class ContactViewSet(CustomFieldModelViewSet): class ContactViewSet(CustomFieldModelViewSet):
queryset = Contact.objects.prefetch_related( queryset = Contact.objects.prefetch_related('group', 'tags')
'group', 'tags'
)
serializer_class = serializers.ContactSerializer serializer_class = serializers.ContactSerializer
filterset_class = filtersets.ContactFilterSet filterset_class = filtersets.ContactFilterSet
class ContactAssignmentViewSet(CustomFieldModelViewSet): class ContactAssignmentViewSet(CustomFieldModelViewSet):
queryset = ContactAssignment.objects.prefetch_related( queryset = ContactAssignment.objects.prefetch_related('contact', 'role')
'contact', 'role'
)
serializer_class = serializers.ContactAssignmentSerializer serializer_class = serializers.ContactAssignmentSerializer
filterset_class = filtersets.ContactAssignmentFilterSet filterset_class = filtersets.ContactAssignmentFilterSet

View File

@ -17,7 +17,7 @@ __all__ = (
# Tenants # Tenants
# #
class TenantGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): class TenantGroupBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=TenantGroup.objects.all(), queryset=TenantGroup.objects.all(),
widget=forms.MultipleHiddenInput widget=forms.MultipleHiddenInput
@ -55,7 +55,7 @@ class TenantBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulk
# Contacts # Contacts
# #
class ContactGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): class ContactGroupBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=ContactGroup.objects.all(), queryset=ContactGroup.objects.all(),
widget=forms.MultipleHiddenInput widget=forms.MultipleHiddenInput
@ -73,7 +73,7 @@ class ContactGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
nullable_fields = ['parent', 'description'] nullable_fields = ['parent', 'description']
class ContactRoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): class ContactRoleBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=ContactRole.objects.all(), queryset=ContactRole.objects.all(),
widget=forms.MultipleHiddenInput widget=forms.MultipleHiddenInput

View File

@ -28,11 +28,15 @@ class TenantGroupForm(BootstrapMixin, CustomFieldModelForm):
required=False required=False
) )
slug = SlugField() slug = SlugField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta: class Meta:
model = TenantGroup model = TenantGroup
fields = [ fields = [
'parent', 'name', 'slug', 'description', 'parent', 'name', 'slug', 'description', 'tags',
] ]
@ -68,18 +72,26 @@ class ContactGroupForm(BootstrapMixin, CustomFieldModelForm):
required=False required=False
) )
slug = SlugField() slug = SlugField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta: class Meta:
model = ContactGroup model = ContactGroup
fields = ['parent', 'name', 'slug', 'description'] fields = ('parent', 'name', 'slug', 'description', 'tags')
class ContactRoleForm(BootstrapMixin, CustomFieldModelForm): class ContactRoleForm(BootstrapMixin, CustomFieldModelForm):
slug = SlugField() slug = SlugField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta: class Meta:
model = ContactRole model = ContactRole
fields = ['name', 'slug', 'description'] fields = ('name', 'slug', 'description', 'tags')
class ContactForm(BootstrapMixin, CustomFieldModelForm): class ContactForm(BootstrapMixin, CustomFieldModelForm):

View File

@ -0,0 +1,30 @@
# Generated by Django 3.2.8 on 2021-10-21 14:50
from django.db import migrations
import taggit.managers
class Migration(migrations.Migration):
dependencies = [
('extras', '0062_clear_secrets_changelog'),
('tenancy', '0003_contacts'),
]
operations = [
migrations.AddField(
model_name='contactgroup',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
migrations.AddField(
model_name='contactrole',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
migrations.AddField(
model_name='tenantgroup',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
]

View File

@ -24,7 +24,7 @@ __all__ = (
# Tenants # Tenants
# #
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class TenantGroup(NestedGroupModel): class TenantGroup(NestedGroupModel):
""" """
An arbitrary collection of Tenants. An arbitrary collection of Tenants.
@ -111,7 +111,7 @@ class Tenant(PrimaryModel):
# Contacts # Contacts
# #
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class ContactGroup(NestedGroupModel): class ContactGroup(NestedGroupModel):
""" """
An arbitrary collection of Contacts. An arbitrary collection of Contacts.
@ -145,7 +145,7 @@ class ContactGroup(NestedGroupModel):
return reverse('tenancy:contactgroup', args=[self.pk]) return reverse('tenancy:contactgroup', args=[self.pk])
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class ContactRole(OrganizationalModel): class ContactRole(OrganizationalModel):
""" """
Functional role for a Contact assigned to an object. Functional role for a Contact assigned to an object.

View File

@ -55,11 +55,14 @@ class TenantGroupTable(BaseTable):
url_params={'group_id': 'pk'}, url_params={'group_id': 'pk'},
verbose_name='Tenants' verbose_name='Tenants'
) )
tags = TagColumn(
url_name='tenancy:tenantgroup_list'
)
actions = ButtonsColumn(TenantGroup) actions = ButtonsColumn(TenantGroup)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = TenantGroup model = TenantGroup
fields = ('pk', 'name', 'tenant_count', 'description', 'slug', 'actions') fields = ('pk', 'name', 'tenant_count', 'description', 'slug', 'tags', 'actions')
default_columns = ('pk', 'name', 'tenant_count', 'description', 'actions') default_columns = ('pk', 'name', 'tenant_count', 'description', 'actions')
@ -96,11 +99,14 @@ class ContactGroupTable(BaseTable):
url_params={'role_id': 'pk'}, url_params={'role_id': 'pk'},
verbose_name='Contacts' verbose_name='Contacts'
) )
tags = TagColumn(
url_name='tenancy:contactgroup_list'
)
actions = ButtonsColumn(ContactGroup) actions = ButtonsColumn(ContactGroup)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = ContactGroup model = ContactGroup
fields = ('pk', 'name', 'contact_count', 'description', 'slug', 'actions') fields = ('pk', 'name', 'contact_count', 'description', 'slug', 'tags', 'actions')
default_columns = ('pk', 'name', 'contact_count', 'description', 'actions') default_columns = ('pk', 'name', 'contact_count', 'description', 'actions')

View File

@ -16,10 +16,13 @@ class TenantGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
for tenanantgroup in tenant_groups: for tenanantgroup in tenant_groups:
tenanantgroup.save() tenanantgroup.save()
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = { cls.form_data = {
'name': 'Tenant Group X', 'name': 'Tenant Group X',
'slug': 'tenant-group-x', 'slug': 'tenant-group-x',
'description': 'A new tenant group', 'description': 'A new tenant group',
'tags': [t.pk for t in tags],
} }
cls.csv_data = ( cls.csv_data = (
@ -90,10 +93,13 @@ class ContactGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
for tenanantgroup in contact_groups: for tenanantgroup in contact_groups:
tenanantgroup.save() tenanantgroup.save()
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = { cls.form_data = {
'name': 'Contact Group X', 'name': 'Contact Group X',
'slug': 'contact-group-x', 'slug': 'contact-group-x',
'description': 'A new contact group', 'description': 'A new contact group',
'tags': [t.pk for t in tags],
} }
cls.csv_data = ( cls.csv_data = (
@ -120,10 +126,13 @@ class ContactRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
ContactRole(name='Contact Role 3', slug='contact-role-3'), ContactRole(name='Contact Role 3', slug='contact-role-3'),
]) ])
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = { cls.form_data = {
'name': 'Devie Role X', 'name': 'Devie Role X',
'slug': 'contact-role-x', 'slug': 'contact-role-x',
'description': 'New contact role', 'description': 'New contact role',
'tags': [t.pk for t in tags],
} }
cls.csv_data = ( cls.csv_data = (

View File

@ -6,7 +6,7 @@ from dcim.choices import InterfaceModeChoices
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
from ipam.models import VLAN from ipam.models import VLAN
from netbox.api import ChoiceField, SerializedPKRelatedField from netbox.api import ChoiceField, SerializedPKRelatedField
from netbox.api.serializers import OrganizationalModelSerializer, PrimaryModelSerializer from netbox.api.serializers import PrimaryModelSerializer
from tenancy.api.nested_serializers import NestedTenantSerializer from tenancy.api.nested_serializers import NestedTenantSerializer
from virtualization.choices import * from virtualization.choices import *
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
@ -17,26 +17,26 @@ from .nested_serializers import *
# Clusters # Clusters
# #
class ClusterTypeSerializer(OrganizationalModelSerializer): class ClusterTypeSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail') url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail')
cluster_count = serializers.IntegerField(read_only=True) cluster_count = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = ClusterType model = ClusterType
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'description', 'custom_fields', 'created', 'last_updated', 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
'cluster_count', 'cluster_count',
] ]
class ClusterGroupSerializer(OrganizationalModelSerializer): class ClusterGroupSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail') url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail')
cluster_count = serializers.IntegerField(read_only=True) cluster_count = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = ClusterGroup model = ClusterGroup
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'description', 'custom_fields', 'created', 'last_updated', 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
'cluster_count', 'cluster_count',
] ]

View File

@ -23,7 +23,7 @@ class VirtualizationRootView(APIRootView):
class ClusterTypeViewSet(CustomFieldModelViewSet): class ClusterTypeViewSet(CustomFieldModelViewSet):
queryset = ClusterType.objects.annotate( queryset = ClusterType.objects.annotate(
cluster_count=count_related(Cluster, 'type') cluster_count=count_related(Cluster, 'type')
) ).prefetch_related('tags')
serializer_class = serializers.ClusterTypeSerializer serializer_class = serializers.ClusterTypeSerializer
filterset_class = filtersets.ClusterTypeFilterSet filterset_class = filtersets.ClusterTypeFilterSet
@ -31,7 +31,7 @@ class ClusterTypeViewSet(CustomFieldModelViewSet):
class ClusterGroupViewSet(CustomFieldModelViewSet): class ClusterGroupViewSet(CustomFieldModelViewSet):
queryset = ClusterGroup.objects.annotate( queryset = ClusterGroup.objects.annotate(
cluster_count=count_related(Cluster, 'group') cluster_count=count_related(Cluster, 'group')
) ).prefetch_related('tags')
serializer_class = serializers.ClusterGroupSerializer serializer_class = serializers.ClusterGroupSerializer
filterset_class = filtersets.ClusterGroupFilterSet filterset_class = filtersets.ClusterGroupFilterSet

View File

@ -23,7 +23,7 @@ __all__ = (
) )
class ClusterTypeBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): class ClusterTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=ClusterType.objects.all(), queryset=ClusterType.objects.all(),
widget=forms.MultipleHiddenInput widget=forms.MultipleHiddenInput
@ -37,7 +37,7 @@ class ClusterTypeBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
nullable_fields = ['description'] nullable_fields = ['description']
class ClusterGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): class ClusterGroupBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=ClusterGroup.objects.all(), queryset=ClusterGroup.objects.all(),
widget=forms.MultipleHiddenInput widget=forms.MultipleHiddenInput

View File

@ -28,22 +28,30 @@ __all__ = (
class ClusterTypeForm(BootstrapMixin, CustomFieldModelForm): class ClusterTypeForm(BootstrapMixin, CustomFieldModelForm):
slug = SlugField() slug = SlugField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta: class Meta:
model = ClusterType model = ClusterType
fields = [ fields = (
'name', 'slug', 'description', 'name', 'slug', 'description', 'tags',
] )
class ClusterGroupForm(BootstrapMixin, CustomFieldModelForm): class ClusterGroupForm(BootstrapMixin, CustomFieldModelForm):
slug = SlugField() slug = SlugField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta: class Meta:
model = ClusterGroup model = ClusterGroup
fields = [ fields = (
'name', 'slug', 'description', 'name', 'slug', 'description', 'tags',
] )
class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):

View File

@ -0,0 +1,25 @@
# Generated by Django 3.2.8 on 2021-10-21 14:50
from django.db import migrations
import taggit.managers
class Migration(migrations.Migration):
dependencies = [
('extras', '0062_clear_secrets_changelog'),
('virtualization', '0024_cluster_relax_uniqueness'),
]
operations = [
migrations.AddField(
model_name='clustergroup',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
migrations.AddField(
model_name='clustertype',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
]

View File

@ -30,7 +30,7 @@ __all__ = (
# Cluster types # Cluster types
# #
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class ClusterType(OrganizationalModel): class ClusterType(OrganizationalModel):
""" """
A type of Cluster. A type of Cluster.
@ -64,7 +64,7 @@ class ClusterType(OrganizationalModel):
# Cluster groups # Cluster groups
# #
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class ClusterGroup(OrganizationalModel): class ClusterGroup(OrganizationalModel):
""" """
An organizational group of Clusters. An organizational group of Clusters.

View File

@ -40,11 +40,14 @@ class ClusterTypeTable(BaseTable):
cluster_count = tables.Column( cluster_count = tables.Column(
verbose_name='Clusters' verbose_name='Clusters'
) )
tags = TagColumn(
url_name='virtualization:clustertype_list'
)
actions = ButtonsColumn(ClusterType) actions = ButtonsColumn(ClusterType)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = ClusterType model = ClusterType
fields = ('pk', 'name', 'slug', 'cluster_count', 'description', 'actions') fields = ('pk', 'name', 'slug', 'cluster_count', 'description', 'tags', 'actions')
default_columns = ('pk', 'name', 'cluster_count', 'description', 'actions') default_columns = ('pk', 'name', 'cluster_count', 'description', 'actions')
@ -60,11 +63,14 @@ class ClusterGroupTable(BaseTable):
cluster_count = tables.Column( cluster_count = tables.Column(
verbose_name='Clusters' verbose_name='Clusters'
) )
tags = TagColumn(
url_name='virtualization:clustergroup_list'
)
actions = ButtonsColumn(ClusterGroup) actions = ButtonsColumn(ClusterGroup)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = ClusterGroup model = ClusterGroup
fields = ('pk', 'name', 'slug', 'cluster_count', 'description', 'actions') fields = ('pk', 'name', 'slug', 'cluster_count', 'description', 'tags', 'actions')
default_columns = ('pk', 'name', 'cluster_count', 'description', 'actions') default_columns = ('pk', 'name', 'cluster_count', 'description', 'actions')

View File

@ -22,10 +22,13 @@ class ClusterGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
ClusterGroup(name='Cluster Group 3', slug='cluster-group-3'), ClusterGroup(name='Cluster Group 3', slug='cluster-group-3'),
]) ])
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = { cls.form_data = {
'name': 'Cluster Group X', 'name': 'Cluster Group X',
'slug': 'cluster-group-x', 'slug': 'cluster-group-x',
'description': 'A new cluster group', 'description': 'A new cluster group',
'tags': [t.pk for t in tags],
} }
cls.csv_data = ( cls.csv_data = (
@ -52,10 +55,13 @@ class ClusterTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
ClusterType(name='Cluster Type 3', slug='cluster-type-3'), ClusterType(name='Cluster Type 3', slug='cluster-type-3'),
]) ])
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = { cls.form_data = {
'name': 'Cluster Type X', 'name': 'Cluster Type X',
'slug': 'cluster-type-x', 'slug': 'cluster-type-x',
'description': 'A new cluster type', 'description': 'A new cluster type',
'tags': [t.pk for t in tags],
} }
cls.csv_data = ( cls.csv_data = (