mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 01:41:22 -06:00
Merge pull request #7610 from netbox-community/6497-organizational-model-tags
Closes #6297: Extend tag support to organizational models
This commit is contained in:
commit
001ab1d067
@ -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: | | | | |
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
]
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
]
|
||||
|
||||
|
||||
|
20
netbox/circuits/migrations/0003_extend_tag_support.py
Normal file
20
netbox/circuits/migrations/0003_extend_tag_support.py
Normal 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'),
|
||||
),
|
||||
]
|
@ -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
|
||||
|
@ -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')
|
||||
|
||||
|
||||
|
@ -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 = (
|
||||
|
@ -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',
|
||||
]
|
||||
|
||||
|
||||
|
@ -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')
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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(),
|
||||
|
50
netbox/dcim/migrations/0138_extend_tag_support.py
Normal file
50
netbox/dcim/migrations/0138_extend_tag_support.py
Normal 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'),
|
||||
),
|
||||
]
|
@ -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".
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
)
|
||||
|
||||
|
||||
|
@ -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')
|
||||
|
||||
|
||||
|
@ -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')
|
||||
|
@ -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 = (
|
||||
|
@ -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 = []
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 = {
|
||||
|
30
netbox/ipam/migrations/0051_extend_tag_support.py
Normal file
30
netbox/ipam/migrations/0051_extend_tag_support.py
Normal 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'),
|
||||
),
|
||||
]
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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')
|
||||
|
||||
|
||||
|
@ -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')
|
||||
|
||||
|
||||
|
@ -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 = (
|
||||
|
@ -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)
|
||||
|
||||
|
@ -41,6 +41,7 @@ class ObjectType(
|
||||
class OrganizationalObjectType(
|
||||
ChangelogMixin,
|
||||
CustomFieldsMixin,
|
||||
TagsMixin,
|
||||
BaseObjectType
|
||||
):
|
||||
"""
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -28,6 +28,7 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -58,6 +58,7 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -68,6 +68,7 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
|
@ -34,6 +34,7 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
|
@ -55,6 +55,7 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -34,6 +34,7 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
|
@ -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">
|
||||
|
@ -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 %}
|
||||
|
@ -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">—</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>
|
||||
|
@ -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 %}
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -38,6 +38,7 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
|
@ -32,6 +32,7 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -54,6 +54,7 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -45,6 +45,7 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
|
@ -30,6 +30,7 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
|
@ -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 %}
|
||||
|
@ -45,6 +45,7 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
|
@ -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>
|
||||
|
@ -28,6 +28,7 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
|
@ -28,6 +28,7 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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',
|
||||
]
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
30
netbox/tenancy/migrations/0004_extend_tag_support.py
Normal file
30
netbox/tenancy/migrations/0004_extend_tag_support.py
Normal 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'),
|
||||
),
|
||||
]
|
@ -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.
|
||||
|
@ -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')
|
||||
|
||||
|
||||
|
@ -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 = (
|
||||
|
@ -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',
|
||||
]
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user