Merge pull request #7610 from netbox-community/6497-organizational-model-tags

Closes #6297: Extend tag support to organizational models
This commit is contained in:
Jeremy Stretch 2021-10-21 11:49:18 -04:00 committed by GitHub
commit 001ab1d067
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
104 changed files with 541 additions and 196 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 |
| ------------------ | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- |
| Primary | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | |
| Organizational | :material-check: | :material-check: | :material-check: | :material-check: | | | |
| Nested Group | :material-check: | :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: | | :material-check: |
| Component | :material-check: | :material-check: | :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
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

@ -20,6 +20,7 @@ When assigning a contact to an object, the user must select a predefined role (e
* [#1337](https://github.com/netbox-community/netbox/issues/1337) - Add WWN field to interfaces
* [#1943](https://github.com/netbox-community/netbox/issues/1943) - Relax uniqueness constraint on cluster names
* [#3839](https://github.com/netbox-community/netbox/issues/3839) - Add `airflow` field for devices types and devices
* [#6497](https://github.com/netbox-community/netbox/issues/6497) - Extend tag support to organizational models
* [#6711](https://github.com/netbox-community/netbox/issues/6711) - Add `longtext` custom field type with Markdown support
* [#6715](https://github.com/netbox-community/netbox/issues/6715) - Add tenant assignment for cables
* [#6874](https://github.com/netbox-community/netbox/issues/6874) - Add tenant assignment for locations
@ -37,6 +38,23 @@ When assigning a contact to an object, the user must select a predefined role (e
* `/api/tenancy/contact-groups/`
* `/api/tenancy/contact-roles/`
* `/api/tenancy/contacts/`
* Added `tags` field to the following models:
* circuits.CircuitType
* dcim.DeviceRole
* dcim.Location
* dcim.Manufacturer
* dcim.Platform
* dcim.RackRole
* dcim.Region
* dcim.SiteGroup
* ipam.RIR
* ipam.Role
* ipam.VLANGroup
* tenancy.ContactGroup
* tenancy.ContactRole
* tenancy.TenantGroup
* virtualization.ClusterGroup
* virtualization.ClusterType
* dcim.Cable
* Added `tenant` field
* dcim.Device

View File

@ -5,9 +5,7 @@ from circuits.models import *
from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
from dcim.api.serializers import CableTerminationSerializer
from netbox.api import ChoiceField
from netbox.api.serializers import (
OrganizationalModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer
)
from netbox.api.serializers import PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer
from tenancy.api.nested_serializers import NestedTenantSerializer
from .nested_serializers import *
@ -48,14 +46,14 @@ class ProviderNetworkSerializer(PrimaryModelSerializer):
# Circuits
#
class CircuitTypeSerializer(OrganizationalModelSerializer):
class CircuitTypeSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
circuit_count = serializers.IntegerField(read_only=True)
class Meta:
model = CircuitType
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',
]

View File

@ -34,7 +34,7 @@ class ProviderViewSet(CustomFieldModelViewSet):
#
class CircuitTypeViewSet(CustomFieldModelViewSet):
queryset = CircuitType.objects.annotate(
queryset = CircuitType.objects.prefetch_related('tags').annotate(
circuit_count=count_related(Circuit, 'type')
)
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(
queryset=CircuitType.objects.all(),
widget=forms.MultipleHiddenInput

View File

@ -75,11 +75,15 @@ class ProviderNetworkForm(BootstrapMixin, CustomFieldModelForm):
class CircuitTypeForm(BootstrapMixin, CustomFieldModelForm):
slug = SlugField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta:
model = CircuitType
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])
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class CircuitType(OrganizationalModel):
"""
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(
linkify=True
)
tags = TagColumn(
url_name='circuits:circuittype_list'
)
circuit_count = tables.Column(
verbose_name='Circuits'
)
@ -89,7 +92,7 @@ class CircuitTypeTable(BaseTable):
class Meta(BaseTable.Meta):
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')

View File

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

View File

@ -11,8 +11,7 @@ from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSer
from ipam.models import VLAN
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
from netbox.api.serializers import (
NestedGroupModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer,
WritableNestedSerializer,
NestedGroupModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer,
)
from tenancy.api.nested_serializers import NestedTenantSerializer
from users.api.nested_serializers import NestedUserSerializer
@ -87,8 +86,8 @@ class RegionSerializer(NestedGroupModelSerializer):
class Meta:
model = Region
fields = [
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'custom_fields', 'created', 'last_updated',
'site_count', '_depth',
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created',
'last_updated', 'site_count', '_depth',
]
@ -100,8 +99,8 @@ class SiteGroupSerializer(NestedGroupModelSerializer):
class Meta:
model = SiteGroup
fields = [
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'custom_fields', 'created', 'last_updated',
'site_count', '_depth',
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created',
'last_updated', 'site_count', '_depth',
]
@ -144,20 +143,20 @@ class LocationSerializer(NestedGroupModelSerializer):
class Meta:
model = Location
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',
]
class RackRoleSerializer(OrganizationalModelSerializer):
class RackRoleSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
rack_count = serializers.IntegerField(read_only=True)
class Meta:
model = RackRole
fields = [
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'custom_fields', 'created', 'last_updated',
'rack_count',
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created',
'last_updated', 'rack_count',
]
@ -254,7 +253,7 @@ class RackElevationDetailFilterSerializer(serializers.Serializer):
# Device types
#
class ManufacturerSerializer(OrganizationalModelSerializer):
class ManufacturerSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
devicetype_count = serializers.IntegerField(read_only=True)
inventoryitem_count = serializers.IntegerField(read_only=True)
@ -263,7 +262,7 @@ class ManufacturerSerializer(OrganizationalModelSerializer):
class Meta:
model = Manufacturer
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',
]
@ -411,7 +410,7 @@ class DeviceBayTemplateSerializer(ValidatedModelSerializer):
# Devices
#
class DeviceRoleSerializer(OrganizationalModelSerializer):
class DeviceRoleSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
device_count = serializers.IntegerField(read_only=True)
virtualmachine_count = serializers.IntegerField(read_only=True)
@ -419,12 +418,12 @@ class DeviceRoleSerializer(OrganizationalModelSerializer):
class Meta:
model = DeviceRole
fields = [
'id', 'url', 'display', 'name', 'slug', 'color', 'vm_role', 'description', 'custom_fields', 'created',
'last_updated', 'device_count', 'virtualmachine_count',
'id', 'url', 'display', 'name', 'slug', 'color', 'vm_role', 'description', 'tags', 'custom_fields',
'created', 'last_updated', 'device_count', 'virtualmachine_count',
]
class PlatformSerializer(OrganizationalModelSerializer):
class PlatformSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True)
device_count = serializers.IntegerField(read_only=True)
@ -434,7 +433,7 @@ class PlatformSerializer(OrganizationalModelSerializer):
model = Platform
fields = [
'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',
'site_count',
cumulative=True
)
).prefetch_related('tags')
serializer_class = serializers.RegionSerializer
filterset_class = filtersets.RegionFilterSet
@ -126,7 +126,7 @@ class SiteGroupViewSet(CustomFieldModelViewSet):
'group',
'site_count',
cumulative=True
)
).prefetch_related('tags')
serializer_class = serializers.SiteGroupSerializer
filterset_class = filtersets.SiteGroupFilterSet
@ -167,7 +167,7 @@ class LocationViewSet(CustomFieldModelViewSet):
'location',
'rack_count',
cumulative=True
).prefetch_related('site')
).prefetch_related('site', 'tags')
serializer_class = serializers.LocationSerializer
filterset_class = filtersets.LocationFilterSet
@ -177,7 +177,7 @@ class LocationViewSet(CustomFieldModelViewSet):
#
class RackRoleViewSet(CustomFieldModelViewSet):
queryset = RackRole.objects.annotate(
queryset = RackRole.objects.prefetch_related('tags').annotate(
rack_count=count_related(Rack, 'role')
)
serializer_class = serializers.RackRoleSerializer
@ -261,7 +261,7 @@ class RackReservationViewSet(ModelViewSet):
#
class ManufacturerViewSet(CustomFieldModelViewSet):
queryset = Manufacturer.objects.annotate(
queryset = Manufacturer.objects.prefetch_related('tags').annotate(
devicetype_count=count_related(DeviceType, 'manufacturer'),
inventoryitem_count=count_related(InventoryItem, 'manufacturer'),
platform_count=count_related(Platform, 'manufacturer')
@ -340,7 +340,7 @@ class DeviceBayTemplateViewSet(ModelViewSet):
#
class DeviceRoleViewSet(CustomFieldModelViewSet):
queryset = DeviceRole.objects.annotate(
queryset = DeviceRole.objects.prefetch_related('tags').annotate(
device_count=count_related(Device, 'device_role'),
virtualmachine_count=count_related(VirtualMachine, 'role')
)
@ -353,7 +353,7 @@ class DeviceRoleViewSet(CustomFieldModelViewSet):
#
class PlatformViewSet(CustomFieldModelViewSet):
queryset = Platform.objects.annotate(
queryset = Platform.objects.prefetch_related('tags').annotate(
device_count=count_related(Device, '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(
queryset=Region.objects.all(),
widget=forms.MultipleHiddenInput
@ -69,7 +69,7 @@ class RegionBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
nullable_fields = ['parent', 'description']
class SiteGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
class SiteGroupBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
widget=forms.MultipleHiddenInput
@ -132,7 +132,7 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEd
]
class LocationBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
class LocationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=Location.objects.all(),
widget=forms.MultipleHiddenInput
@ -161,7 +161,7 @@ class LocationBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
nullable_fields = ['parent', 'tenant', 'description']
class RackRoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
class RackRoleBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=RackRole.objects.all(),
widget=forms.MultipleHiddenInput
@ -303,7 +303,7 @@ class RackReservationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomField
nullable_fields = []
class ManufacturerBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
class ManufacturerBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=Manufacturer.objects.all(),
widget=forms.MultipleHiddenInput
@ -345,7 +345,7 @@ class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModel
nullable_fields = ['airflow']
class DeviceRoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
class DeviceRoleBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=DeviceRole.objects.all(),
widget=forms.MultipleHiddenInput
@ -367,7 +367,7 @@ class DeviceRoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
nullable_fields = ['color', 'description']
class PlatformBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
class PlatformBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=Platform.objects.all(),
widget=forms.MultipleHiddenInput

View File

@ -70,11 +70,15 @@ class RegionForm(BootstrapMixin, CustomFieldModelForm):
required=False
)
slug = SlugField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta:
model = Region
fields = (
'parent', 'name', 'slug', 'description',
'parent', 'name', 'slug', 'description', 'tags',
)
@ -84,11 +88,15 @@ class SiteGroupForm(BootstrapMixin, CustomFieldModelForm):
required=False
)
slug = SlugField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta:
model = SiteGroup
fields = (
'parent', 'name', 'slug', 'description',
'parent', 'name', 'slug', 'description', 'tags',
)
@ -187,15 +195,19 @@ class LocationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
}
)
slug = SlugField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta:
model = Location
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 = (
('Location', (
'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description',
'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description', 'tags',
)),
('Tenancy', ('tenant_group', 'tenant')),
)
@ -203,11 +215,15 @@ class LocationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
class RackRoleForm(BootstrapMixin, CustomFieldModelForm):
slug = SlugField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta:
model = RackRole
fields = [
'name', 'slug', 'color', 'description',
'name', 'slug', 'color', 'description', 'tags',
]
@ -343,11 +359,15 @@ class RackReservationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
class ManufacturerForm(BootstrapMixin, CustomFieldModelForm):
slug = SlugField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta:
model = Manufacturer
fields = [
'name', 'slug', 'description',
'name', 'slug', 'description', 'tags',
]
@ -392,11 +412,15 @@ class DeviceTypeForm(BootstrapMixin, CustomFieldModelForm):
class DeviceRoleForm(BootstrapMixin, CustomFieldModelForm):
slug = SlugField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta:
model = DeviceRole
fields = [
'name', 'slug', 'color', 'vm_role', 'description',
'name', 'slug', 'color', 'vm_role', 'description', 'tags',
]
@ -408,11 +432,15 @@ class PlatformForm(BootstrapMixin, CustomFieldModelForm):
slug = SlugField(
max_length=64
)
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta:
model = Platform
fields = [
'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description',
'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'tags',
]
widgets = {
'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
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Manufacturer(OrganizationalModel):
"""
A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
@ -351,7 +351,7 @@ class DeviceType(PrimaryModel):
# Devices
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class DeviceRole(OrganizationalModel):
"""
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])
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Platform(OrganizationalModel):
"""
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
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class RackRole(OrganizationalModel):
"""
Racks can be organized by functional role, similar to Devices.

View File

@ -25,7 +25,7 @@ __all__ = (
# Regions
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Region(NestedGroupModel):
"""
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
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class SiteGroup(NestedGroupModel):
"""
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
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Location(NestedGroupModel):
"""
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()
vm_role = BooleanColumn()
tags = TagColumn(
url_name='dcim:devicerole_list'
)
actions = ButtonsColumn(DeviceRole)
class Meta(BaseTable.Meta):
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')
@ -111,13 +116,16 @@ class PlatformTable(BaseTable):
url_params={'platform_id': 'pk'},
verbose_name='VMs'
)
tags = TagColumn(
url_name='dcim:platform_list'
)
actions = ButtonsColumn(Platform)
class Meta(BaseTable.Meta):
model = Platform
fields = (
'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'napalm_args',
'description', 'actions',
'description', 'tags', 'actions',
)
default_columns = (
'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'napalm_driver', 'description', 'actions',

View File

@ -41,12 +41,16 @@ class ManufacturerTable(BaseTable):
verbose_name='Platforms'
)
slug = tables.Column()
tags = TagColumn(
url_name='dcim:manufacturer_list'
)
actions = ButtonsColumn(Manufacturer)
class Meta(BaseTable.Meta):
model = Manufacturer
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)
rack_count = tables.Column(verbose_name='Racks')
color = ColorColumn()
tags = TagColumn(
url_name='dcim:rackrole_list'
)
actions = ButtonsColumn(RackRole)
class Meta(BaseTable.Meta):
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')

View File

@ -29,11 +29,14 @@ class RegionTable(BaseTable):
url_params={'region_id': 'pk'},
verbose_name='Sites'
)
tags = TagColumn(
url_name='dcim:region_list'
)
actions = ButtonsColumn(Region)
class Meta(BaseTable.Meta):
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')
@ -51,11 +54,14 @@ class SiteGroupTable(BaseTable):
url_params={'group_id': 'pk'},
verbose_name='Sites'
)
tags = TagColumn(
url_name='dcim:sitegroup_list'
)
actions = ButtonsColumn(SiteGroup)
class Meta(BaseTable.Meta):
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')
@ -114,6 +120,9 @@ class LocationTable(BaseTable):
url_params={'location_id': 'pk'},
verbose_name='Devices'
)
tags = TagColumn(
url_name='dcim:location_list'
)
actions = ButtonsColumn(
model=Location,
prepend_template=LOCATION_ELEVATIONS
@ -121,5 +130,7 @@ class LocationTable(BaseTable):
class Meta(BaseTable.Meta):
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')

View File

@ -31,11 +31,14 @@ class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
for region in regions:
region.save()
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'name': 'Region X',
'slug': 'region-x',
'parent': regions[2].pk,
'description': 'A new region',
'tags': [t.pk for t in tags],
}
cls.csv_data = (
@ -65,11 +68,14 @@ class SiteGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
for sitegroup in sitegroups:
sitegroup.save()
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'name': 'Site Group X',
'slug': 'site-group-x',
'parent': sitegroups[2].pk,
'description': 'A new site group',
'tags': [t.pk for t in tags],
}
cls.csv_data = (
@ -169,12 +175,15 @@ class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
for location in locations:
location.save()
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'name': 'Location X',
'slug': 'location-x',
'site': site.pk,
'tenant': tenant.pk,
'description': 'A new location',
'tags': [t.pk for t in tags],
}
cls.csv_data = (
@ -201,11 +210,14 @@ class RackRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
RackRole(name='Rack Role 3', slug='rack-role-3'),
])
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'name': 'Rack Role X',
'slug': 'rack-role-x',
'color': 'c0c0c0',
'description': 'New role',
'tags': [t.pk for t in tags],
}
cls.csv_data = (
@ -368,10 +380,13 @@ class ManufacturerTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
Manufacturer(name='Manufacturer 3', slug='manufacturer-3'),
])
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'name': 'Manufacturer X',
'slug': 'manufacturer-x',
'description': 'A new manufacturer',
'tags': [t.pk for t in tags],
}
cls.csv_data = (
@ -1034,12 +1049,15 @@ class DeviceRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
DeviceRole(name='Device Role 3', slug='device-role-3'),
])
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'name': 'Devie Role X',
'slug': 'device-role-x',
'color': 'c0c0c0',
'vm_role': False,
'description': 'New device role',
'tags': [t.pk for t in tags],
}
cls.csv_data = (
@ -1069,6 +1087,8 @@ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturer),
])
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'name': 'Platform X',
'slug': 'platform-x',
@ -1076,6 +1096,7 @@ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
'napalm_driver': 'junos',
'napalm_args': None,
'description': 'A new platform',
'tags': [t.pk for t in tags],
}
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.models import *
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
from netbox.api.serializers import OrganizationalModelSerializer
from netbox.api.serializers import PrimaryModelSerializer
from tenancy.api.nested_serializers import NestedTenantSerializer
from utilities.api import get_serializer_for_model
@ -66,14 +65,14 @@ class RouteTargetSerializer(PrimaryModelSerializer):
# RIRs/aggregates
#
class RIRSerializer(OrganizationalModelSerializer):
class RIRSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
aggregate_count = serializers.IntegerField(read_only=True)
class Meta:
model = RIR
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',
]
@ -97,7 +96,7 @@ class AggregateSerializer(PrimaryModelSerializer):
# VLANs
#
class RoleSerializer(OrganizationalModelSerializer):
class RoleSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
prefix_count = serializers.IntegerField(read_only=True)
vlan_count = serializers.IntegerField(read_only=True)
@ -105,12 +104,12 @@ class RoleSerializer(OrganizationalModelSerializer):
class Meta:
model = Role
fields = [
'id', 'url', 'display', 'name', 'slug', 'weight', 'description', 'custom_fields', 'created', 'last_updated',
'prefix_count', 'vlan_count',
'id', 'url', 'display', 'name', 'slug', 'weight', 'description', 'tags', 'custom_fields', 'created',
'last_updated', 'prefix_count', 'vlan_count',
]
class VLANGroupSerializer(OrganizationalModelSerializer):
class VLANGroupSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
scope_type = ContentTypeField(
queryset=ContentType.objects.filter(
@ -126,8 +125,8 @@ class VLANGroupSerializer(OrganizationalModelSerializer):
class Meta:
model = VLANGroup
fields = [
'id', 'url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'description', 'custom_fields',
'created', 'last_updated', 'vlan_count',
'id', 'url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'description', 'tags',
'custom_fields', 'created', 'last_updated', 'vlan_count',
]
validators = []

View File

@ -48,7 +48,7 @@ class RouteTargetViewSet(CustomFieldModelViewSet):
class RIRViewSet(CustomFieldModelViewSet):
queryset = RIR.objects.annotate(
aggregate_count=count_related(Aggregate, 'rir')
)
).prefetch_related('tags')
serializer_class = serializers.RIRSerializer
filterset_class = filtersets.RIRFilterSet
@ -71,7 +71,7 @@ class RoleViewSet(CustomFieldModelViewSet):
queryset = Role.objects.annotate(
prefix_count=count_related(Prefix, 'role'),
vlan_count=count_related(VLAN, 'role')
)
).prefetch_related('tags')
serializer_class = serializers.RoleSerializer
filterset_class = filtersets.RoleFilterSet
@ -126,7 +126,7 @@ class IPAddressViewSet(CustomFieldModelViewSet):
class VLANGroupViewSet(CustomFieldModelViewSet):
queryset = VLANGroup.objects.annotate(
vlan_count=count_related(VLAN, 'group')
)
).prefetch_related('tags')
serializer_class = serializers.VLANGroupSerializer
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(
queryset=RIR.objects.all(),
widget=forms.MultipleHiddenInput
@ -120,7 +120,7 @@ class AggregateBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelB
}
class RoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
class RoleBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=Role.objects.all(),
widget=forms.MultipleHiddenInput
@ -280,7 +280,7 @@ class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelB
]
class VLANGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
class VLANGroupBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=VLANGroup.objects.all(),
widget=forms.MultipleHiddenInput

View File

@ -82,11 +82,15 @@ class RouteTargetForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
class RIRForm(BootstrapMixin, CustomFieldModelForm):
slug = SlugField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta:
model = RIR
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):
slug = SlugField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta:
model = Role
fields = [
'name', 'slug', 'weight', 'description',
'name', 'slug', 'weight', 'description', 'tags',
]
@ -530,15 +538,19 @@ class VLANGroupForm(BootstrapMixin, CustomFieldModelForm):
}
)
slug = SlugField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta:
model = VLANGroup
fields = [
'name', 'slug', 'description', 'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack',
'clustergroup', 'cluster',
'clustergroup', 'cluster', 'tags',
]
fieldsets = (
('VLAN Group', ('name', 'slug', 'description')),
('VLAN Group', ('name', 'slug', 'description', 'tags')),
('Scope', ('scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster')),
)
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):
"""
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)
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Role(OrganizationalModel):
"""
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):
"""
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'},
verbose_name='Aggregates'
)
tags = TagColumn(
url_name='ipam:rir_list'
)
actions = ButtonsColumn(RIR)
class Meta(BaseTable.Meta):
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')
@ -144,11 +147,14 @@ class RoleTable(BaseTable):
url_params={'role_id': 'pk'},
verbose_name='VLANs'
)
tags = TagColumn(
url_name='ipam:role_list'
)
actions = ButtonsColumn(Role)
class Meta(BaseTable.Meta):
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')

View File

@ -74,6 +74,9 @@ class VLANGroupTable(BaseTable):
url_params={'group_id': 'pk'},
verbose_name='VLANs'
)
tags = TagColumn(
url_name='ipam:vlangroup_list'
)
actions = ButtonsColumn(
model=VLANGroup,
prepend_template=VLANGROUP_ADD_VLAN
@ -81,7 +84,7 @@ class VLANGroupTable(BaseTable):
class Meta(BaseTable.Meta):
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')

View File

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

View File

@ -147,13 +147,6 @@ class NestedTagSerializer(WritableNestedSerializer):
# Base model serializers
#
class OrganizationalModelSerializer(CustomFieldModelSerializer):
"""
Adds support for custom fields.
"""
pass
class PrimaryModelSerializer(CustomFieldModelSerializer):
"""
Adds support for custom fields and tags.
@ -189,9 +182,9 @@ class PrimaryModelSerializer(CustomFieldModelSerializer):
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)

View File

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

View File

@ -143,6 +143,18 @@ class CustomValidationMixin(models.Model):
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
@ -166,7 +178,7 @@ class ChangeLoggedModel(ChangeLoggingMixin, CustomValidationMixin, BigIDModel):
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.
"""
@ -175,15 +187,12 @@ class PrimaryModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin,
object_id_field='assigned_object_id',
content_type_field='assigned_object_type'
)
tags = TaggableManager(
through='extras.TaggedItem'
)
class Meta:
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
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
any real information about the infrastructure being modeled (for example, functional device roles). Organizational

View File

@ -65,7 +65,7 @@
</div>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' with tags=object.tags.all url='circuits:circuit_list' %}
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
</div>

View File

@ -28,6 +28,7 @@
</table>
</div>
</div>
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">

View File

@ -47,7 +47,7 @@
</table>
</div>
</div>
{% include 'inc/panels/tags.html' with tags=object.tags.all url='circuits:provider_list' %}
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">

View File

@ -38,7 +38,7 @@
</div>
<div class="col col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' with tags=object.tags.all url='circuits:providernetwork_list' %}
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_right_page object %}
</div>

View File

@ -64,7 +64,7 @@
</div>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' with tags=object.tags.all url='dcim:cable_list' %}
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">

View File

@ -41,7 +41,7 @@
</div>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' with tags=object.tags.all %}
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">

View File

@ -41,7 +41,7 @@
</div>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' with tags=object.tags.all %}
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">

View File

@ -221,7 +221,7 @@
</div>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' with tags=object.tags.all url='dcim:device_list' %}
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
</div>

View File

@ -33,7 +33,7 @@
</div>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' with tags=object.tags.all %}
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">

View File

@ -58,6 +58,7 @@
</table>
</div>
</div>
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">

View File

@ -88,7 +88,7 @@
</div>
<div class="col col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' with tags=object.tags.all url='dcim:devicetype_list' %}
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_right_page object %}
</div>

View File

@ -53,7 +53,7 @@
</div>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' with tags=object.tags.all %}
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">

View File

@ -103,7 +103,7 @@
</div>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' with tags=object.tags.all %}
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">

View File

@ -65,7 +65,7 @@
</div>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' with tags=object.tags.all %}
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">

View File

@ -68,6 +68,7 @@
</table>
</div>
</div>
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">

View File

@ -34,6 +34,7 @@
</table>
</div>
</div>
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">

View File

@ -55,6 +55,7 @@
</table>
</div>
</div>
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">

View File

@ -108,7 +108,7 @@
</div>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' with tags=object.tags.all url='dcim:powerfeed_list' %}
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">

View File

@ -45,7 +45,7 @@
</div>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' with tags=object.tags.all %}
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">

View File

@ -39,7 +39,7 @@
</table>
</div>
</div>
{% include 'inc/panels/tags.html' with tags=object.tags.all url='dcim:powerpanel_list' %}
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">

View File

@ -45,7 +45,7 @@
</div>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' with tags=object.tags.all %}
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">

View File

@ -163,7 +163,7 @@
</div>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' with tags=object.tags.all url='dcim:rack_list' %}
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/comments.html' %}
{% if power_feeds %}
<div class="card">

View File

@ -84,7 +84,7 @@
</div>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' with tags=object.tags.all url='dcim:rackreservation_list' %}
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-12 col-xl-7">

View File

@ -34,6 +34,7 @@
</table>
</div>
</div>
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">

View File

@ -47,7 +47,7 @@
</div>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' with tags=object.tags.all %}
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">

View File

@ -45,6 +45,7 @@
</table>
</div>
</div>
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/contacts.html' %}
{% plugin_left_page object %}

View File

@ -169,7 +169,6 @@
<div class="float-end text-warning">
<i class="mdi mdi-alert" title="{{ deprecation_warning }}"></i>
</div>
<a href="tel:{{ object.contact_
<a href="mailto:{{ object.contact_email }}">{{ object.contact_email }}</a>
{% else %}
<span class="text-muted">&mdash;</span>
@ -181,7 +180,7 @@
</div>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' with tags=object.tags.all url='dcim:site_list' %}
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
</div>

View File

@ -45,6 +45,7 @@
</table>
</div>
</div>
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/contacts.html' %}
{% plugin_left_page object %}

View File

@ -39,7 +39,7 @@
</div>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' with tags=object.tags.all url='dcim:virtualchassis_list' %}
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-8">

View File

@ -1,11 +1,14 @@
{% load helpers %}
<div class="card">
<h5 class="card-header">
Tags
</h5>
<h5 class="card-header">Tags</h5>
<div class="card-body">
{% for tag in tags.all %} {% tag tag url %} {% empty %}
<span class="text-muted">No tags assigned</span>
{% endfor %}
{% with url=object|validated_viewname:"list" %}
{% for tag in object.tags.all %}
{% tag tag url %}
{% empty %}
<span class="text-muted">No tags assigned</span>
{% endfor %}
{% endwith %}
</div>
</div>

View File

@ -65,7 +65,7 @@
</div>
<div class="col col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:aggregate_list' %}
{% include 'inc/panels/tags.html' %}
{% plugin_right_page object %}
</div>
</div>

View File

@ -145,7 +145,7 @@
<div class="row my-3">
<div class="col col-md-4">
{% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:ipaddress_list' %}
{% include 'inc/panels/tags.html' %}
</div>
</div>

View File

@ -82,7 +82,7 @@
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
{% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:prefix_list' %}
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% plugin_right_page object %}
</div>

View File

@ -122,7 +122,7 @@
</div>
<div class="col col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:prefix_list' %}
{% include 'inc/panels/tags.html' %}
{% plugin_right_page object %}
</div>
</div>

View File

@ -38,6 +38,7 @@
</table>
</div>
</div>
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">

View File

@ -32,6 +32,7 @@
</table>
</div>
</div>
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">

View File

@ -30,7 +30,7 @@
</table>
</div>
</div>
{% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:routetarget_list' %}
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% plugin_left_page object %}
</div>

View File

@ -61,7 +61,7 @@
</div>
<div class="col col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:service_list' %}
{% include 'inc/panels/tags.html' %}
{% plugin_right_page object %}
</div>
</div>

View File

@ -83,7 +83,7 @@
</div>
<div class="col col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:vlan_list' %}
{% include 'inc/panels/tags.html' %}
{% plugin_right_page object %}
</div>
</div>

View File

@ -54,6 +54,7 @@
</table>
</div>
</div>
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">

View File

@ -60,7 +60,7 @@
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
{% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:vrf_list' %}
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% plugin_right_page object %}
</div>

View File

@ -60,7 +60,7 @@
</div>
<div class="col col-md-5">
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' with tags=object.tags.all url='tenancy:tenant_list' %}
{% include 'inc/panels/tags.html' %}
{% plugin_right_page object %}
</div>
</div>

View File

@ -45,6 +45,7 @@
</table>
</div>
</div>
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">

View File

@ -30,6 +30,7 @@
</table>
</div>
</div>
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">

View File

@ -36,7 +36,7 @@
</div>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' with tags=object.tags.all url='tenancy:tenant_list' %}
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/comments.html' %}
{% include 'inc/panels/contacts.html' %}
{% plugin_left_page object %}

View File

@ -45,6 +45,7 @@
</table>
</div>
</div>
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">

View File

@ -61,7 +61,7 @@
</div>
<div class="col col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' with tags=object.tags.all url='virtualization:cluster_list' %}
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/contacts.html' %}
{% plugin_right_page object %}
</div>

View File

@ -28,6 +28,7 @@
</table>
</div>
</div>
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">

View File

@ -28,6 +28,7 @@
</table>
</div>
</div>
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">

View File

@ -90,7 +90,7 @@
</div>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' with tags=object.tags.all url='virtualization:virtualmachine_list' %}
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
</div>

View File

@ -70,8 +70,8 @@
</div>
<div class="col col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' with tags=object.tags.all %}
{% plugin_right_page object %}
{% include 'inc/panels/tags.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row mb-3">

View File

@ -2,7 +2,7 @@ from django.contrib.auth.models import ContentType
from rest_framework import serializers
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.models import *
from .nested_serializers import *
@ -20,8 +20,8 @@ class TenantGroupSerializer(NestedGroupModelSerializer):
class Meta:
model = TenantGroup
fields = [
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'custom_fields', 'created', 'last_updated',
'tenant_count', '_depth',
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created',
'last_updated', 'tenant_count', '_depth',
]
@ -60,18 +60,18 @@ class ContactGroupSerializer(NestedGroupModelSerializer):
class Meta:
model = ContactGroup
fields = [
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'custom_fields', 'created', 'last_updated',
'contact_count', '_depth',
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created',
'last_updated', 'contact_count', '_depth',
]
class ContactRoleSerializer(OrganizationalModelSerializer):
class ContactRoleSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactrole-detail')
class Meta:
model = ContactRole
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',
'tenant_count',
cumulative=True
)
).prefetch_related('tags')
serializer_class = serializers.TenantGroupSerializer
filterset_class = filtersets.TenantGroupFilterSet
@ -64,28 +64,24 @@ class ContactGroupViewSet(CustomFieldModelViewSet):
'group',
'contact_count',
cumulative=True
)
).prefetch_related('tags')
serializer_class = serializers.ContactGroupSerializer
filterset_class = filtersets.ContactGroupFilterSet
class ContactRoleViewSet(CustomFieldModelViewSet):
queryset = ContactRole.objects.all()
queryset = ContactRole.objects.prefetch_related('tags')
serializer_class = serializers.ContactRoleSerializer
filterset_class = filtersets.ContactRoleFilterSet
class ContactViewSet(CustomFieldModelViewSet):
queryset = Contact.objects.prefetch_related(
'group', 'tags'
)
queryset = Contact.objects.prefetch_related('group', 'tags')
serializer_class = serializers.ContactSerializer
filterset_class = filtersets.ContactFilterSet
class ContactAssignmentViewSet(CustomFieldModelViewSet):
queryset = ContactAssignment.objects.prefetch_related(
'contact', 'role'
)
queryset = ContactAssignment.objects.prefetch_related('contact', 'role')
serializer_class = serializers.ContactAssignmentSerializer
filterset_class = filtersets.ContactAssignmentFilterSet

View File

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

View File

@ -28,11 +28,15 @@ class TenantGroupForm(BootstrapMixin, CustomFieldModelForm):
required=False
)
slug = SlugField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta:
model = TenantGroup
fields = [
'parent', 'name', 'slug', 'description',
'parent', 'name', 'slug', 'description', 'tags',
]
@ -68,18 +72,26 @@ class ContactGroupForm(BootstrapMixin, CustomFieldModelForm):
required=False
)
slug = SlugField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta:
model = ContactGroup
fields = ['parent', 'name', 'slug', 'description']
fields = ('parent', 'name', 'slug', 'description', 'tags')
class ContactRoleForm(BootstrapMixin, CustomFieldModelForm):
slug = SlugField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta:
model = ContactRole
fields = ['name', 'slug', 'description']
fields = ('name', 'slug', 'description', 'tags')
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
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class TenantGroup(NestedGroupModel):
"""
An arbitrary collection of Tenants.
@ -111,7 +111,7 @@ class Tenant(PrimaryModel):
# Contacts
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class ContactGroup(NestedGroupModel):
"""
An arbitrary collection of Contacts.
@ -145,7 +145,7 @@ class ContactGroup(NestedGroupModel):
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):
"""
Functional role for a Contact assigned to an object.

View File

@ -55,11 +55,14 @@ class TenantGroupTable(BaseTable):
url_params={'group_id': 'pk'},
verbose_name='Tenants'
)
tags = TagColumn(
url_name='tenancy:tenantgroup_list'
)
actions = ButtonsColumn(TenantGroup)
class Meta(BaseTable.Meta):
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')
@ -96,11 +99,14 @@ class ContactGroupTable(BaseTable):
url_params={'role_id': 'pk'},
verbose_name='Contacts'
)
tags = TagColumn(
url_name='tenancy:contactgroup_list'
)
actions = ButtonsColumn(ContactGroup)
class Meta(BaseTable.Meta):
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')

View File

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

View File

@ -6,7 +6,7 @@ from dcim.choices import InterfaceModeChoices
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
from ipam.models import VLAN
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 virtualization.choices import *
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
@ -17,26 +17,26 @@ from .nested_serializers import *
# Clusters
#
class ClusterTypeSerializer(OrganizationalModelSerializer):
class ClusterTypeSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail')
cluster_count = serializers.IntegerField(read_only=True)
class Meta:
model = ClusterType
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',
]
class ClusterGroupSerializer(OrganizationalModelSerializer):
class ClusterGroupSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail')
cluster_count = serializers.IntegerField(read_only=True)
class Meta:
model = ClusterGroup
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',
]

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More