mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Add tags to organizational & nested group models
This commit is contained in:
@ -19,8 +19,8 @@ The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/
|
|||||||
| Type | Change Logging | Webhooks | Custom Fields | Export Templates | Tags | Journaling | Nesting |
|
| Type | Change Logging | Webhooks | Custom Fields | Export Templates | Tags | Journaling | Nesting |
|
||||||
| ------------------ | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- |
|
| ------------------ | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- |
|
||||||
| Primary | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | |
|
| Primary | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | |
|
||||||
| Organizational | :material-check: | :material-check: | :material-check: | :material-check: | | | |
|
| Organizational | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | | |
|
||||||
| Nested Group | :material-check: | :material-check: | :material-check: | :material-check: | | | :material-check: |
|
| Nested Group | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | | :material-check: |
|
||||||
| Component | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | | |
|
| Component | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | | |
|
||||||
| Component Template | :material-check: | :material-check: | :material-check: | | | | |
|
| Component Template | :material-check: | :material-check: | :material-check: | | | | |
|
||||||
|
|
||||||
|
@ -15,6 +15,3 @@ The `tag` filter can be specified multiple times to match only objects which hav
|
|||||||
```no-highlight
|
```no-highlight
|
||||||
GET /api/dcim/devices/?tag=monitored&tag=deprecated
|
GET /api/dcim/devices/?tag=monitored&tag=deprecated
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! note
|
|
||||||
Tags have changed substantially in NetBox v2.9. They are no longer created on-demand when editing an object, and their representation in the REST API now includes a complete depiction of the tag rather than only its label.
|
|
||||||
|
@ -5,9 +5,7 @@ from circuits.models import *
|
|||||||
from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
|
from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
|
||||||
from dcim.api.serializers import CableTerminationSerializer
|
from dcim.api.serializers import CableTerminationSerializer
|
||||||
from netbox.api import ChoiceField
|
from netbox.api import ChoiceField
|
||||||
from netbox.api.serializers import (
|
from netbox.api.serializers import PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer
|
||||||
OrganizationalModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer
|
|
||||||
)
|
|
||||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||||
from .nested_serializers import *
|
from .nested_serializers import *
|
||||||
|
|
||||||
@ -48,14 +46,14 @@ class ProviderNetworkSerializer(PrimaryModelSerializer):
|
|||||||
# Circuits
|
# Circuits
|
||||||
#
|
#
|
||||||
|
|
||||||
class CircuitTypeSerializer(OrganizationalModelSerializer):
|
class CircuitTypeSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
|
||||||
circuit_count = serializers.IntegerField(read_only=True)
|
circuit_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitType
|
model = CircuitType
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'slug', 'description', 'custom_fields', 'created', 'last_updated',
|
'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
'circuit_count',
|
'circuit_count',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ class ProviderViewSet(CustomFieldModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class CircuitTypeViewSet(CustomFieldModelViewSet):
|
class CircuitTypeViewSet(CustomFieldModelViewSet):
|
||||||
queryset = CircuitType.objects.annotate(
|
queryset = CircuitType.objects.prefetch_related('tags').annotate(
|
||||||
circuit_count=count_related(Circuit, 'type')
|
circuit_count=count_related(Circuit, 'type')
|
||||||
)
|
)
|
||||||
serializer_class = serializers.CircuitTypeSerializer
|
serializer_class = serializers.CircuitTypeSerializer
|
||||||
|
@ -79,7 +79,7 @@ class ProviderNetworkBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomField
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class CircuitTypeBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
class CircuitTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=CircuitType.objects.all(),
|
queryset=CircuitType.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
|
@ -75,11 +75,15 @@ class ProviderNetworkForm(BootstrapMixin, CustomFieldModelForm):
|
|||||||
|
|
||||||
class CircuitTypeForm(BootstrapMixin, CustomFieldModelForm):
|
class CircuitTypeForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitType
|
model = CircuitType
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug', 'description',
|
'name', 'slug', 'description', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
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])
|
return reverse('circuits:providernetwork', args=[self.pk])
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class CircuitType(OrganizationalModel):
|
class CircuitType(OrganizationalModel):
|
||||||
"""
|
"""
|
||||||
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
|
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
|
||||||
|
@ -82,6 +82,9 @@ class CircuitTypeTable(BaseTable):
|
|||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='circuits:circuittype_list'
|
||||||
|
)
|
||||||
circuit_count = tables.Column(
|
circuit_count = tables.Column(
|
||||||
verbose_name='Circuits'
|
verbose_name='Circuits'
|
||||||
)
|
)
|
||||||
@ -89,7 +92,7 @@ class CircuitTypeTable(BaseTable):
|
|||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = CircuitType
|
model = CircuitType
|
||||||
fields = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions')
|
fields = ('pk', 'name', 'circuit_count', 'description', 'slug', 'tags', 'actions')
|
||||||
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions')
|
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions')
|
||||||
|
|
||||||
|
|
||||||
|
@ -64,10 +64,13 @@ class CircuitTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
CircuitType(name='Circuit Type 3', slug='circuit-type-3'),
|
CircuitType(name='Circuit Type 3', slug='circuit-type-3'),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'Circuit Type X',
|
'name': 'Circuit Type X',
|
||||||
'slug': 'circuit-type-x',
|
'slug': 'circuit-type-x',
|
||||||
'description': 'A new circuit type',
|
'description': 'A new circuit type',
|
||||||
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
|
@ -11,8 +11,7 @@ from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSer
|
|||||||
from ipam.models import VLAN
|
from ipam.models import VLAN
|
||||||
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
||||||
from netbox.api.serializers import (
|
from netbox.api.serializers import (
|
||||||
NestedGroupModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer,
|
NestedGroupModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer,
|
||||||
WritableNestedSerializer,
|
|
||||||
)
|
)
|
||||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||||
from users.api.nested_serializers import NestedUserSerializer
|
from users.api.nested_serializers import NestedUserSerializer
|
||||||
@ -87,8 +86,8 @@ class RegionSerializer(NestedGroupModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Region
|
model = Region
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'custom_fields', 'created', 'last_updated',
|
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created',
|
||||||
'site_count', '_depth',
|
'last_updated', 'site_count', '_depth',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -100,8 +99,8 @@ class SiteGroupSerializer(NestedGroupModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = SiteGroup
|
model = SiteGroup
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'custom_fields', 'created', 'last_updated',
|
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created',
|
||||||
'site_count', '_depth',
|
'last_updated', 'site_count', '_depth',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -144,20 +143,20 @@ class LocationSerializer(NestedGroupModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Location
|
model = Location
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'tenant', 'description', 'custom_fields',
|
'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'tenant', 'description', 'tags', 'custom_fields',
|
||||||
'created', 'last_updated', 'rack_count', 'device_count', '_depth',
|
'created', 'last_updated', 'rack_count', 'device_count', '_depth',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class RackRoleSerializer(OrganizationalModelSerializer):
|
class RackRoleSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
|
||||||
rack_count = serializers.IntegerField(read_only=True)
|
rack_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RackRole
|
model = RackRole
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'custom_fields', 'created', 'last_updated',
|
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created',
|
||||||
'rack_count',
|
'last_updated', 'rack_count',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -254,7 +253,7 @@ class RackElevationDetailFilterSerializer(serializers.Serializer):
|
|||||||
# Device types
|
# Device types
|
||||||
#
|
#
|
||||||
|
|
||||||
class ManufacturerSerializer(OrganizationalModelSerializer):
|
class ManufacturerSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
|
||||||
devicetype_count = serializers.IntegerField(read_only=True)
|
devicetype_count = serializers.IntegerField(read_only=True)
|
||||||
inventoryitem_count = serializers.IntegerField(read_only=True)
|
inventoryitem_count = serializers.IntegerField(read_only=True)
|
||||||
@ -263,7 +262,7 @@ class ManufacturerSerializer(OrganizationalModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Manufacturer
|
model = Manufacturer
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'slug', 'description', 'custom_fields', 'created', 'last_updated',
|
'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
'devicetype_count', 'inventoryitem_count', 'platform_count',
|
'devicetype_count', 'inventoryitem_count', 'platform_count',
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -411,7 +410,7 @@ class DeviceBayTemplateSerializer(ValidatedModelSerializer):
|
|||||||
# Devices
|
# Devices
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceRoleSerializer(OrganizationalModelSerializer):
|
class DeviceRoleSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
|
||||||
device_count = serializers.IntegerField(read_only=True)
|
device_count = serializers.IntegerField(read_only=True)
|
||||||
virtualmachine_count = serializers.IntegerField(read_only=True)
|
virtualmachine_count = serializers.IntegerField(read_only=True)
|
||||||
@ -419,12 +418,12 @@ class DeviceRoleSerializer(OrganizationalModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceRole
|
model = DeviceRole
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'slug', 'color', 'vm_role', 'description', 'custom_fields', 'created',
|
'id', 'url', 'display', 'name', 'slug', 'color', 'vm_role', 'description', 'tags', 'custom_fields',
|
||||||
'last_updated', 'device_count', 'virtualmachine_count',
|
'created', 'last_updated', 'device_count', 'virtualmachine_count',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class PlatformSerializer(OrganizationalModelSerializer):
|
class PlatformSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
|
||||||
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True)
|
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True)
|
||||||
device_count = serializers.IntegerField(read_only=True)
|
device_count = serializers.IntegerField(read_only=True)
|
||||||
@ -434,7 +433,7 @@ class PlatformSerializer(OrganizationalModelSerializer):
|
|||||||
model = Platform
|
model = Platform
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description',
|
'id', 'url', 'display', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description',
|
||||||
'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
|
'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -110,7 +110,7 @@ class RegionViewSet(CustomFieldModelViewSet):
|
|||||||
'region',
|
'region',
|
||||||
'site_count',
|
'site_count',
|
||||||
cumulative=True
|
cumulative=True
|
||||||
)
|
).prefetch_related('tags')
|
||||||
serializer_class = serializers.RegionSerializer
|
serializer_class = serializers.RegionSerializer
|
||||||
filterset_class = filtersets.RegionFilterSet
|
filterset_class = filtersets.RegionFilterSet
|
||||||
|
|
||||||
@ -126,7 +126,7 @@ class SiteGroupViewSet(CustomFieldModelViewSet):
|
|||||||
'group',
|
'group',
|
||||||
'site_count',
|
'site_count',
|
||||||
cumulative=True
|
cumulative=True
|
||||||
)
|
).prefetch_related('tags')
|
||||||
serializer_class = serializers.SiteGroupSerializer
|
serializer_class = serializers.SiteGroupSerializer
|
||||||
filterset_class = filtersets.SiteGroupFilterSet
|
filterset_class = filtersets.SiteGroupFilterSet
|
||||||
|
|
||||||
@ -167,7 +167,7 @@ class LocationViewSet(CustomFieldModelViewSet):
|
|||||||
'location',
|
'location',
|
||||||
'rack_count',
|
'rack_count',
|
||||||
cumulative=True
|
cumulative=True
|
||||||
).prefetch_related('site')
|
).prefetch_related('site', 'tags')
|
||||||
serializer_class = serializers.LocationSerializer
|
serializer_class = serializers.LocationSerializer
|
||||||
filterset_class = filtersets.LocationFilterSet
|
filterset_class = filtersets.LocationFilterSet
|
||||||
|
|
||||||
@ -177,7 +177,7 @@ class LocationViewSet(CustomFieldModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class RackRoleViewSet(CustomFieldModelViewSet):
|
class RackRoleViewSet(CustomFieldModelViewSet):
|
||||||
queryset = RackRole.objects.annotate(
|
queryset = RackRole.objects.prefetch_related('tags').annotate(
|
||||||
rack_count=count_related(Rack, 'role')
|
rack_count=count_related(Rack, 'role')
|
||||||
)
|
)
|
||||||
serializer_class = serializers.RackRoleSerializer
|
serializer_class = serializers.RackRoleSerializer
|
||||||
@ -261,7 +261,7 @@ class RackReservationViewSet(ModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class ManufacturerViewSet(CustomFieldModelViewSet):
|
class ManufacturerViewSet(CustomFieldModelViewSet):
|
||||||
queryset = Manufacturer.objects.annotate(
|
queryset = Manufacturer.objects.prefetch_related('tags').annotate(
|
||||||
devicetype_count=count_related(DeviceType, 'manufacturer'),
|
devicetype_count=count_related(DeviceType, 'manufacturer'),
|
||||||
inventoryitem_count=count_related(InventoryItem, 'manufacturer'),
|
inventoryitem_count=count_related(InventoryItem, 'manufacturer'),
|
||||||
platform_count=count_related(Platform, 'manufacturer')
|
platform_count=count_related(Platform, 'manufacturer')
|
||||||
@ -340,7 +340,7 @@ class DeviceBayTemplateViewSet(ModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class DeviceRoleViewSet(CustomFieldModelViewSet):
|
class DeviceRoleViewSet(CustomFieldModelViewSet):
|
||||||
queryset = DeviceRole.objects.annotate(
|
queryset = DeviceRole.objects.prefetch_related('tags').annotate(
|
||||||
device_count=count_related(Device, 'device_role'),
|
device_count=count_related(Device, 'device_role'),
|
||||||
virtualmachine_count=count_related(VirtualMachine, 'role')
|
virtualmachine_count=count_related(VirtualMachine, 'role')
|
||||||
)
|
)
|
||||||
@ -353,7 +353,7 @@ class DeviceRoleViewSet(CustomFieldModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class PlatformViewSet(CustomFieldModelViewSet):
|
class PlatformViewSet(CustomFieldModelViewSet):
|
||||||
queryset = Platform.objects.annotate(
|
queryset = Platform.objects.prefetch_related('tags').annotate(
|
||||||
device_count=count_related(Device, 'platform'),
|
device_count=count_related(Device, 'platform'),
|
||||||
virtualmachine_count=count_related(VirtualMachine, 'platform')
|
virtualmachine_count=count_related(VirtualMachine, 'platform')
|
||||||
)
|
)
|
||||||
|
@ -51,7 +51,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class RegionBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
class RegionBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
@ -69,7 +69,7 @@ class RegionBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
|||||||
nullable_fields = ['parent', 'description']
|
nullable_fields = ['parent', 'description']
|
||||||
|
|
||||||
|
|
||||||
class SiteGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
class SiteGroupBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=SiteGroup.objects.all(),
|
queryset=SiteGroup.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
@ -132,7 +132,7 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEd
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class LocationBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
class LocationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=Location.objects.all(),
|
queryset=Location.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
@ -161,7 +161,7 @@ class LocationBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
|||||||
nullable_fields = ['parent', 'tenant', 'description']
|
nullable_fields = ['parent', 'tenant', 'description']
|
||||||
|
|
||||||
|
|
||||||
class RackRoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
class RackRoleBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=RackRole.objects.all(),
|
queryset=RackRole.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
@ -303,7 +303,7 @@ class RackReservationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomField
|
|||||||
nullable_fields = []
|
nullable_fields = []
|
||||||
|
|
||||||
|
|
||||||
class ManufacturerBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
class ManufacturerBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
@ -345,7 +345,7 @@ class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModel
|
|||||||
nullable_fields = ['airflow']
|
nullable_fields = ['airflow']
|
||||||
|
|
||||||
|
|
||||||
class DeviceRoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
class DeviceRoleBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=DeviceRole.objects.all(),
|
queryset=DeviceRole.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
@ -367,7 +367,7 @@ class DeviceRoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
|||||||
nullable_fields = ['color', 'description']
|
nullable_fields = ['color', 'description']
|
||||||
|
|
||||||
|
|
||||||
class PlatformBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
class PlatformBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=Platform.objects.all(),
|
queryset=Platform.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
|
@ -70,11 +70,15 @@ class RegionForm(BootstrapMixin, CustomFieldModelForm):
|
|||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Region
|
model = Region
|
||||||
fields = (
|
fields = (
|
||||||
'parent', 'name', 'slug', 'description',
|
'parent', 'name', 'slug', 'description', 'tags',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -84,11 +88,15 @@ class SiteGroupForm(BootstrapMixin, CustomFieldModelForm):
|
|||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SiteGroup
|
model = SiteGroup
|
||||||
fields = (
|
fields = (
|
||||||
'parent', 'name', 'slug', 'description',
|
'parent', 'name', 'slug', 'description', 'tags',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -187,15 +195,19 @@ class LocationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Location
|
model = Location
|
||||||
fields = (
|
fields = (
|
||||||
'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description', 'tenant_group', 'tenant',
|
'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description', 'tenant_group', 'tenant', 'tags',
|
||||||
)
|
)
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Location', (
|
('Location', (
|
||||||
'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description',
|
'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description', 'tags',
|
||||||
)),
|
)),
|
||||||
('Tenancy', ('tenant_group', 'tenant')),
|
('Tenancy', ('tenant_group', 'tenant')),
|
||||||
)
|
)
|
||||||
@ -203,11 +215,15 @@ class LocationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
|
|
||||||
class RackRoleForm(BootstrapMixin, CustomFieldModelForm):
|
class RackRoleForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RackRole
|
model = RackRole
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug', 'color', 'description',
|
'name', 'slug', 'color', 'description', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -343,11 +359,15 @@ class RackReservationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
|
|
||||||
class ManufacturerForm(BootstrapMixin, CustomFieldModelForm):
|
class ManufacturerForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Manufacturer
|
model = Manufacturer
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug', 'description',
|
'name', 'slug', 'description', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -392,11 +412,15 @@ class DeviceTypeForm(BootstrapMixin, CustomFieldModelForm):
|
|||||||
|
|
||||||
class DeviceRoleForm(BootstrapMixin, CustomFieldModelForm):
|
class DeviceRoleForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceRole
|
model = DeviceRole
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug', 'color', 'vm_role', 'description',
|
'name', 'slug', 'color', 'vm_role', 'description', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -408,11 +432,15 @@ class PlatformForm(BootstrapMixin, CustomFieldModelForm):
|
|||||||
slug = SlugField(
|
slug = SlugField(
|
||||||
max_length=64
|
max_length=64
|
||||||
)
|
)
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Platform
|
model = Platform
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description',
|
'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'tags',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'napalm_args': SmallTextarea(),
|
'napalm_args': SmallTextarea(),
|
||||||
|
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
|
# Device Types
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class Manufacturer(OrganizationalModel):
|
class Manufacturer(OrganizationalModel):
|
||||||
"""
|
"""
|
||||||
A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
|
A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
|
||||||
@ -351,7 +351,7 @@ class DeviceType(PrimaryModel):
|
|||||||
# Devices
|
# Devices
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class DeviceRole(OrganizationalModel):
|
class DeviceRole(OrganizationalModel):
|
||||||
"""
|
"""
|
||||||
Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a
|
Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a
|
||||||
@ -391,7 +391,7 @@ class DeviceRole(OrganizationalModel):
|
|||||||
return reverse('dcim:devicerole', args=[self.pk])
|
return reverse('dcim:devicerole', args=[self.pk])
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class Platform(OrganizationalModel):
|
class Platform(OrganizationalModel):
|
||||||
"""
|
"""
|
||||||
Platform refers to the software or firmware running on a Device. For example, "Cisco IOS-XR" or "Juniper Junos".
|
Platform refers to the software or firmware running on a Device. For example, "Cisco IOS-XR" or "Juniper Junos".
|
||||||
|
@ -35,7 +35,7 @@ __all__ = (
|
|||||||
# Racks
|
# Racks
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class RackRole(OrganizationalModel):
|
class RackRole(OrganizationalModel):
|
||||||
"""
|
"""
|
||||||
Racks can be organized by functional role, similar to Devices.
|
Racks can be organized by functional role, similar to Devices.
|
||||||
|
@ -25,7 +25,7 @@ __all__ = (
|
|||||||
# Regions
|
# Regions
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class Region(NestedGroupModel):
|
class Region(NestedGroupModel):
|
||||||
"""
|
"""
|
||||||
A region represents a geographic collection of sites. For example, you might create regions representing countries,
|
A region represents a geographic collection of sites. For example, you might create regions representing countries,
|
||||||
@ -82,7 +82,7 @@ class Region(NestedGroupModel):
|
|||||||
# Site groups
|
# Site groups
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class SiteGroup(NestedGroupModel):
|
class SiteGroup(NestedGroupModel):
|
||||||
"""
|
"""
|
||||||
A site group is an arbitrary grouping of sites. For example, you might have corporate sites and customer sites; and
|
A site group is an arbitrary grouping of sites. For example, you might have corporate sites and customer sites; and
|
||||||
@ -278,7 +278,7 @@ class Site(PrimaryModel):
|
|||||||
# Locations
|
# Locations
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class Location(NestedGroupModel):
|
class Location(NestedGroupModel):
|
||||||
"""
|
"""
|
||||||
A Location represents a subgroup of Racks and/or Devices within a Site. A Location may represent a building within a
|
A Location represents a subgroup of Racks and/or Devices within a Site. A Location may represent a building within a
|
||||||
|
@ -84,11 +84,16 @@ class DeviceRoleTable(BaseTable):
|
|||||||
)
|
)
|
||||||
color = ColorColumn()
|
color = ColorColumn()
|
||||||
vm_role = BooleanColumn()
|
vm_role = BooleanColumn()
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:devicerole_list'
|
||||||
|
)
|
||||||
actions = ButtonsColumn(DeviceRole)
|
actions = ButtonsColumn(DeviceRole)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = DeviceRole
|
model = DeviceRole
|
||||||
fields = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'actions')
|
fields = (
|
||||||
|
'pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'tags', 'actions',
|
||||||
|
)
|
||||||
default_columns = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'actions')
|
default_columns = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'actions')
|
||||||
|
|
||||||
|
|
||||||
@ -111,13 +116,16 @@ class PlatformTable(BaseTable):
|
|||||||
url_params={'platform_id': 'pk'},
|
url_params={'platform_id': 'pk'},
|
||||||
verbose_name='VMs'
|
verbose_name='VMs'
|
||||||
)
|
)
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:platform_list'
|
||||||
|
)
|
||||||
actions = ButtonsColumn(Platform)
|
actions = ButtonsColumn(Platform)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Platform
|
model = Platform
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'napalm_args',
|
'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'napalm_args',
|
||||||
'description', 'actions',
|
'description', 'tags', 'actions',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'napalm_driver', 'description', 'actions',
|
'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'napalm_driver', 'description', 'actions',
|
||||||
|
@ -41,12 +41,16 @@ class ManufacturerTable(BaseTable):
|
|||||||
verbose_name='Platforms'
|
verbose_name='Platforms'
|
||||||
)
|
)
|
||||||
slug = tables.Column()
|
slug = tables.Column()
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:manufacturer_list'
|
||||||
|
)
|
||||||
actions = ButtonsColumn(Manufacturer)
|
actions = ButtonsColumn(Manufacturer)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Manufacturer
|
model = Manufacturer
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug', 'actions',
|
'pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug', 'tags',
|
||||||
|
'actions',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,11 +24,14 @@ class RackRoleTable(BaseTable):
|
|||||||
name = tables.Column(linkify=True)
|
name = tables.Column(linkify=True)
|
||||||
rack_count = tables.Column(verbose_name='Racks')
|
rack_count = tables.Column(verbose_name='Racks')
|
||||||
color = ColorColumn()
|
color = ColorColumn()
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:rackrole_list'
|
||||||
|
)
|
||||||
actions = ButtonsColumn(RackRole)
|
actions = ButtonsColumn(RackRole)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = RackRole
|
model = RackRole
|
||||||
fields = ('pk', 'name', 'rack_count', 'color', 'description', 'slug', 'actions')
|
fields = ('pk', 'name', 'rack_count', 'color', 'description', 'slug', 'tags', 'actions')
|
||||||
default_columns = ('pk', 'name', 'rack_count', 'color', 'description', 'actions')
|
default_columns = ('pk', 'name', 'rack_count', 'color', 'description', 'actions')
|
||||||
|
|
||||||
|
|
||||||
|
@ -29,11 +29,14 @@ class RegionTable(BaseTable):
|
|||||||
url_params={'region_id': 'pk'},
|
url_params={'region_id': 'pk'},
|
||||||
verbose_name='Sites'
|
verbose_name='Sites'
|
||||||
)
|
)
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:region_list'
|
||||||
|
)
|
||||||
actions = ButtonsColumn(Region)
|
actions = ButtonsColumn(Region)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Region
|
model = Region
|
||||||
fields = ('pk', 'name', 'slug', 'site_count', 'description', 'actions')
|
fields = ('pk', 'name', 'slug', 'site_count', 'description', 'tags', 'actions')
|
||||||
default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
|
default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
|
||||||
|
|
||||||
|
|
||||||
@ -51,11 +54,14 @@ class SiteGroupTable(BaseTable):
|
|||||||
url_params={'group_id': 'pk'},
|
url_params={'group_id': 'pk'},
|
||||||
verbose_name='Sites'
|
verbose_name='Sites'
|
||||||
)
|
)
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:sitegroup_list'
|
||||||
|
)
|
||||||
actions = ButtonsColumn(SiteGroup)
|
actions = ButtonsColumn(SiteGroup)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = SiteGroup
|
model = SiteGroup
|
||||||
fields = ('pk', 'name', 'slug', 'site_count', 'description', 'actions')
|
fields = ('pk', 'name', 'slug', 'site_count', 'description', 'tags', 'actions')
|
||||||
default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
|
default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
|
||||||
|
|
||||||
|
|
||||||
@ -114,6 +120,9 @@ class LocationTable(BaseTable):
|
|||||||
url_params={'location_id': 'pk'},
|
url_params={'location_id': 'pk'},
|
||||||
verbose_name='Devices'
|
verbose_name='Devices'
|
||||||
)
|
)
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:location_list'
|
||||||
|
)
|
||||||
actions = ButtonsColumn(
|
actions = ButtonsColumn(
|
||||||
model=Location,
|
model=Location,
|
||||||
prepend_template=LOCATION_ELEVATIONS
|
prepend_template=LOCATION_ELEVATIONS
|
||||||
@ -121,5 +130,7 @@ class LocationTable(BaseTable):
|
|||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Location
|
model = Location
|
||||||
fields = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'actions')
|
fields = (
|
||||||
|
'pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'tags', 'actions',
|
||||||
|
)
|
||||||
default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'actions')
|
default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'actions')
|
||||||
|
@ -31,11 +31,14 @@ class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
for region in regions:
|
for region in regions:
|
||||||
region.save()
|
region.save()
|
||||||
|
|
||||||
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'Region X',
|
'name': 'Region X',
|
||||||
'slug': 'region-x',
|
'slug': 'region-x',
|
||||||
'parent': regions[2].pk,
|
'parent': regions[2].pk,
|
||||||
'description': 'A new region',
|
'description': 'A new region',
|
||||||
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
@ -65,11 +68,14 @@ class SiteGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
for sitegroup in sitegroups:
|
for sitegroup in sitegroups:
|
||||||
sitegroup.save()
|
sitegroup.save()
|
||||||
|
|
||||||
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'Site Group X',
|
'name': 'Site Group X',
|
||||||
'slug': 'site-group-x',
|
'slug': 'site-group-x',
|
||||||
'parent': sitegroups[2].pk,
|
'parent': sitegroups[2].pk,
|
||||||
'description': 'A new site group',
|
'description': 'A new site group',
|
||||||
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
@ -169,12 +175,15 @@ class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
for location in locations:
|
for location in locations:
|
||||||
location.save()
|
location.save()
|
||||||
|
|
||||||
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'Location X',
|
'name': 'Location X',
|
||||||
'slug': 'location-x',
|
'slug': 'location-x',
|
||||||
'site': site.pk,
|
'site': site.pk,
|
||||||
'tenant': tenant.pk,
|
'tenant': tenant.pk,
|
||||||
'description': 'A new location',
|
'description': 'A new location',
|
||||||
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
@ -201,11 +210,14 @@ class RackRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
RackRole(name='Rack Role 3', slug='rack-role-3'),
|
RackRole(name='Rack Role 3', slug='rack-role-3'),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'Rack Role X',
|
'name': 'Rack Role X',
|
||||||
'slug': 'rack-role-x',
|
'slug': 'rack-role-x',
|
||||||
'color': 'c0c0c0',
|
'color': 'c0c0c0',
|
||||||
'description': 'New role',
|
'description': 'New role',
|
||||||
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
@ -368,10 +380,13 @@ class ManufacturerTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
Manufacturer(name='Manufacturer 3', slug='manufacturer-3'),
|
Manufacturer(name='Manufacturer 3', slug='manufacturer-3'),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'Manufacturer X',
|
'name': 'Manufacturer X',
|
||||||
'slug': 'manufacturer-x',
|
'slug': 'manufacturer-x',
|
||||||
'description': 'A new manufacturer',
|
'description': 'A new manufacturer',
|
||||||
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
@ -1034,12 +1049,15 @@ class DeviceRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
DeviceRole(name='Device Role 3', slug='device-role-3'),
|
DeviceRole(name='Device Role 3', slug='device-role-3'),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'Devie Role X',
|
'name': 'Devie Role X',
|
||||||
'slug': 'device-role-x',
|
'slug': 'device-role-x',
|
||||||
'color': 'c0c0c0',
|
'color': 'c0c0c0',
|
||||||
'vm_role': False,
|
'vm_role': False,
|
||||||
'description': 'New device role',
|
'description': 'New device role',
|
||||||
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
@ -1069,6 +1087,8 @@ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturer),
|
Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturer),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'Platform X',
|
'name': 'Platform X',
|
||||||
'slug': 'platform-x',
|
'slug': 'platform-x',
|
||||||
@ -1076,6 +1096,7 @@ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
'napalm_driver': 'junos',
|
'napalm_driver': 'junos',
|
||||||
'napalm_args': None,
|
'napalm_args': None,
|
||||||
'description': 'A new platform',
|
'description': 'A new platform',
|
||||||
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
|
@ -9,7 +9,6 @@ from ipam.choices import *
|
|||||||
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES
|
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES
|
||||||
from ipam.models import *
|
from ipam.models import *
|
||||||
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
||||||
from netbox.api.serializers import OrganizationalModelSerializer
|
|
||||||
from netbox.api.serializers import PrimaryModelSerializer
|
from netbox.api.serializers import PrimaryModelSerializer
|
||||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||||
from utilities.api import get_serializer_for_model
|
from utilities.api import get_serializer_for_model
|
||||||
@ -66,14 +65,14 @@ class RouteTargetSerializer(PrimaryModelSerializer):
|
|||||||
# RIRs/aggregates
|
# RIRs/aggregates
|
||||||
#
|
#
|
||||||
|
|
||||||
class RIRSerializer(OrganizationalModelSerializer):
|
class RIRSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
|
||||||
aggregate_count = serializers.IntegerField(read_only=True)
|
aggregate_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RIR
|
model = RIR
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'slug', 'is_private', 'description', 'custom_fields', 'created',
|
'id', 'url', 'display', 'name', 'slug', 'is_private', 'description', 'tags', 'custom_fields', 'created',
|
||||||
'last_updated', 'aggregate_count',
|
'last_updated', 'aggregate_count',
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -97,7 +96,7 @@ class AggregateSerializer(PrimaryModelSerializer):
|
|||||||
# VLANs
|
# VLANs
|
||||||
#
|
#
|
||||||
|
|
||||||
class RoleSerializer(OrganizationalModelSerializer):
|
class RoleSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
|
||||||
prefix_count = serializers.IntegerField(read_only=True)
|
prefix_count = serializers.IntegerField(read_only=True)
|
||||||
vlan_count = serializers.IntegerField(read_only=True)
|
vlan_count = serializers.IntegerField(read_only=True)
|
||||||
@ -105,12 +104,12 @@ class RoleSerializer(OrganizationalModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Role
|
model = Role
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'slug', 'weight', 'description', 'custom_fields', 'created', 'last_updated',
|
'id', 'url', 'display', 'name', 'slug', 'weight', 'description', 'tags', 'custom_fields', 'created',
|
||||||
'prefix_count', 'vlan_count',
|
'last_updated', 'prefix_count', 'vlan_count',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class VLANGroupSerializer(OrganizationalModelSerializer):
|
class VLANGroupSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
|
||||||
scope_type = ContentTypeField(
|
scope_type = ContentTypeField(
|
||||||
queryset=ContentType.objects.filter(
|
queryset=ContentType.objects.filter(
|
||||||
@ -126,8 +125,8 @@ class VLANGroupSerializer(OrganizationalModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = VLANGroup
|
model = VLANGroup
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'description', 'custom_fields',
|
'id', 'url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'description', 'tags',
|
||||||
'created', 'last_updated', 'vlan_count',
|
'custom_fields', 'created', 'last_updated', 'vlan_count',
|
||||||
]
|
]
|
||||||
validators = []
|
validators = []
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ class RouteTargetViewSet(CustomFieldModelViewSet):
|
|||||||
class RIRViewSet(CustomFieldModelViewSet):
|
class RIRViewSet(CustomFieldModelViewSet):
|
||||||
queryset = RIR.objects.annotate(
|
queryset = RIR.objects.annotate(
|
||||||
aggregate_count=count_related(Aggregate, 'rir')
|
aggregate_count=count_related(Aggregate, 'rir')
|
||||||
)
|
).prefetch_related('tags')
|
||||||
serializer_class = serializers.RIRSerializer
|
serializer_class = serializers.RIRSerializer
|
||||||
filterset_class = filtersets.RIRFilterSet
|
filterset_class = filtersets.RIRFilterSet
|
||||||
|
|
||||||
@ -71,7 +71,7 @@ class RoleViewSet(CustomFieldModelViewSet):
|
|||||||
queryset = Role.objects.annotate(
|
queryset = Role.objects.annotate(
|
||||||
prefix_count=count_related(Prefix, 'role'),
|
prefix_count=count_related(Prefix, 'role'),
|
||||||
vlan_count=count_related(VLAN, 'role')
|
vlan_count=count_related(VLAN, 'role')
|
||||||
)
|
).prefetch_related('tags')
|
||||||
serializer_class = serializers.RoleSerializer
|
serializer_class = serializers.RoleSerializer
|
||||||
filterset_class = filtersets.RoleFilterSet
|
filterset_class = filtersets.RoleFilterSet
|
||||||
|
|
||||||
@ -126,7 +126,7 @@ class IPAddressViewSet(CustomFieldModelViewSet):
|
|||||||
class VLANGroupViewSet(CustomFieldModelViewSet):
|
class VLANGroupViewSet(CustomFieldModelViewSet):
|
||||||
queryset = VLANGroup.objects.annotate(
|
queryset = VLANGroup.objects.annotate(
|
||||||
vlan_count=count_related(VLAN, 'group')
|
vlan_count=count_related(VLAN, 'group')
|
||||||
)
|
).prefetch_related('tags')
|
||||||
serializer_class = serializers.VLANGroupSerializer
|
serializer_class = serializers.VLANGroupSerializer
|
||||||
filterset_class = filtersets.VLANGroupFilterSet
|
filterset_class = filtersets.VLANGroupFilterSet
|
||||||
|
|
||||||
|
@ -71,7 +71,7 @@ class RouteTargetBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldMode
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class RIRBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
class RIRBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=RIR.objects.all(),
|
queryset=RIR.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
@ -120,7 +120,7 @@ class AggregateBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelB
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class RoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
class RoleBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=Role.objects.all(),
|
queryset=Role.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
@ -280,7 +280,7 @@ class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelB
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class VLANGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
class VLANGroupBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=VLANGroup.objects.all(),
|
queryset=VLANGroup.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
|
@ -82,11 +82,15 @@ class RouteTargetForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
|
|
||||||
class RIRForm(BootstrapMixin, CustomFieldModelForm):
|
class RIRForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RIR
|
model = RIR
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug', 'is_private', 'description',
|
'name', 'slug', 'is_private', 'description', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -120,11 +124,15 @@ class AggregateForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
|
|
||||||
class RoleForm(BootstrapMixin, CustomFieldModelForm):
|
class RoleForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Role
|
model = Role
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug', 'weight', 'description',
|
'name', 'slug', 'weight', 'description', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -530,15 +538,19 @@ class VLANGroupForm(BootstrapMixin, CustomFieldModelForm):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VLANGroup
|
model = VLANGroup
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug', 'description', 'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack',
|
'name', 'slug', 'description', 'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack',
|
||||||
'clustergroup', 'cluster',
|
'clustergroup', 'cluster', 'tags',
|
||||||
]
|
]
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('VLAN Group', ('name', 'slug', 'description')),
|
('VLAN Group', ('name', 'slug', 'description', 'tags')),
|
||||||
('Scope', ('scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster')),
|
('Scope', ('scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster')),
|
||||||
)
|
)
|
||||||
widgets = {
|
widgets = {
|
||||||
|
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):
|
class RIR(OrganizationalModel):
|
||||||
"""
|
"""
|
||||||
A Regional Internet Registry (RIR) is responsible for the allocation of a large portion of the global IP address
|
A Regional Internet Registry (RIR) is responsible for the allocation of a large portion of the global IP address
|
||||||
@ -168,7 +168,7 @@ class Aggregate(PrimaryModel):
|
|||||||
return min(utilization, 100)
|
return min(utilization, 100)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class Role(OrganizationalModel):
|
class Role(OrganizationalModel):
|
||||||
"""
|
"""
|
||||||
A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or
|
A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or
|
||||||
|
@ -21,7 +21,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class VLANGroup(OrganizationalModel):
|
class VLANGroup(OrganizationalModel):
|
||||||
"""
|
"""
|
||||||
A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.
|
A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.
|
||||||
|
@ -85,11 +85,14 @@ class RIRTable(BaseTable):
|
|||||||
url_params={'rir_id': 'pk'},
|
url_params={'rir_id': 'pk'},
|
||||||
verbose_name='Aggregates'
|
verbose_name='Aggregates'
|
||||||
)
|
)
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='ipam:rir_list'
|
||||||
|
)
|
||||||
actions = ButtonsColumn(RIR)
|
actions = ButtonsColumn(RIR)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = RIR
|
model = RIR
|
||||||
fields = ('pk', 'name', 'slug', 'is_private', 'aggregate_count', 'description', 'actions')
|
fields = ('pk', 'name', 'slug', 'is_private', 'aggregate_count', 'description', 'tags', 'actions')
|
||||||
default_columns = ('pk', 'name', 'is_private', 'aggregate_count', 'description', 'actions')
|
default_columns = ('pk', 'name', 'is_private', 'aggregate_count', 'description', 'actions')
|
||||||
|
|
||||||
|
|
||||||
@ -144,11 +147,14 @@ class RoleTable(BaseTable):
|
|||||||
url_params={'role_id': 'pk'},
|
url_params={'role_id': 'pk'},
|
||||||
verbose_name='VLANs'
|
verbose_name='VLANs'
|
||||||
)
|
)
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='ipam:role_list'
|
||||||
|
)
|
||||||
actions = ButtonsColumn(Role)
|
actions = ButtonsColumn(Role)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Role
|
model = Role
|
||||||
fields = ('pk', 'name', 'slug', 'prefix_count', 'vlan_count', 'description', 'weight', 'actions')
|
fields = ('pk', 'name', 'slug', 'prefix_count', 'vlan_count', 'description', 'weight', 'tags', 'actions')
|
||||||
default_columns = ('pk', 'name', 'prefix_count', 'vlan_count', 'description', 'actions')
|
default_columns = ('pk', 'name', 'prefix_count', 'vlan_count', 'description', 'actions')
|
||||||
|
|
||||||
|
|
||||||
|
@ -74,6 +74,9 @@ class VLANGroupTable(BaseTable):
|
|||||||
url_params={'group_id': 'pk'},
|
url_params={'group_id': 'pk'},
|
||||||
verbose_name='VLANs'
|
verbose_name='VLANs'
|
||||||
)
|
)
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='ipam:vlangroup_list'
|
||||||
|
)
|
||||||
actions = ButtonsColumn(
|
actions = ButtonsColumn(
|
||||||
model=VLANGroup,
|
model=VLANGroup,
|
||||||
prepend_template=VLANGROUP_ADD_VLAN
|
prepend_template=VLANGROUP_ADD_VLAN
|
||||||
@ -81,7 +84,7 @@ class VLANGroupTable(BaseTable):
|
|||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = VLANGroup
|
model = VLANGroup
|
||||||
fields = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'slug', 'description', 'actions')
|
fields = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'slug', 'description', 'tags', 'actions')
|
||||||
default_columns = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'description', 'actions')
|
default_columns = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'description', 'actions')
|
||||||
|
|
||||||
|
|
||||||
|
@ -104,11 +104,14 @@ class RIRTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
RIR(name='RIR 3', slug='rir-3'),
|
RIR(name='RIR 3', slug='rir-3'),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'RIR X',
|
'name': 'RIR X',
|
||||||
'slug': 'rir-x',
|
'slug': 'rir-x',
|
||||||
'is_private': True,
|
'is_private': True,
|
||||||
'description': 'A new RIR',
|
'description': 'A new RIR',
|
||||||
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
@ -177,11 +180,14 @@ class RoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
Role(name='Role 3', slug='role-3'),
|
Role(name='Role 3', slug='role-3'),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'Role X',
|
'name': 'Role X',
|
||||||
'slug': 'role-x',
|
'slug': 'role-x',
|
||||||
'weight': 200,
|
'weight': 200,
|
||||||
'description': 'A new role',
|
'description': 'A new role',
|
||||||
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
@ -384,10 +390,13 @@ class VLANGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
VLANGroup(name='VLAN Group 3', slug='vlan-group-3', scope=sites[0]),
|
VLANGroup(name='VLAN Group 3', slug='vlan-group-3', scope=sites[0]),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'VLAN Group X',
|
'name': 'VLAN Group X',
|
||||||
'slug': 'vlan-group-x',
|
'slug': 'vlan-group-x',
|
||||||
'description': 'A new VLAN group',
|
'description': 'A new VLAN group',
|
||||||
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
|
@ -147,13 +147,6 @@ class NestedTagSerializer(WritableNestedSerializer):
|
|||||||
# Base model serializers
|
# Base model serializers
|
||||||
#
|
#
|
||||||
|
|
||||||
class OrganizationalModelSerializer(CustomFieldModelSerializer):
|
|
||||||
"""
|
|
||||||
Adds support for custom fields.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class PrimaryModelSerializer(CustomFieldModelSerializer):
|
class PrimaryModelSerializer(CustomFieldModelSerializer):
|
||||||
"""
|
"""
|
||||||
Adds support for custom fields and tags.
|
Adds support for custom fields and tags.
|
||||||
@ -189,9 +182,9 @@ class PrimaryModelSerializer(CustomFieldModelSerializer):
|
|||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
class NestedGroupModelSerializer(CustomFieldModelSerializer):
|
class NestedGroupModelSerializer(PrimaryModelSerializer):
|
||||||
"""
|
"""
|
||||||
Extends OrganizationalModelSerializer to include MPTT support.
|
Extends PrimaryModelSerializer to include MPTT support.
|
||||||
"""
|
"""
|
||||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||||
|
|
||||||
|
@ -41,6 +41,7 @@ class ObjectType(
|
|||||||
class OrganizationalObjectType(
|
class OrganizationalObjectType(
|
||||||
ChangelogMixin,
|
ChangelogMixin,
|
||||||
CustomFieldsMixin,
|
CustomFieldsMixin,
|
||||||
|
TagsMixin,
|
||||||
BaseObjectType
|
BaseObjectType
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
|
@ -143,6 +143,18 @@ class CustomValidationMixin(models.Model):
|
|||||||
post_clean.send(sender=self.__class__, instance=self)
|
post_clean.send(sender=self.__class__, instance=self)
|
||||||
|
|
||||||
|
|
||||||
|
class TagsMixin(models.Model):
|
||||||
|
"""
|
||||||
|
Enable the assignment of Tags.
|
||||||
|
"""
|
||||||
|
tags = TaggableManager(
|
||||||
|
through='extras.TaggedItem'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Base model classes
|
# Base model classes
|
||||||
|
|
||||||
@ -166,7 +178,7 @@ class ChangeLoggedModel(ChangeLoggingMixin, CustomValidationMixin, BigIDModel):
|
|||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
class PrimaryModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin, BigIDModel):
|
class PrimaryModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin, TagsMixin, BigIDModel):
|
||||||
"""
|
"""
|
||||||
Primary models represent real objects within the infrastructure being modeled.
|
Primary models represent real objects within the infrastructure being modeled.
|
||||||
"""
|
"""
|
||||||
@ -175,15 +187,12 @@ class PrimaryModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin,
|
|||||||
object_id_field='assigned_object_id',
|
object_id_field='assigned_object_id',
|
||||||
content_type_field='assigned_object_type'
|
content_type_field='assigned_object_type'
|
||||||
)
|
)
|
||||||
tags = TaggableManager(
|
|
||||||
through='extras.TaggedItem'
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
class NestedGroupModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin, BigIDModel, MPTTModel):
|
class NestedGroupModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin, TagsMixin, BigIDModel, MPTTModel):
|
||||||
"""
|
"""
|
||||||
Base model for objects which are used to form a hierarchy (regions, locations, etc.). These models nest
|
Base model for objects which are used to form a hierarchy (regions, locations, etc.). These models nest
|
||||||
recursively using MPTT. Within each parent, each child instance must have a unique name.
|
recursively using MPTT. Within each parent, each child instance must have a unique name.
|
||||||
@ -225,7 +234,7 @@ class NestedGroupModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMi
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class OrganizationalModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin, BigIDModel):
|
class OrganizationalModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin, TagsMixin, BigIDModel):
|
||||||
"""
|
"""
|
||||||
Organizational models are those which are used solely to categorize and qualify other objects, and do not convey
|
Organizational models are those which are used solely to categorize and qualify other objects, and do not convey
|
||||||
any real information about the infrastructure being modeled (for example, functional device roles). Organizational
|
any real information about the infrastructure being modeled (for example, functional device roles). Organizational
|
||||||
|
@ -2,7 +2,7 @@ from django.contrib.auth.models import ContentType
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from netbox.api import ChoiceField, ContentTypeField
|
from netbox.api import ChoiceField, ContentTypeField
|
||||||
from netbox.api.serializers import NestedGroupModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer
|
from netbox.api.serializers import NestedGroupModelSerializer, PrimaryModelSerializer
|
||||||
from tenancy.choices import ContactPriorityChoices
|
from tenancy.choices import ContactPriorityChoices
|
||||||
from tenancy.models import *
|
from tenancy.models import *
|
||||||
from .nested_serializers import *
|
from .nested_serializers import *
|
||||||
@ -20,8 +20,8 @@ class TenantGroupSerializer(NestedGroupModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = TenantGroup
|
model = TenantGroup
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'custom_fields', 'created', 'last_updated',
|
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created',
|
||||||
'tenant_count', '_depth',
|
'last_updated', 'tenant_count', '_depth',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -60,18 +60,18 @@ class ContactGroupSerializer(NestedGroupModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = ContactGroup
|
model = ContactGroup
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'custom_fields', 'created', 'last_updated',
|
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created',
|
||||||
'contact_count', '_depth',
|
'last_updated', 'contact_count', '_depth',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ContactRoleSerializer(OrganizationalModelSerializer):
|
class ContactRoleSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactrole-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactrole-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ContactRole
|
model = ContactRole
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'slug', 'description', 'custom_fields', 'created', 'last_updated',
|
'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ class TenantGroupViewSet(CustomFieldModelViewSet):
|
|||||||
'group',
|
'group',
|
||||||
'tenant_count',
|
'tenant_count',
|
||||||
cumulative=True
|
cumulative=True
|
||||||
)
|
).prefetch_related('tags')
|
||||||
serializer_class = serializers.TenantGroupSerializer
|
serializer_class = serializers.TenantGroupSerializer
|
||||||
filterset_class = filtersets.TenantGroupFilterSet
|
filterset_class = filtersets.TenantGroupFilterSet
|
||||||
|
|
||||||
@ -64,28 +64,24 @@ class ContactGroupViewSet(CustomFieldModelViewSet):
|
|||||||
'group',
|
'group',
|
||||||
'contact_count',
|
'contact_count',
|
||||||
cumulative=True
|
cumulative=True
|
||||||
)
|
).prefetch_related('tags')
|
||||||
serializer_class = serializers.ContactGroupSerializer
|
serializer_class = serializers.ContactGroupSerializer
|
||||||
filterset_class = filtersets.ContactGroupFilterSet
|
filterset_class = filtersets.ContactGroupFilterSet
|
||||||
|
|
||||||
|
|
||||||
class ContactRoleViewSet(CustomFieldModelViewSet):
|
class ContactRoleViewSet(CustomFieldModelViewSet):
|
||||||
queryset = ContactRole.objects.all()
|
queryset = ContactRole.objects.prefetch_related('tags')
|
||||||
serializer_class = serializers.ContactRoleSerializer
|
serializer_class = serializers.ContactRoleSerializer
|
||||||
filterset_class = filtersets.ContactRoleFilterSet
|
filterset_class = filtersets.ContactRoleFilterSet
|
||||||
|
|
||||||
|
|
||||||
class ContactViewSet(CustomFieldModelViewSet):
|
class ContactViewSet(CustomFieldModelViewSet):
|
||||||
queryset = Contact.objects.prefetch_related(
|
queryset = Contact.objects.prefetch_related('group', 'tags')
|
||||||
'group', 'tags'
|
|
||||||
)
|
|
||||||
serializer_class = serializers.ContactSerializer
|
serializer_class = serializers.ContactSerializer
|
||||||
filterset_class = filtersets.ContactFilterSet
|
filterset_class = filtersets.ContactFilterSet
|
||||||
|
|
||||||
|
|
||||||
class ContactAssignmentViewSet(CustomFieldModelViewSet):
|
class ContactAssignmentViewSet(CustomFieldModelViewSet):
|
||||||
queryset = ContactAssignment.objects.prefetch_related(
|
queryset = ContactAssignment.objects.prefetch_related('contact', 'role')
|
||||||
'contact', 'role'
|
|
||||||
)
|
|
||||||
serializer_class = serializers.ContactAssignmentSerializer
|
serializer_class = serializers.ContactAssignmentSerializer
|
||||||
filterset_class = filtersets.ContactAssignmentFilterSet
|
filterset_class = filtersets.ContactAssignmentFilterSet
|
||||||
|
@ -17,7 +17,7 @@ __all__ = (
|
|||||||
# Tenants
|
# Tenants
|
||||||
#
|
#
|
||||||
|
|
||||||
class TenantGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
class TenantGroupBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=TenantGroup.objects.all(),
|
queryset=TenantGroup.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
@ -55,7 +55,7 @@ class TenantBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulk
|
|||||||
# Contacts
|
# Contacts
|
||||||
#
|
#
|
||||||
|
|
||||||
class ContactGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
class ContactGroupBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=ContactGroup.objects.all(),
|
queryset=ContactGroup.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
@ -73,7 +73,7 @@ class ContactGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
|||||||
nullable_fields = ['parent', 'description']
|
nullable_fields = ['parent', 'description']
|
||||||
|
|
||||||
|
|
||||||
class ContactRoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
class ContactRoleBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=ContactRole.objects.all(),
|
queryset=ContactRole.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
|
@ -28,11 +28,15 @@ class TenantGroupForm(BootstrapMixin, CustomFieldModelForm):
|
|||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = TenantGroup
|
model = TenantGroup
|
||||||
fields = [
|
fields = [
|
||||||
'parent', 'name', 'slug', 'description',
|
'parent', 'name', 'slug', 'description', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -68,18 +72,26 @@ class ContactGroupForm(BootstrapMixin, CustomFieldModelForm):
|
|||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ContactGroup
|
model = ContactGroup
|
||||||
fields = ['parent', 'name', 'slug', 'description']
|
fields = ('parent', 'name', 'slug', 'description', 'tags')
|
||||||
|
|
||||||
|
|
||||||
class ContactRoleForm(BootstrapMixin, CustomFieldModelForm):
|
class ContactRoleForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ContactRole
|
model = ContactRole
|
||||||
fields = ['name', 'slug', 'description']
|
fields = ('name', 'slug', 'description', 'tags')
|
||||||
|
|
||||||
|
|
||||||
class ContactForm(BootstrapMixin, CustomFieldModelForm):
|
class ContactForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
|
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
|
# Tenants
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class TenantGroup(NestedGroupModel):
|
class TenantGroup(NestedGroupModel):
|
||||||
"""
|
"""
|
||||||
An arbitrary collection of Tenants.
|
An arbitrary collection of Tenants.
|
||||||
@ -111,7 +111,7 @@ class Tenant(PrimaryModel):
|
|||||||
# Contacts
|
# Contacts
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class ContactGroup(NestedGroupModel):
|
class ContactGroup(NestedGroupModel):
|
||||||
"""
|
"""
|
||||||
An arbitrary collection of Contacts.
|
An arbitrary collection of Contacts.
|
||||||
@ -145,7 +145,7 @@ class ContactGroup(NestedGroupModel):
|
|||||||
return reverse('tenancy:contactgroup', args=[self.pk])
|
return reverse('tenancy:contactgroup', args=[self.pk])
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class ContactRole(OrganizationalModel):
|
class ContactRole(OrganizationalModel):
|
||||||
"""
|
"""
|
||||||
Functional role for a Contact assigned to an object.
|
Functional role for a Contact assigned to an object.
|
||||||
|
@ -55,11 +55,14 @@ class TenantGroupTable(BaseTable):
|
|||||||
url_params={'group_id': 'pk'},
|
url_params={'group_id': 'pk'},
|
||||||
verbose_name='Tenants'
|
verbose_name='Tenants'
|
||||||
)
|
)
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='tenancy:tenantgroup_list'
|
||||||
|
)
|
||||||
actions = ButtonsColumn(TenantGroup)
|
actions = ButtonsColumn(TenantGroup)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = TenantGroup
|
model = TenantGroup
|
||||||
fields = ('pk', 'name', 'tenant_count', 'description', 'slug', 'actions')
|
fields = ('pk', 'name', 'tenant_count', 'description', 'slug', 'tags', 'actions')
|
||||||
default_columns = ('pk', 'name', 'tenant_count', 'description', 'actions')
|
default_columns = ('pk', 'name', 'tenant_count', 'description', 'actions')
|
||||||
|
|
||||||
|
|
||||||
@ -96,11 +99,14 @@ class ContactGroupTable(BaseTable):
|
|||||||
url_params={'role_id': 'pk'},
|
url_params={'role_id': 'pk'},
|
||||||
verbose_name='Contacts'
|
verbose_name='Contacts'
|
||||||
)
|
)
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='tenancy:contactgroup_list'
|
||||||
|
)
|
||||||
actions = ButtonsColumn(ContactGroup)
|
actions = ButtonsColumn(ContactGroup)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = ContactGroup
|
model = ContactGroup
|
||||||
fields = ('pk', 'name', 'contact_count', 'description', 'slug', 'actions')
|
fields = ('pk', 'name', 'contact_count', 'description', 'slug', 'tags', 'actions')
|
||||||
default_columns = ('pk', 'name', 'contact_count', 'description', 'actions')
|
default_columns = ('pk', 'name', 'contact_count', 'description', 'actions')
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,10 +16,13 @@ class TenantGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
for tenanantgroup in tenant_groups:
|
for tenanantgroup in tenant_groups:
|
||||||
tenanantgroup.save()
|
tenanantgroup.save()
|
||||||
|
|
||||||
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'Tenant Group X',
|
'name': 'Tenant Group X',
|
||||||
'slug': 'tenant-group-x',
|
'slug': 'tenant-group-x',
|
||||||
'description': 'A new tenant group',
|
'description': 'A new tenant group',
|
||||||
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
@ -90,10 +93,13 @@ class ContactGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
for tenanantgroup in contact_groups:
|
for tenanantgroup in contact_groups:
|
||||||
tenanantgroup.save()
|
tenanantgroup.save()
|
||||||
|
|
||||||
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'Contact Group X',
|
'name': 'Contact Group X',
|
||||||
'slug': 'contact-group-x',
|
'slug': 'contact-group-x',
|
||||||
'description': 'A new contact group',
|
'description': 'A new contact group',
|
||||||
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
@ -120,10 +126,13 @@ class ContactRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
ContactRole(name='Contact Role 3', slug='contact-role-3'),
|
ContactRole(name='Contact Role 3', slug='contact-role-3'),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'Devie Role X',
|
'name': 'Devie Role X',
|
||||||
'slug': 'contact-role-x',
|
'slug': 'contact-role-x',
|
||||||
'description': 'New contact role',
|
'description': 'New contact role',
|
||||||
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
|
@ -6,7 +6,7 @@ from dcim.choices import InterfaceModeChoices
|
|||||||
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
|
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
|
||||||
from ipam.models import VLAN
|
from ipam.models import VLAN
|
||||||
from netbox.api import ChoiceField, SerializedPKRelatedField
|
from netbox.api import ChoiceField, SerializedPKRelatedField
|
||||||
from netbox.api.serializers import OrganizationalModelSerializer, PrimaryModelSerializer
|
from netbox.api.serializers import PrimaryModelSerializer
|
||||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||||
from virtualization.choices import *
|
from virtualization.choices import *
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
||||||
@ -17,26 +17,26 @@ from .nested_serializers import *
|
|||||||
# Clusters
|
# Clusters
|
||||||
#
|
#
|
||||||
|
|
||||||
class ClusterTypeSerializer(OrganizationalModelSerializer):
|
class ClusterTypeSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail')
|
||||||
cluster_count = serializers.IntegerField(read_only=True)
|
cluster_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ClusterType
|
model = ClusterType
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'slug', 'description', 'custom_fields', 'created', 'last_updated',
|
'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
'cluster_count',
|
'cluster_count',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ClusterGroupSerializer(OrganizationalModelSerializer):
|
class ClusterGroupSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail')
|
||||||
cluster_count = serializers.IntegerField(read_only=True)
|
cluster_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ClusterGroup
|
model = ClusterGroup
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'slug', 'description', 'custom_fields', 'created', 'last_updated',
|
'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
'cluster_count',
|
'cluster_count',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ class VirtualizationRootView(APIRootView):
|
|||||||
class ClusterTypeViewSet(CustomFieldModelViewSet):
|
class ClusterTypeViewSet(CustomFieldModelViewSet):
|
||||||
queryset = ClusterType.objects.annotate(
|
queryset = ClusterType.objects.annotate(
|
||||||
cluster_count=count_related(Cluster, 'type')
|
cluster_count=count_related(Cluster, 'type')
|
||||||
)
|
).prefetch_related('tags')
|
||||||
serializer_class = serializers.ClusterTypeSerializer
|
serializer_class = serializers.ClusterTypeSerializer
|
||||||
filterset_class = filtersets.ClusterTypeFilterSet
|
filterset_class = filtersets.ClusterTypeFilterSet
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ class ClusterTypeViewSet(CustomFieldModelViewSet):
|
|||||||
class ClusterGroupViewSet(CustomFieldModelViewSet):
|
class ClusterGroupViewSet(CustomFieldModelViewSet):
|
||||||
queryset = ClusterGroup.objects.annotate(
|
queryset = ClusterGroup.objects.annotate(
|
||||||
cluster_count=count_related(Cluster, 'group')
|
cluster_count=count_related(Cluster, 'group')
|
||||||
)
|
).prefetch_related('tags')
|
||||||
serializer_class = serializers.ClusterGroupSerializer
|
serializer_class = serializers.ClusterGroupSerializer
|
||||||
filterset_class = filtersets.ClusterGroupFilterSet
|
filterset_class = filtersets.ClusterGroupFilterSet
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ClusterTypeBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
class ClusterTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=ClusterType.objects.all(),
|
queryset=ClusterType.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
@ -37,7 +37,7 @@ class ClusterTypeBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
|||||||
nullable_fields = ['description']
|
nullable_fields = ['description']
|
||||||
|
|
||||||
|
|
||||||
class ClusterGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
class ClusterGroupBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=ClusterGroup.objects.all(),
|
queryset=ClusterGroup.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
|
@ -28,22 +28,30 @@ __all__ = (
|
|||||||
|
|
||||||
class ClusterTypeForm(BootstrapMixin, CustomFieldModelForm):
|
class ClusterTypeForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ClusterType
|
model = ClusterType
|
||||||
fields = [
|
fields = (
|
||||||
'name', 'slug', 'description',
|
'name', 'slug', 'description', 'tags',
|
||||||
]
|
)
|
||||||
|
|
||||||
|
|
||||||
class ClusterGroupForm(BootstrapMixin, CustomFieldModelForm):
|
class ClusterGroupForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ClusterGroup
|
model = ClusterGroup
|
||||||
fields = [
|
fields = (
|
||||||
'name', 'slug', 'description',
|
'name', 'slug', 'description', 'tags',
|
||||||
]
|
)
|
||||||
|
|
||||||
|
|
||||||
class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||||
|
25
netbox/virtualization/migrations/0025_extend_tag_support.py
Normal file
25
netbox/virtualization/migrations/0025_extend_tag_support.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-10-21 14:50
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import taggit.managers
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('extras', '0062_clear_secrets_changelog'),
|
||||||
|
('virtualization', '0024_cluster_relax_uniqueness'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='clustergroup',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='clustertype',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
]
|
@ -30,7 +30,7 @@ __all__ = (
|
|||||||
# Cluster types
|
# Cluster types
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class ClusterType(OrganizationalModel):
|
class ClusterType(OrganizationalModel):
|
||||||
"""
|
"""
|
||||||
A type of Cluster.
|
A type of Cluster.
|
||||||
@ -64,7 +64,7 @@ class ClusterType(OrganizationalModel):
|
|||||||
# Cluster groups
|
# Cluster groups
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class ClusterGroup(OrganizationalModel):
|
class ClusterGroup(OrganizationalModel):
|
||||||
"""
|
"""
|
||||||
An organizational group of Clusters.
|
An organizational group of Clusters.
|
||||||
|
@ -40,11 +40,14 @@ class ClusterTypeTable(BaseTable):
|
|||||||
cluster_count = tables.Column(
|
cluster_count = tables.Column(
|
||||||
verbose_name='Clusters'
|
verbose_name='Clusters'
|
||||||
)
|
)
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='virtualization:clustertype_list'
|
||||||
|
)
|
||||||
actions = ButtonsColumn(ClusterType)
|
actions = ButtonsColumn(ClusterType)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = ClusterType
|
model = ClusterType
|
||||||
fields = ('pk', 'name', 'slug', 'cluster_count', 'description', 'actions')
|
fields = ('pk', 'name', 'slug', 'cluster_count', 'description', 'tags', 'actions')
|
||||||
default_columns = ('pk', 'name', 'cluster_count', 'description', 'actions')
|
default_columns = ('pk', 'name', 'cluster_count', 'description', 'actions')
|
||||||
|
|
||||||
|
|
||||||
@ -60,11 +63,14 @@ class ClusterGroupTable(BaseTable):
|
|||||||
cluster_count = tables.Column(
|
cluster_count = tables.Column(
|
||||||
verbose_name='Clusters'
|
verbose_name='Clusters'
|
||||||
)
|
)
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='virtualization:clustergroup_list'
|
||||||
|
)
|
||||||
actions = ButtonsColumn(ClusterGroup)
|
actions = ButtonsColumn(ClusterGroup)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = ClusterGroup
|
model = ClusterGroup
|
||||||
fields = ('pk', 'name', 'slug', 'cluster_count', 'description', 'actions')
|
fields = ('pk', 'name', 'slug', 'cluster_count', 'description', 'tags', 'actions')
|
||||||
default_columns = ('pk', 'name', 'cluster_count', 'description', 'actions')
|
default_columns = ('pk', 'name', 'cluster_count', 'description', 'actions')
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,10 +22,13 @@ class ClusterGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
ClusterGroup(name='Cluster Group 3', slug='cluster-group-3'),
|
ClusterGroup(name='Cluster Group 3', slug='cluster-group-3'),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'Cluster Group X',
|
'name': 'Cluster Group X',
|
||||||
'slug': 'cluster-group-x',
|
'slug': 'cluster-group-x',
|
||||||
'description': 'A new cluster group',
|
'description': 'A new cluster group',
|
||||||
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
@ -52,10 +55,13 @@ class ClusterTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
ClusterType(name='Cluster Type 3', slug='cluster-type-3'),
|
ClusterType(name='Cluster Type 3', slug='cluster-type-3'),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'Cluster Type X',
|
'name': 'Cluster Type X',
|
||||||
'slug': 'cluster-type-x',
|
'slug': 'cluster-type-x',
|
||||||
'description': 'A new cluster type',
|
'description': 'A new cluster type',
|
||||||
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
|
Reference in New Issue
Block a user