mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -06:00
Merge pull request #4770 from netbox-community/3703-limit-tag-creation
Closes #3703: Restrict tag creation
This commit is contained in:
commit
2d4694e72d
@ -42,10 +42,6 @@ django-tables2
|
||||
# https://github.com/alex/django-taggit
|
||||
django-taggit
|
||||
|
||||
# A Django REST Framework serializer which represents tags
|
||||
# https://github.com/glemmaPaul/django-taggit-serializer
|
||||
django-taggit-serializer
|
||||
|
||||
# A Django field for representing time zones
|
||||
# https://github.com/mfogel/django-timezone-field/
|
||||
django-timezone-field
|
||||
|
@ -10,6 +10,7 @@ NetBox v2.9 replaces Django's built-in permissions framework with one that suppo
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#3703](https://github.com/netbox-community/netbox/issues/3703) - Tags must be created administratively before being assigned to an object
|
||||
* [#4615](https://github.com/netbox-community/netbox/issues/4615) - Add `label` field for all device components
|
||||
* [#4742](https://github.com/netbox-community/netbox/issues/4742) - Add tagging for cables, power panels, and rack reservations
|
||||
|
||||
@ -18,6 +19,20 @@ NetBox v2.9 replaces Django's built-in permissions framework with one that suppo
|
||||
* If in use, LDAP authentication must be enabled by setting `REMOTE_AUTH_BACKEND` to `'netbox.authentication.LDAPBackend'`. (LDAP configuration parameters in `ldap_config.py` remain unchanged.)
|
||||
* `REMOTE_AUTH_DEFAULT_PERMISSIONS` now takes a dictionary rather than a list. This is a mapping of permission names to a dictionary of constraining attributes, or `None`. For example, `['dcim.add_site', 'dcim.change_site']` would become `{'dcim.add_site': None, 'dcim.change_site': None}`.
|
||||
|
||||
### REST API Changes
|
||||
|
||||
* The count of `tagged_items` is no longer included when viewing the tags list when `brief` is passed.
|
||||
* The assignment of tags to an object is now achieved in the same manner as specifying any other related device. The `tags` field accepts a list of JSON objects each matching a desired tag. (Alternatively, a list of numeric primary keys corresponding to tags may be passed instead.) For example:
|
||||
|
||||
```json
|
||||
"tags": [
|
||||
{"name": "First Tag"},
|
||||
{"name": "Second Tag"}
|
||||
]
|
||||
```
|
||||
|
||||
* The `tags` field of an object now includes a more complete representation of each tag, rather than just its name.
|
||||
|
||||
### Other Changes
|
||||
|
||||
* The `secrets.activate_userkey` permission no longer exists. Instead, `secrets.change_userkey` is checked to determine whether a user has the ability to activate a UserKey.
|
||||
|
@ -1,11 +1,11 @@
|
||||
from rest_framework import serializers
|
||||
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
|
||||
|
||||
from circuits.choices import CircuitStatusChoices
|
||||
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
|
||||
from dcim.api.nested_serializers import NestedCableSerializer, NestedInterfaceSerializer, NestedSiteSerializer
|
||||
from dcim.api.serializers import ConnectedEndpointSerializer
|
||||
from extras.api.customfields import CustomFieldModelSerializer
|
||||
from extras.api.serializers import TaggedObjectSerializer
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||
from utilities.api import ChoiceField, ValidatedModelSerializer, WritableNestedSerializer
|
||||
from .nested_serializers import *
|
||||
@ -15,8 +15,7 @@ from .nested_serializers import *
|
||||
# Providers
|
||||
#
|
||||
|
||||
class ProviderSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
tags = TagListSerializerField(required=False)
|
||||
class ProviderSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
circuit_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
@ -49,14 +48,13 @@ class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
|
||||
fields = ['id', 'url', 'site', 'connected_endpoint', 'port_speed', 'upstream_speed', 'xconnect_id']
|
||||
|
||||
|
||||
class CircuitSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
class CircuitSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
provider = NestedProviderSerializer()
|
||||
status = ChoiceField(choices=CircuitStatusChoices, required=False)
|
||||
type = NestedCircuitTypeSerializer()
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
termination_a = CircuitCircuitTerminationSerializer(read_only=True)
|
||||
termination_z = CircuitCircuitTerminationSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
|
@ -3,8 +3,8 @@ from django import forms
|
||||
from dcim.models import Region, Site
|
||||
from extras.forms import (
|
||||
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm,
|
||||
TagField,
|
||||
)
|
||||
from extras.models import Tag
|
||||
from tenancy.forms import TenancyFilterForm, TenancyForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
@ -23,7 +23,8 @@ from .models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
class ProviderForm(BootstrapMixin, CustomFieldModelForm):
|
||||
slug = SlugField()
|
||||
comments = CommentField()
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
@ -165,7 +166,8 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
queryset=CircuitType.objects.all()
|
||||
)
|
||||
comments = CommentField()
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
|
@ -26,7 +26,7 @@ class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'noc_contact': 'noc@example.com',
|
||||
'admin_contact': 'admin@example.com',
|
||||
'comments': 'Another provider',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': cls.create_tags('Alpha', 'Bravo', 'Charlie'),
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
@ -106,7 +106,7 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'commit_rate': 1000,
|
||||
'description': 'A new circuit',
|
||||
'comments': 'Some comments',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': cls.create_tags('Alpha', 'Bravo', 'Charlie'),
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
@ -124,5 +124,4 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'commit_rate': 2000,
|
||||
'description': 'New description',
|
||||
'comments': 'New comments',
|
||||
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from drf_yasg.utils import swagger_serializer_method
|
||||
from rest_framework import serializers
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
@ -14,6 +13,7 @@ from dcim.models import (
|
||||
VirtualChassis,
|
||||
)
|
||||
from extras.api.customfields import CustomFieldModelSerializer
|
||||
from extras.api.serializers import TaggedObjectSerializer
|
||||
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
|
||||
from ipam.models import VLAN
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||
@ -67,12 +67,11 @@ class RegionSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'name', 'slug', 'parent', 'description', 'site_count']
|
||||
|
||||
|
||||
class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
class SiteSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
status = ChoiceField(choices=SiteStatusChoices, required=False)
|
||||
region = NestedRegionSerializer(required=False, allow_null=True)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
time_zone = TimeZoneField(required=False)
|
||||
tags = TagListSerializerField(required=False)
|
||||
circuit_count = serializers.IntegerField(read_only=True)
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
prefix_count = serializers.IntegerField(read_only=True)
|
||||
@ -112,7 +111,7 @@ class RackRoleSerializer(ValidatedModelSerializer):
|
||||
fields = ['id', 'name', 'slug', 'color', 'description', 'rack_count']
|
||||
|
||||
|
||||
class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
class RackSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
site = NestedSiteSerializer()
|
||||
group = NestedRackGroupSerializer(required=False, allow_null=True, default=None)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
@ -121,7 +120,6 @@ class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
type = ChoiceField(choices=RackTypeChoices, allow_blank=True, required=False)
|
||||
width = ChoiceField(choices=RackWidthChoices, required=False)
|
||||
outer_unit = ChoiceField(choices=RackDimensionUnitChoices, allow_blank=True, required=False)
|
||||
tags = TagListSerializerField(required=False)
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
powerfeed_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
@ -161,11 +159,10 @@ class RackUnitSerializer(serializers.Serializer):
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
|
||||
|
||||
class RackReservationSerializer(ValidatedModelSerializer):
|
||||
class RackReservationSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
|
||||
rack = NestedRackSerializer()
|
||||
user = NestedUserSerializer()
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = RackReservation
|
||||
@ -224,10 +221,9 @@ class ManufacturerSerializer(ValidatedModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
class DeviceTypeSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
manufacturer = NestedManufacturerSerializer()
|
||||
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False)
|
||||
tags = TagListSerializerField(required=False)
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
@ -363,7 +359,7 @@ class PlatformSerializer(ValidatedModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
class DeviceSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
device_role = NestedDeviceRoleSerializer()
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
@ -378,7 +374,6 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
parent_device = serializers.SerializerMethodField()
|
||||
cluster = NestedClusterSerializer(required=False, allow_null=True)
|
||||
virtual_chassis = NestedVirtualChassisSerializer(required=False, allow_null=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
@ -434,7 +429,7 @@ class DeviceNAPALMSerializer(serializers.Serializer):
|
||||
method = serializers.DictField()
|
||||
|
||||
|
||||
class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
class ConsoleServerPortSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(
|
||||
choices=ConsolePortTypeChoices,
|
||||
@ -442,7 +437,6 @@ class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer)
|
||||
required=False
|
||||
)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
@ -452,7 +446,7 @@ class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer)
|
||||
]
|
||||
|
||||
|
||||
class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
class ConsolePortSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(
|
||||
choices=ConsolePortTypeChoices,
|
||||
@ -460,7 +454,6 @@ class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
required=False
|
||||
)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
@ -470,7 +463,7 @@ class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
]
|
||||
|
||||
|
||||
class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
class PowerOutletSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(
|
||||
choices=PowerOutletTypeChoices,
|
||||
@ -488,9 +481,6 @@ class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
cable = NestedCableSerializer(
|
||||
read_only=True
|
||||
)
|
||||
tags = TagListSerializerField(
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
@ -500,7 +490,7 @@ class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
]
|
||||
|
||||
|
||||
class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
class PowerPortSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(
|
||||
choices=PowerPortTypeChoices,
|
||||
@ -508,7 +498,6 @@ class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
required=False
|
||||
)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
@ -518,7 +507,7 @@ class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
]
|
||||
|
||||
|
||||
class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
class InterfaceSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(choices=InterfaceTypeChoices)
|
||||
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||
@ -531,7 +520,6 @@ class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
many=True
|
||||
)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
count_ipaddresses = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
@ -563,11 +551,10 @@ class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
return super().validate(data)
|
||||
|
||||
|
||||
class RearPortSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
class RearPortSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(choices=PortTypeChoices)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = RearPort
|
||||
@ -585,22 +572,20 @@ class FrontPortRearPortSerializer(WritableNestedSerializer):
|
||||
fields = ['id', 'url', 'name']
|
||||
|
||||
|
||||
class FrontPortSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
class FrontPortSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(choices=PortTypeChoices)
|
||||
rear_port = FrontPortRearPortSerializer()
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = FrontPort
|
||||
fields = ['id', 'device', 'name', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'tags']
|
||||
|
||||
|
||||
class DeviceBaySerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
class DeviceBaySerializer(TaggedObjectSerializer, ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
installed_device = NestedDeviceSerializer(required=False, allow_null=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = DeviceBay
|
||||
@ -611,12 +596,11 @@ class DeviceBaySerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
# Inventory items
|
||||
#
|
||||
|
||||
class InventoryItemSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
class InventoryItemSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
# Provide a default value to satisfy UniqueTogetherValidator
|
||||
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
|
||||
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True, default=None)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = InventoryItem
|
||||
@ -630,7 +614,7 @@ class InventoryItemSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
# Cables
|
||||
#
|
||||
|
||||
class CableSerializer(ValidatedModelSerializer):
|
||||
class CableSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
|
||||
termination_a_type = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS)
|
||||
)
|
||||
@ -641,7 +625,6 @@ class CableSerializer(ValidatedModelSerializer):
|
||||
termination_b = serializers.SerializerMethodField(read_only=True)
|
||||
status = ChoiceField(choices=CableStatusChoices, required=False)
|
||||
length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
@ -710,9 +693,8 @@ class InterfaceConnectionSerializer(ValidatedModelSerializer):
|
||||
# Virtual chassis
|
||||
#
|
||||
|
||||
class VirtualChassisSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
class VirtualChassisSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
|
||||
master = NestedDeviceSerializer()
|
||||
tags = TagListSerializerField(required=False)
|
||||
member_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
@ -724,14 +706,13 @@ class VirtualChassisSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
# Power panels
|
||||
#
|
||||
|
||||
class PowerPanelSerializer(ValidatedModelSerializer):
|
||||
class PowerPanelSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
|
||||
site = NestedSiteSerializer()
|
||||
rack_group = NestedRackGroupSerializer(
|
||||
required=False,
|
||||
allow_null=True,
|
||||
default=None
|
||||
)
|
||||
tags = TagListSerializerField(required=False)
|
||||
powerfeed_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
@ -739,7 +720,7 @@ class PowerPanelSerializer(ValidatedModelSerializer):
|
||||
fields = ['id', 'site', 'rack_group', 'name', 'tags', 'powerfeed_count']
|
||||
|
||||
|
||||
class PowerFeedSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
class PowerFeedSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
power_panel = NestedPowerPanelSerializer()
|
||||
rack = NestedRackSerializer(
|
||||
required=False,
|
||||
@ -762,9 +743,6 @@ class PowerFeedSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
choices=PowerFeedPhaseChoices,
|
||||
default=PowerFeedPhaseChoices.PHASE_SINGLE
|
||||
)
|
||||
tags = TagListSerializerField(
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerFeed
|
||||
|
@ -14,8 +14,9 @@ from timezone_field import TimeZoneFormField
|
||||
from circuits.models import Circuit, Provider
|
||||
from extras.forms import (
|
||||
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldFilterForm, CustomFieldModelForm,
|
||||
LocalConfigContextFilterForm, TagField,
|
||||
LocalConfigContextFilterForm,
|
||||
)
|
||||
from extras.models import Tag
|
||||
from ipam.constants import BGP_ASN_MAX, BGP_ASN_MIN
|
||||
from ipam.models import IPAddress, VLAN
|
||||
from tenancy.forms import TenancyFilterForm, TenancyForm
|
||||
@ -225,7 +226,8 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
)
|
||||
slug = SlugField()
|
||||
comments = CommentField()
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
@ -486,7 +488,8 @@ class RackForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField()
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
@ -766,7 +769,8 @@ class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm):
|
||||
),
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
@ -911,7 +915,8 @@ class DeviceTypeForm(BootstrapMixin, CustomFieldModelForm):
|
||||
slug_source='model'
|
||||
)
|
||||
comments = CommentField()
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
@ -1736,11 +1741,14 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField()
|
||||
tags = TagField(required=False)
|
||||
local_context_data = JSONField(
|
||||
required=False,
|
||||
label=''
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
@ -2229,7 +2237,8 @@ class ConsolePortFilterForm(DeviceComponentFilterForm):
|
||||
|
||||
|
||||
class ConsolePortForm(BootstrapMixin, forms.ModelForm):
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
@ -2256,7 +2265,8 @@ class ConsolePortCreateForm(LabeledComponentForm):
|
||||
max_length=100,
|
||||
required=False
|
||||
)
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
@ -2312,7 +2322,8 @@ class ConsoleServerPortFilterForm(DeviceComponentFilterForm):
|
||||
|
||||
|
||||
class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm):
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
@ -2339,7 +2350,8 @@ class ConsoleServerPortCreateForm(LabeledComponentForm):
|
||||
max_length=100,
|
||||
required=False
|
||||
)
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
@ -2409,7 +2421,8 @@ class PowerPortFilterForm(DeviceComponentFilterForm):
|
||||
|
||||
|
||||
class PowerPortForm(BootstrapMixin, forms.ModelForm):
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
@ -2446,7 +2459,8 @@ class PowerPortCreateForm(LabeledComponentForm):
|
||||
max_length=100,
|
||||
required=False
|
||||
)
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
@ -2506,7 +2520,8 @@ class PowerOutletForm(BootstrapMixin, forms.ModelForm):
|
||||
queryset=PowerPort.objects.all(),
|
||||
required=False
|
||||
)
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
@ -2550,7 +2565,8 @@ class PowerOutletCreateForm(LabeledComponentForm):
|
||||
max_length=100,
|
||||
required=False
|
||||
)
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
@ -2709,7 +2725,8 @@ class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
|
||||
},
|
||||
)
|
||||
)
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
@ -2793,7 +2810,8 @@ class InterfaceCreateForm(InterfaceCommonForm, LabeledComponentForm):
|
||||
required=False,
|
||||
widget=StaticSelect2(),
|
||||
)
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
untagged_vlan = DynamicModelChoiceField(
|
||||
@ -3005,7 +3023,8 @@ class FrontPortFilterForm(DeviceComponentFilterForm):
|
||||
|
||||
|
||||
class FrontPortForm(BootstrapMixin, forms.ModelForm):
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
@ -3196,7 +3215,8 @@ class RearPortFilterForm(DeviceComponentFilterForm):
|
||||
|
||||
|
||||
class RearPortForm(BootstrapMixin, forms.ModelForm):
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
@ -3299,7 +3319,8 @@ class DeviceBayFilterForm(DeviceComponentFilterForm):
|
||||
|
||||
|
||||
class DeviceBayForm(BootstrapMixin, forms.ModelForm):
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
@ -3320,7 +3341,8 @@ class DeviceBayCreateForm(BootstrapMixin, forms.Form):
|
||||
name_pattern = ExpandableNameField(
|
||||
label='Name'
|
||||
)
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
@ -3350,7 +3372,8 @@ class DeviceBayBulkCreateForm(
|
||||
form_from_model(DeviceBay, ['description', 'tags']),
|
||||
DeviceBulkAddComponentForm
|
||||
):
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
@ -3654,7 +3677,8 @@ class ConnectCableToPowerFeedForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
|
||||
class CableForm(BootstrapMixin, forms.ModelForm):
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
@ -3983,7 +4007,8 @@ class InventoryItemForm(BootstrapMixin, forms.ModelForm):
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False
|
||||
)
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
@ -4131,7 +4156,8 @@ class DeviceSelectionForm(forms.Form):
|
||||
|
||||
|
||||
class VirtualChassisForm(BootstrapMixin, forms.ModelForm):
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
@ -4321,7 +4347,8 @@ class PowerPanelForm(BootstrapMixin, forms.ModelForm):
|
||||
queryset=RackGroup.objects.all(),
|
||||
required=False
|
||||
)
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
@ -4445,7 +4472,8 @@ class PowerFeedForm(BootstrapMixin, CustomFieldModelForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField()
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
|
@ -94,7 +94,7 @@ class SiteTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'contact_phone': '123-555-9999',
|
||||
'contact_email': 'hank@stricklandpropane.com',
|
||||
'comments': 'Test site',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': cls.create_tags('Alpha', 'Bravo', 'Charlie'),
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
@ -202,7 +202,7 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'user': user3.pk,
|
||||
'tenant': None,
|
||||
'description': 'Rack reservation',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': cls.create_tags('Alpha', 'Bravo', 'Charlie'),
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
@ -268,7 +268,7 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'outer_depth': 500,
|
||||
'outer_unit': RackDimensionUnitChoices.UNIT_MILLIMETER,
|
||||
'comments': 'Some comments',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': cls.create_tags('Alpha', 'Bravo', 'Charlie'),
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
@ -359,7 +359,7 @@ class DeviceTypeTestCase(
|
||||
'is_full_depth': True,
|
||||
'subdevice_role': '', # CharField
|
||||
'comments': 'Some comments',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': cls.create_tags('Alpha', 'Bravo', 'Charlie'),
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
@ -967,7 +967,7 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'vc_position': None,
|
||||
'vc_priority': None,
|
||||
'comments': 'A new device',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': cls.create_tags('Alpha', 'Bravo', 'Charlie'),
|
||||
'local_context_data': None,
|
||||
}
|
||||
|
||||
@ -1001,12 +1001,14 @@ class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
ConsolePort(device=device, name='Console Port 3'),
|
||||
])
|
||||
|
||||
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'device': device.pk,
|
||||
'name': 'Console Port X',
|
||||
'type': ConsolePortTypeChoices.TYPE_RJ45,
|
||||
'description': 'A console port',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': tags,
|
||||
}
|
||||
|
||||
cls.bulk_create_data = {
|
||||
@ -1016,7 +1018,7 @@ class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
'label_pattern': 'Serial[3-5]',
|
||||
'type': ConsolePortTypeChoices.TYPE_RJ45,
|
||||
'description': 'A console port',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': tags,
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
@ -1045,12 +1047,14 @@ class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
ConsoleServerPort(device=device, name='Console Server Port 3'),
|
||||
])
|
||||
|
||||
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'device': device.pk,
|
||||
'name': 'Console Server Port X',
|
||||
'type': ConsolePortTypeChoices.TYPE_RJ45,
|
||||
'description': 'A console server port',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': tags,
|
||||
}
|
||||
|
||||
cls.bulk_create_data = {
|
||||
@ -1058,7 +1062,7 @@ class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
'name_pattern': 'Console Server Port [4-6]',
|
||||
'type': ConsolePortTypeChoices.TYPE_RJ45,
|
||||
'description': 'A console server port',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': tags,
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
@ -1087,6 +1091,8 @@ class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
PowerPort(device=device, name='Power Port 3'),
|
||||
])
|
||||
|
||||
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'device': device.pk,
|
||||
'name': 'Power Port X',
|
||||
@ -1094,7 +1100,7 @@ class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
'maximum_draw': 100,
|
||||
'allocated_draw': 50,
|
||||
'description': 'A power port',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': tags,
|
||||
}
|
||||
|
||||
cls.bulk_create_data = {
|
||||
@ -1104,7 +1110,7 @@ class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
'maximum_draw': 100,
|
||||
'allocated_draw': 50,
|
||||
'description': 'A power port',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': tags,
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
@ -1141,6 +1147,8 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
PowerOutlet(device=device, name='Power Outlet 3', power_port=powerports[0]),
|
||||
])
|
||||
|
||||
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'device': device.pk,
|
||||
'name': 'Power Outlet X',
|
||||
@ -1148,7 +1156,7 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
'power_port': powerports[1].pk,
|
||||
'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B,
|
||||
'description': 'A power outlet',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': tags,
|
||||
}
|
||||
|
||||
cls.bulk_create_data = {
|
||||
@ -1158,7 +1166,7 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
'power_port': powerports[1].pk,
|
||||
'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B,
|
||||
'description': 'A power outlet',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': tags,
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
@ -1202,6 +1210,8 @@ class InterfaceTestCase(
|
||||
)
|
||||
VLAN.objects.bulk_create(vlans)
|
||||
|
||||
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'device': device.pk,
|
||||
'virtual_machine': None,
|
||||
@ -1216,7 +1226,7 @@ class InterfaceTestCase(
|
||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||
'untagged_vlan': vlans[0].pk,
|
||||
'tagged_vlans': [v.pk for v in vlans[1:4]],
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': tags,
|
||||
}
|
||||
|
||||
cls.bulk_create_data = {
|
||||
@ -1232,7 +1242,7 @@ class InterfaceTestCase(
|
||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||
'untagged_vlan': vlans[0].pk,
|
||||
'tagged_vlans': [v.pk for v in vlans[1:4]],
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': tags,
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
@ -1279,6 +1289,8 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
FrontPort(device=device, name='Front Port 3', rear_port=rearports[2]),
|
||||
])
|
||||
|
||||
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'device': device.pk,
|
||||
'name': 'Front Port X',
|
||||
@ -1286,7 +1298,7 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
'rear_port': rearports[3].pk,
|
||||
'rear_port_position': 1,
|
||||
'description': 'New description',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': tags,
|
||||
}
|
||||
|
||||
cls.bulk_create_data = {
|
||||
@ -1297,7 +1309,7 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
'{}:1'.format(rp.pk) for rp in rearports[3:6]
|
||||
],
|
||||
'description': 'New description',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': tags,
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
@ -1326,13 +1338,15 @@ class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
RearPort(device=device, name='Rear Port 3'),
|
||||
])
|
||||
|
||||
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'device': device.pk,
|
||||
'name': 'Rear Port X',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'positions': 3,
|
||||
'description': 'A rear port',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': tags,
|
||||
}
|
||||
|
||||
cls.bulk_create_data = {
|
||||
@ -1341,7 +1355,7 @@ class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'positions': 3,
|
||||
'description': 'A rear port',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': tags,
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
@ -1373,18 +1387,20 @@ class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
DeviceBay(device=device, name='Device Bay 3'),
|
||||
])
|
||||
|
||||
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'device': device.pk,
|
||||
'name': 'Device Bay X',
|
||||
'description': 'A device bay',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': tags,
|
||||
}
|
||||
|
||||
cls.bulk_create_data = {
|
||||
'device': device.pk,
|
||||
'name_pattern': 'Device Bay [4-6]',
|
||||
'description': 'A device bay',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': tags,
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
@ -1413,6 +1429,8 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
InventoryItem(device=device, name='Inventory Item 3'),
|
||||
])
|
||||
|
||||
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'device': device.pk,
|
||||
'manufacturer': manufacturer.pk,
|
||||
@ -1423,7 +1441,7 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
'serial': '123ABC',
|
||||
'asset_tag': 'ABC123',
|
||||
'description': 'An inventory item',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': tags,
|
||||
}
|
||||
|
||||
cls.bulk_create_data = {
|
||||
@ -1435,7 +1453,7 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
'part_id': '123456',
|
||||
'serial': '123ABC',
|
||||
'description': 'An inventory item',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': tags,
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
@ -1513,7 +1531,7 @@ class CableTestCase(
|
||||
'color': 'c0c0c0',
|
||||
'length': 100,
|
||||
'length_unit': CableLengthUnitChoices.UNIT_FOOT,
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': cls.create_tags('Alpha', 'Bravo', 'Charlie'),
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
@ -1626,7 +1644,7 @@ class PowerPanelTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'site': sites[1].pk,
|
||||
'rack_group': rackgroups[1].pk,
|
||||
'name': 'Power Panel X',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': cls.create_tags('Alpha', 'Bravo', 'Charlie'),
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
@ -1680,7 +1698,7 @@ class PowerFeedTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'amperage': 100,
|
||||
'max_utilization': 50,
|
||||
'comments': 'New comments',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': cls.create_tags('Alpha', 'Bravo', 'Charlie'),
|
||||
|
||||
# Connection
|
||||
'cable': None,
|
||||
|
@ -38,11 +38,10 @@ class NestedGraphSerializer(WritableNestedSerializer):
|
||||
|
||||
class NestedTagSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail')
|
||||
tagged_items = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.Tag
|
||||
fields = ['id', 'url', 'name', 'slug', 'color', 'tagged_items']
|
||||
fields = ['id', 'url', 'name', 'slug', 'color']
|
||||
|
||||
|
||||
class NestedReportResultSerializer(serializers.ModelSerializer):
|
||||
|
@ -95,6 +95,28 @@ class TagSerializer(ValidatedModelSerializer):
|
||||
fields = ['id', 'name', 'slug', 'color', 'description', 'tagged_items']
|
||||
|
||||
|
||||
class TaggedObjectSerializer(serializers.Serializer):
|
||||
tags = NestedTagSerializer(many=True, required=False)
|
||||
|
||||
def create(self, validated_data):
|
||||
tags = validated_data.pop('tags', [])
|
||||
instance = super().create(validated_data)
|
||||
|
||||
return self._save_tags(instance, tags)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
tags = validated_data.pop('tags', [])
|
||||
instance = super().update(instance, validated_data)
|
||||
|
||||
return self._save_tags(instance, tags)
|
||||
|
||||
def _save_tags(self, instance, tags):
|
||||
if tags:
|
||||
instance.tags.set(*[t.name for t in tags])
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
#
|
||||
# Image attachments
|
||||
#
|
||||
|
@ -1,8 +1,8 @@
|
||||
from django import forms
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.safestring import mark_safe
|
||||
from mptt.forms import TreeNodeMultipleChoiceField
|
||||
from taggit.forms import TagField as TagField_
|
||||
|
||||
from dcim.models import DeviceRole, Platform, Region, Site
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
@ -142,15 +142,6 @@ class CustomFieldFilterForm(forms.Form):
|
||||
# Tags
|
||||
#
|
||||
|
||||
class TagField(TagField_):
|
||||
|
||||
def widget_attrs(self, widget):
|
||||
# Apply the "tagfield" CSS class to trigger the special API-based selection widget for tags
|
||||
return {
|
||||
'class': 'tagfield'
|
||||
}
|
||||
|
||||
|
||||
class TagForm(BootstrapMixin, forms.ModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
@ -161,14 +152,31 @@ class TagForm(BootstrapMixin, forms.ModelForm):
|
||||
]
|
||||
|
||||
|
||||
class TagCSVForm(CSVModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
fields = Tag.csv_headers
|
||||
help_texts = {
|
||||
'color': mark_safe('RGB color in hexadecimal (e.g. <code>00ff00</code>)'),
|
||||
}
|
||||
|
||||
|
||||
class AddRemoveTagsForm(forms.Form):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Add add/remove tags fields
|
||||
self.fields['add_tags'] = TagField(required=False)
|
||||
self.fields['remove_tags'] = TagField(required=False)
|
||||
self.fields['add_tags'] = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
self.fields['remove_tags'] = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
|
||||
class TagFilterForm(BootstrapMixin, forms.Form):
|
||||
|
@ -25,6 +25,8 @@ class Tag(TagBase, ChangeLoggedModel):
|
||||
objects = models.Manager()
|
||||
restricted = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['name', 'slug', 'color', 'description']
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('extras:tag', args=[self.slug])
|
||||
|
||||
@ -35,6 +37,14 @@ class Tag(TagBase, ChangeLoggedModel):
|
||||
slug += "_%d" % i
|
||||
return slug
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
self.color,
|
||||
self.description
|
||||
)
|
||||
|
||||
|
||||
class TaggedItem(GenericTaggedItemBase):
|
||||
tag = models.ForeignKey(
|
||||
|
@ -102,7 +102,7 @@ class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class TagTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Tag
|
||||
brief_fields = ['color', 'id', 'name', 'slug', 'tagged_items', 'url']
|
||||
brief_fields = ['color', 'id', 'name', 'slug', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'name': 'Tag 4',
|
||||
|
@ -11,7 +11,6 @@ from utilities.testing import APITestCase
|
||||
class ChangeLogTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super().setUp()
|
||||
|
||||
# Create a custom field on the Site model
|
||||
@ -31,9 +30,6 @@ class ChangeLogTest(APITestCase):
|
||||
'custom_fields': {
|
||||
'my_field': 'ABC'
|
||||
},
|
||||
'tags': [
|
||||
'bar', 'foo'
|
||||
],
|
||||
}
|
||||
self.assertEqual(ObjectChange.objects.count(), 0)
|
||||
url = reverse('dcim-api:site-list')
|
||||
@ -50,7 +46,6 @@ class ChangeLogTest(APITestCase):
|
||||
self.assertEqual(oc.changed_object, site)
|
||||
self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_CREATE)
|
||||
self.assertEqual(oc.object_data['custom_fields'], data['custom_fields'])
|
||||
self.assertListEqual(sorted(oc.object_data['tags']), data['tags'])
|
||||
|
||||
def test_update_object(self):
|
||||
site = Site(name='Test Site 1', slug='test-site-1')
|
||||
@ -62,9 +57,6 @@ class ChangeLogTest(APITestCase):
|
||||
'custom_fields': {
|
||||
'my_field': 'DEF'
|
||||
},
|
||||
'tags': [
|
||||
'abc', 'xyz'
|
||||
],
|
||||
}
|
||||
self.assertEqual(ObjectChange.objects.count(), 0)
|
||||
self.add_permissions('dcim.change_site')
|
||||
@ -81,7 +73,6 @@ class ChangeLogTest(APITestCase):
|
||||
self.assertEqual(oc.changed_object, site)
|
||||
self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_UPDATE)
|
||||
self.assertEqual(oc.object_data['custom_fields'], data['custom_fields'])
|
||||
self.assertListEqual(sorted(oc.object_data['tags']), data['tags'])
|
||||
|
||||
def test_delete_object(self):
|
||||
site = Site(
|
||||
@ -89,7 +80,6 @@ class ChangeLogTest(APITestCase):
|
||||
slug='test-site-1'
|
||||
)
|
||||
site.save()
|
||||
site.tags.add('foo', 'bar')
|
||||
CustomFieldValue.objects.create(
|
||||
field=CustomField.objects.get(name='my_field'),
|
||||
obj=site,
|
||||
@ -108,4 +98,3 @@ class ChangeLogTest(APITestCase):
|
||||
self.assertEqual(oc.object_repr, site.name)
|
||||
self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_DELETE)
|
||||
self.assertEqual(oc.object_data['custom_fields'], {'my_field': 'ABC'})
|
||||
self.assertListEqual(sorted(oc.object_data['tags']), ['bar', 'foo'])
|
||||
|
@ -9,42 +9,53 @@ class TaggedItemTest(APITestCase):
|
||||
"""
|
||||
Test the application of Tags to and item (a Site, for example) upon creation (POST) and modification (PATCH).
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super().setUp()
|
||||
|
||||
def test_create_tagged_item(self):
|
||||
tags = self.create_tags("Foo", "Bar", "Baz")
|
||||
data = {
|
||||
'name': 'Test Site',
|
||||
'slug': 'test-site',
|
||||
'tags': ['Foo', 'Bar', 'Baz']
|
||||
'tags': [t.pk for t in tags]
|
||||
}
|
||||
url = reverse('dcim-api:site-list')
|
||||
self.add_permissions('dcim.add_site')
|
||||
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(sorted(response.data['tags']), sorted(data['tags']))
|
||||
self.assertListEqual(
|
||||
sorted([t['id'] for t in response.data['tags']]),
|
||||
sorted(data['tags'])
|
||||
)
|
||||
site = Site.objects.get(pk=response.data['id'])
|
||||
tags = [tag.name for tag in site.tags.all()]
|
||||
self.assertEqual(sorted(tags), sorted(data['tags']))
|
||||
self.assertListEqual(
|
||||
sorted([t.name for t in site.tags.all()]),
|
||||
sorted(["Foo", "Bar", "Baz"])
|
||||
)
|
||||
|
||||
def test_update_tagged_item(self):
|
||||
site = Site.objects.create(
|
||||
name='Test Site',
|
||||
slug='test-site'
|
||||
)
|
||||
site.tags.add('Foo', 'Bar', 'Baz')
|
||||
site.tags.add("Foo", "Bar", "Baz")
|
||||
self.create_tags("New Tag")
|
||||
data = {
|
||||
'tags': ['Foo', 'Bar', 'New Tag']
|
||||
'tags': [
|
||||
{"name": "Foo"},
|
||||
{"name": "Bar"},
|
||||
{"name": "New Tag"},
|
||||
]
|
||||
}
|
||||
self.add_permissions('dcim.change_site')
|
||||
url = reverse('dcim-api:site-detail', kwargs={'pk': site.pk})
|
||||
|
||||
response = self.client.patch(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(sorted(response.data['tags']), sorted(data['tags']))
|
||||
self.assertListEqual(
|
||||
sorted([t['name'] for t in response.data['tags']]),
|
||||
sorted([t['name'] for t in data['tags']])
|
||||
)
|
||||
site = Site.objects.get(pk=response.data['id'])
|
||||
tags = [tag.name for tag in site.tags.all()]
|
||||
self.assertEqual(sorted(tags), sorted(data['tags']))
|
||||
self.assertListEqual(
|
||||
sorted([t.name for t in site.tags.all()]),
|
||||
sorted(["Foo", "Bar", "New Tag"])
|
||||
)
|
||||
|
@ -10,16 +10,7 @@ from extras.models import ConfigContext, ObjectChange, Tag
|
||||
from utilities.testing import ViewTestCases, TestCase
|
||||
|
||||
|
||||
# TODO: Change base class to PrimaryObjectViewTestCase
|
||||
# Blocked by #3703
|
||||
class TagTestCase(
|
||||
ViewTestCases.GetObjectViewTestCase,
|
||||
ViewTestCases.EditObjectViewTestCase,
|
||||
ViewTestCases.DeleteObjectViewTestCase,
|
||||
ViewTestCases.ListObjectsViewTestCase,
|
||||
ViewTestCases.BulkEditObjectsViewTestCase,
|
||||
ViewTestCases.BulkDeleteObjectsViewTestCase
|
||||
):
|
||||
class TagTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = Tag
|
||||
|
||||
@classmethod
|
||||
@ -38,6 +29,13 @@ class TagTestCase(
|
||||
'comments': 'Some comments',
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"name,slug,color,description",
|
||||
"Tag 4,tag-4,ff0000,Fourth tag",
|
||||
"Tag 5,tag-5,00ff00,Fifth tag",
|
||||
"Tag 6,tag-6,0000ff,Sixth tag",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'color': '00ff00',
|
||||
}
|
||||
|
@ -9,6 +9,8 @@ urlpatterns = [
|
||||
|
||||
# Tags
|
||||
path('tags/', views.TagListView.as_view(), name='tag_list'),
|
||||
path('tags/add/', views.TagEditView.as_view(), name='tag_add'),
|
||||
path('tags/import/', views.TagBulkImportView.as_view(), name='tag_import'),
|
||||
path('tags/edit/', views.TagBulkEditView.as_view(), name='tag_bulk_edit'),
|
||||
path('tags/delete/', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'),
|
||||
path('tags/<str:slug>/', views.TagView.as_view(), name='tag'),
|
||||
|
@ -13,14 +13,13 @@ from utilities.forms import ConfirmationForm
|
||||
from utilities.paginator import EnhancedPaginator
|
||||
from utilities.utils import shallow_compare_dict
|
||||
from utilities.views import (
|
||||
BulkDeleteView, BulkEditView, ObjectView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||
BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||
ObjectPermissionRequiredMixin,
|
||||
)
|
||||
from . import filters, forms
|
||||
from . import filters, forms, tables
|
||||
from .models import ConfigContext, ImageAttachment, ObjectChange, ReportResult, Tag, TaggedItem
|
||||
from .reports import get_report, get_reports
|
||||
from .scripts import get_scripts, run_script
|
||||
from .tables import ConfigContextTable, ObjectChangeTable, TagTable, TaggedItemTable
|
||||
|
||||
|
||||
#
|
||||
@ -35,8 +34,7 @@ class TagListView(ObjectListView):
|
||||
)
|
||||
filterset = filters.TagFilterSet
|
||||
filterset_form = forms.TagFilterForm
|
||||
table = TagTable
|
||||
action_buttons = ()
|
||||
table = tables.TagTable
|
||||
|
||||
|
||||
class TagView(ObjectView):
|
||||
@ -52,7 +50,7 @@ class TagView(ObjectView):
|
||||
)
|
||||
|
||||
# Generate a table of all items tagged with this Tag
|
||||
items_table = TaggedItemTable(tagged_items)
|
||||
items_table = tables.TaggedItemTable(tagged_items)
|
||||
paginate = {
|
||||
'paginator_class': EnhancedPaginator,
|
||||
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
||||
@ -78,13 +76,20 @@ class TagDeleteView(ObjectDeleteView):
|
||||
default_return_url = 'extras:tag_list'
|
||||
|
||||
|
||||
class TagBulkImportView(BulkImportView):
|
||||
queryset = Tag.objects.all()
|
||||
model_form = forms.TagCSVForm
|
||||
table = tables.TagTable
|
||||
default_return_url = 'extras:tag_list'
|
||||
|
||||
|
||||
class TagBulkEditView(BulkEditView):
|
||||
queryset = Tag.restricted.annotate(
|
||||
items=Count('extras_taggeditem_items', distinct=True)
|
||||
).order_by(
|
||||
'name'
|
||||
)
|
||||
table = TagTable
|
||||
table = tables.TagTable
|
||||
form = forms.TagBulkEditForm
|
||||
default_return_url = 'extras:tag_list'
|
||||
|
||||
@ -95,7 +100,7 @@ class TagBulkDeleteView(BulkDeleteView):
|
||||
).order_by(
|
||||
'name'
|
||||
)
|
||||
table = TagTable
|
||||
table = tables.TagTable
|
||||
default_return_url = 'extras:tag_list'
|
||||
|
||||
|
||||
@ -107,7 +112,7 @@ class ConfigContextListView(ObjectListView):
|
||||
queryset = ConfigContext.objects.all()
|
||||
filterset = filters.ConfigContextFilterSet
|
||||
filterset_form = forms.ConfigContextFilterForm
|
||||
table = ConfigContextTable
|
||||
table = tables.ConfigContextTable
|
||||
action_buttons = ('add',)
|
||||
|
||||
|
||||
@ -143,7 +148,7 @@ class ConfigContextEditView(ObjectEditView):
|
||||
class ConfigContextBulkEditView(BulkEditView):
|
||||
queryset = ConfigContext.objects.all()
|
||||
filterset = filters.ConfigContextFilterSet
|
||||
table = ConfigContextTable
|
||||
table = tables.ConfigContextTable
|
||||
form = forms.ConfigContextBulkEditForm
|
||||
default_return_url = 'extras:configcontext_list'
|
||||
|
||||
@ -155,7 +160,7 @@ class ConfigContextDeleteView(ObjectDeleteView):
|
||||
|
||||
class ConfigContextBulkDeleteView(BulkDeleteView):
|
||||
queryset = ConfigContext.objects.all()
|
||||
table = ConfigContextTable
|
||||
table = tables.ConfigContextTable
|
||||
default_return_url = 'extras:configcontext_list'
|
||||
|
||||
|
||||
@ -197,7 +202,7 @@ class ObjectChangeListView(ObjectListView):
|
||||
queryset = ObjectChange.objects.prefetch_related('user', 'changed_object_type')
|
||||
filterset = filters.ObjectChangeFilterSet
|
||||
filterset_form = forms.ObjectChangeFilterForm
|
||||
table = ObjectChangeTable
|
||||
table = tables.ObjectChangeTable
|
||||
template_name = 'extras/objectchange_list.html'
|
||||
action_buttons = ('export',)
|
||||
|
||||
@ -214,7 +219,7 @@ class ObjectChangeView(ObjectView):
|
||||
).exclude(
|
||||
pk=objectchange.pk
|
||||
)
|
||||
related_changes_table = ObjectChangeTable(
|
||||
related_changes_table = tables.ObjectChangeTable(
|
||||
data=related_changes[:50],
|
||||
orderable=False
|
||||
)
|
||||
@ -268,7 +273,7 @@ class ObjectChangeLogView(View):
|
||||
Q(changed_object_type=content_type, changed_object_id=obj.pk) |
|
||||
Q(related_object_type=content_type, related_object_id=obj.pk)
|
||||
)
|
||||
objectchanges_table = ObjectChangeTable(
|
||||
objectchanges_table = tables.ObjectChangeTable(
|
||||
data=objectchanges,
|
||||
orderable=False
|
||||
)
|
||||
|
@ -3,11 +3,11 @@ from collections import OrderedDict
|
||||
from rest_framework import serializers
|
||||
from rest_framework.reverse import reverse
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
|
||||
|
||||
from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerializer
|
||||
from dcim.models import Interface
|
||||
from extras.api.customfields import CustomFieldModelSerializer
|
||||
from extras.api.serializers import TaggedObjectSerializer
|
||||
from ipam.choices import *
|
||||
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||
@ -22,9 +22,8 @@ from .nested_serializers import *
|
||||
# VRFs
|
||||
#
|
||||
|
||||
class VRFSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
class VRFSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
ipaddress_count = serializers.IntegerField(read_only=True)
|
||||
prefix_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
@ -48,10 +47,9 @@ class RIRSerializer(ValidatedModelSerializer):
|
||||
fields = ['id', 'name', 'slug', 'is_private', 'description', 'aggregate_count']
|
||||
|
||||
|
||||
class AggregateSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
class AggregateSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
||||
rir = NestedRIRSerializer()
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = Aggregate
|
||||
@ -98,13 +96,12 @@ class VLANGroupSerializer(ValidatedModelSerializer):
|
||||
return data
|
||||
|
||||
|
||||
class VLANSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
class VLANSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
site = NestedSiteSerializer(required=False, allow_null=True)
|
||||
group = NestedVLANGroupSerializer(required=False, allow_null=True)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
status = ChoiceField(choices=VLANStatusChoices, required=False)
|
||||
role = NestedRoleSerializer(required=False, allow_null=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
prefix_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
@ -133,7 +130,7 @@ class VLANSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
# Prefixes
|
||||
#
|
||||
|
||||
class PrefixSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
class PrefixSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
||||
site = NestedSiteSerializer(required=False, allow_null=True)
|
||||
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
||||
@ -141,7 +138,6 @@ class PrefixSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
vlan = NestedVLANSerializer(required=False, allow_null=True)
|
||||
status = ChoiceField(choices=PrefixStatusChoices, required=False)
|
||||
role = NestedRoleSerializer(required=False, allow_null=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = Prefix
|
||||
@ -226,7 +222,7 @@ class IPAddressInterfaceSerializer(WritableNestedSerializer):
|
||||
return reverse(url_name, kwargs={'pk': obj.pk}, request=self.context['request'])
|
||||
|
||||
|
||||
class IPAddressSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
class IPAddressSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
||||
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
@ -235,7 +231,6 @@ class IPAddressSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
interface = IPAddressInterfaceSerializer(required=False, allow_null=True)
|
||||
nat_inside = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||
nat_outside = NestedIPAddressSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
@ -270,7 +265,7 @@ class AvailableIPSerializer(serializers.Serializer):
|
||||
# Services
|
||||
#
|
||||
|
||||
class ServiceSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
class ServiceSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
device = NestedDeviceSerializer(required=False, allow_null=True)
|
||||
virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True)
|
||||
protocol = ChoiceField(choices=ServiceProtocolChoices, required=False)
|
||||
@ -280,7 +275,6 @@ class ServiceSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = Service
|
||||
|
@ -4,8 +4,8 @@ from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from dcim.models import Device, Interface, Rack, Region, Site
|
||||
from extras.forms import (
|
||||
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm, CustomFieldFilterForm,
|
||||
TagField,
|
||||
)
|
||||
from extras.models import Tag
|
||||
from tenancy.forms import TenancyFilterForm, TenancyForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
@ -33,7 +33,8 @@ IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([
|
||||
#
|
||||
|
||||
class VRFForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
@ -141,7 +142,8 @@ class AggregateForm(BootstrapMixin, CustomFieldModelForm):
|
||||
rir = DynamicModelChoiceField(
|
||||
queryset=RIR.objects.all()
|
||||
)
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
@ -292,7 +294,10 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
queryset=Role.objects.all(),
|
||||
required=False
|
||||
)
|
||||
tags = TagField(required=False)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Prefix
|
||||
@ -584,7 +589,8 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
|
||||
required=False,
|
||||
label='Make this the primary IP for the device/VM'
|
||||
)
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
@ -993,7 +999,10 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
queryset=Role.objects.all(),
|
||||
required=False
|
||||
)
|
||||
tags = TagField(required=False)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VLAN
|
||||
@ -1160,7 +1169,8 @@ class ServiceForm(BootstrapMixin, CustomFieldModelForm):
|
||||
min_value=SERVICE_PORT_MIN,
|
||||
max_value=SERVICE_PORT_MAX
|
||||
)
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
|
@ -33,7 +33,7 @@ class VRFTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'tenant': tenants[0].pk,
|
||||
'enforce_unique': True,
|
||||
'description': 'A new VRF',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': cls.create_tags('Alpha', 'Bravo', 'Charlie'),
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
@ -100,7 +100,7 @@ class AggregateTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'rir': rirs[1].pk,
|
||||
'date_added': datetime.date(2020, 1, 1),
|
||||
'description': 'A new aggregate',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': cls.create_tags('Alpha', 'Bravo', 'Charlie'),
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
@ -183,7 +183,7 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'role': roles[1].pk,
|
||||
'is_pool': True,
|
||||
'description': 'A new prefix',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': cls.create_tags('Alpha', 'Bravo', 'Charlie'),
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
@ -232,7 +232,7 @@ class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'nat_inside': None,
|
||||
'dns_name': 'example',
|
||||
'description': 'A new IP address',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': cls.create_tags('Alpha', 'Bravo', 'Charlie'),
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
@ -320,7 +320,7 @@ class VLANTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'status': VLANStatusChoices.STATUS_RESERVED,
|
||||
'role': roles[1].pk,
|
||||
'description': 'A new VLAN',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': cls.create_tags('Alpha', 'Bravo', 'Charlie'),
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
@ -376,7 +376,7 @@ class ServiceTestCase(
|
||||
'port': 999,
|
||||
'ipaddresses': [],
|
||||
'description': 'A new service',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': cls.create_tags('Alpha', 'Bravo', 'Charlie'),
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
|
@ -285,7 +285,6 @@ INSTALLED_APPS = [
|
||||
'mptt',
|
||||
'rest_framework',
|
||||
'taggit',
|
||||
'taggit_serializer',
|
||||
'timezone_field',
|
||||
'circuits',
|
||||
'dcim',
|
||||
@ -489,7 +488,6 @@ SWAGGER_SETTINGS = {
|
||||
'utilities.custom_inspectors.JSONFieldInspector',
|
||||
'utilities.custom_inspectors.NullableBooleanFieldInspector',
|
||||
'utilities.custom_inspectors.CustomChoiceFieldInspector',
|
||||
'utilities.custom_inspectors.TagListFieldInspector',
|
||||
'utilities.custom_inspectors.SerializedPKRelatedFieldInspector',
|
||||
'drf_yasg.inspectors.CamelCaseJSONFilter',
|
||||
'drf_yasg.inspectors.ReferencingSerializerInspector',
|
||||
|
@ -1,8 +1,8 @@
|
||||
from rest_framework import serializers
|
||||
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
|
||||
|
||||
from dcim.api.nested_serializers import NestedDeviceSerializer
|
||||
from extras.api.customfields import CustomFieldModelSerializer
|
||||
from extras.api.serializers import TaggedObjectSerializer
|
||||
from secrets.models import Secret, SecretRole
|
||||
from utilities.api import ValidatedModelSerializer
|
||||
from .nested_serializers import *
|
||||
@ -20,11 +20,10 @@ class SecretRoleSerializer(ValidatedModelSerializer):
|
||||
fields = ['id', 'name', 'slug', 'description', 'secret_count']
|
||||
|
||||
|
||||
class SecretSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
class SecretSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
role = NestedSecretRoleSerializer()
|
||||
plaintext = serializers.CharField()
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = Secret
|
||||
|
@ -5,8 +5,8 @@ from django import forms
|
||||
from dcim.models import Device
|
||||
from extras.forms import (
|
||||
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm,
|
||||
TagField,
|
||||
)
|
||||
from extras.models import Tag
|
||||
from utilities.forms import (
|
||||
APISelectMultiple, BootstrapMixin, CSVModelChoiceField, CSVModelForm, DynamicModelChoiceField,
|
||||
DynamicModelMultipleChoiceField, SlugField, StaticSelect2Multiple, TagFilterField,
|
||||
@ -90,7 +90,8 @@ class SecretForm(BootstrapMixin, CustomFieldModelForm):
|
||||
role = DynamicModelChoiceField(
|
||||
queryset=SecretRole.objects.all()
|
||||
)
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
|
@ -85,7 +85,7 @@
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>
|
||||
{{ tag.description }}
|
||||
{{ tag.description|placeholder }}
|
||||
</td>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -102,6 +102,12 @@
|
||||
<li class="divider"></li>
|
||||
<li class="dropdown-header">Tags</li>
|
||||
<li{% if not perms.extras.view_tag %} class="disabled"{% endif %}>
|
||||
{% if perms.extras.add_tag %}
|
||||
<div class="buttons pull-right">
|
||||
<a href="{% url 'extras:tag_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
|
||||
<a href="{% url 'extras:tag_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="{% url 'extras:tag_list' %}">Tags</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -1,7 +1,7 @@
|
||||
from rest_framework import serializers
|
||||
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
|
||||
|
||||
from extras.api.customfields import CustomFieldModelSerializer
|
||||
from extras.api.serializers import TaggedObjectSerializer
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.api import ValidatedModelSerializer
|
||||
from .nested_serializers import *
|
||||
@ -20,9 +20,8 @@ class TenantGroupSerializer(ValidatedModelSerializer):
|
||||
fields = ['id', 'name', 'slug', 'parent', 'description', 'tenant_count']
|
||||
|
||||
|
||||
class TenantSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
class TenantSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
group = NestedTenantGroupSerializer(required=False)
|
||||
tags = TagListSerializerField(required=False)
|
||||
circuit_count = serializers.IntegerField(read_only=True)
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
ipaddress_count = serializers.IntegerField(read_only=True)
|
||||
|
@ -2,8 +2,8 @@ from django import forms
|
||||
|
||||
from extras.forms import (
|
||||
AddRemoveTagsForm, CustomFieldModelForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelCSVForm,
|
||||
TagField,
|
||||
)
|
||||
from extras.models import Tag
|
||||
from utilities.forms import (
|
||||
APISelect, APISelectMultiple, BootstrapMixin, CommentField, CSVModelChoiceField, CSVModelForm,
|
||||
DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField, TagFilterField,
|
||||
@ -57,7 +57,8 @@ class TenantForm(BootstrapMixin, CustomFieldModelForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField()
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
|
@ -55,7 +55,7 @@ class TenantTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'group': tenant_groups[1].pk,
|
||||
'description': 'A new tenant',
|
||||
'comments': 'Some comments',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': cls.create_tags('Alpha', 'Bravo', 'Charlie'),
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
|
@ -1,10 +1,9 @@
|
||||
from django.contrib.postgres.fields import JSONField
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.inspectors import FieldInspector, NotHandled, PaginatorInspector, FilterInspector, SwaggerAutoSchema
|
||||
from drf_yasg.inspectors import FieldInspector, NotHandled, PaginatorInspector, SwaggerAutoSchema
|
||||
from drf_yasg.utils import get_serializer_ref_name
|
||||
from rest_framework.fields import ChoiceField
|
||||
from rest_framework.relations import ManyRelatedField
|
||||
from taggit_serializer.serializers import TagListSerializerField
|
||||
|
||||
from dcim.api.serializers import InterfaceSerializer as DeviceInterfaceSerializer
|
||||
from extras.api.customfields import CustomFieldsSerializer
|
||||
@ -56,19 +55,6 @@ class SerializedPKRelatedFieldInspector(FieldInspector):
|
||||
return NotHandled
|
||||
|
||||
|
||||
class TagListFieldInspector(FieldInspector):
|
||||
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
|
||||
SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
|
||||
if isinstance(field, TagListSerializerField):
|
||||
child_schema = self.probe_field_inspectors(field.child, ChildSwaggerType, use_references)
|
||||
return SwaggerType(
|
||||
type=openapi.TYPE_ARRAY,
|
||||
items=child_schema,
|
||||
)
|
||||
|
||||
return NotHandled
|
||||
|
||||
|
||||
class CustomChoiceFieldInspector(FieldInspector):
|
||||
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
|
||||
# this returns a callable which extracts title, description and other stuff
|
||||
|
@ -12,6 +12,9 @@ class DummyQuerySet:
|
||||
def __init__(self, queryset):
|
||||
self._cache = [obj for obj in queryset.all()]
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._cache)
|
||||
|
||||
def all(self):
|
||||
return self._cache
|
||||
|
||||
|
@ -14,7 +14,14 @@ def post_data(data):
|
||||
if value is None:
|
||||
ret[key] = ''
|
||||
elif type(value) in (list, tuple):
|
||||
ret[key] = value
|
||||
if value and hasattr(value[0], 'pk'):
|
||||
# Value is a list of instances
|
||||
ret[key] = [v.pk for v in value]
|
||||
else:
|
||||
ret[key] = value
|
||||
elif hasattr(value, 'pk'):
|
||||
# Value is an instance
|
||||
ret[key] = value.pk
|
||||
else:
|
||||
ret[key] = str(value)
|
||||
|
||||
|
@ -6,8 +6,10 @@ from django.db.models import ForeignKey, ManyToManyField
|
||||
from django.forms.models import model_to_dict
|
||||
from django.test import Client, TestCase as _TestCase, override_settings
|
||||
from django.urls import reverse, NoReverseMatch
|
||||
from django.utils.text import slugify
|
||||
from netaddr import IPNetwork
|
||||
|
||||
from extras.models import Tag
|
||||
from users.models import ObjectPermission
|
||||
from utilities.permissions import resolve_permission_ct
|
||||
from .utils import disable_warnings, post_data
|
||||
@ -49,7 +51,7 @@ class TestCase(_TestCase):
|
||||
obj_perm.object_types.add(ct)
|
||||
|
||||
#
|
||||
# Convenience methods
|
||||
# Custom assertions
|
||||
#
|
||||
|
||||
def assertHttpStatus(self, response, expected_status):
|
||||
@ -75,7 +77,7 @@ class TestCase(_TestCase):
|
||||
|
||||
# TODO: Differentiate between tags assigned to the instance and a M2M field for tags (ex: ConfigContext)
|
||||
if key == 'tags':
|
||||
model_dict[key] = ','.join(sorted([tag.name for tag in value]))
|
||||
model_dict[key] = sorted(value)
|
||||
|
||||
# Convert ManyToManyField to list of instance PKs
|
||||
elif model_dict[key] and type(value) in (list, tuple) and hasattr(value[0], 'pk'):
|
||||
@ -108,6 +110,19 @@ class TestCase(_TestCase):
|
||||
|
||||
self.assertDictEqual(model_dict, relevant_data)
|
||||
|
||||
#
|
||||
# Convenience methods
|
||||
#
|
||||
|
||||
@classmethod
|
||||
def create_tags(cls, *names):
|
||||
"""
|
||||
Create and return a Tag instance for each name given.
|
||||
"""
|
||||
tags = [Tag(name=name, slug=slugify(name)) for name in names]
|
||||
Tag.objects.bulk_create(tags)
|
||||
return tags
|
||||
|
||||
|
||||
#
|
||||
# UI Tests
|
||||
|
@ -1,11 +1,11 @@
|
||||
from drf_yasg.utils import swagger_serializer_method
|
||||
from rest_framework import serializers
|
||||
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
|
||||
|
||||
from dcim.api.nested_serializers import NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer
|
||||
from dcim.choices import InterfaceModeChoices, InterfaceTypeChoices
|
||||
from dcim.choices import InterfaceModeChoices
|
||||
from dcim.models import Interface
|
||||
from extras.api.customfields import CustomFieldModelSerializer
|
||||
from extras.api.serializers import TaggedObjectSerializer
|
||||
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
|
||||
from ipam.models import VLAN
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||
@ -35,12 +35,11 @@ class ClusterGroupSerializer(ValidatedModelSerializer):
|
||||
fields = ['id', 'name', 'slug', 'description', 'cluster_count']
|
||||
|
||||
|
||||
class ClusterSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
class ClusterSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
type = NestedClusterTypeSerializer()
|
||||
group = NestedClusterGroupSerializer(required=False, allow_null=True)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
site = NestedSiteSerializer(required=False, allow_null=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
virtualmachine_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
@ -56,7 +55,7 @@ class ClusterSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
# Virtual machines
|
||||
#
|
||||
|
||||
class VirtualMachineSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
class VirtualMachineSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
status = ChoiceField(choices=VirtualMachineStatusChoices, required=False)
|
||||
site = NestedSiteSerializer(read_only=True)
|
||||
cluster = NestedClusterSerializer()
|
||||
@ -66,7 +65,6 @@ class VirtualMachineSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
primary_ip = NestedIPAddressSerializer(read_only=True)
|
||||
primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||
primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = VirtualMachine
|
||||
@ -97,7 +95,7 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
|
||||
# VM interfaces
|
||||
#
|
||||
|
||||
class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
class InterfaceSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
|
||||
virtual_machine = NestedVirtualMachineSerializer()
|
||||
type = ChoiceField(choices=VMInterfaceTypeChoices, default=VMInterfaceTypeChoices.TYPE_VIRTUAL, required=False)
|
||||
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
|
||||
@ -108,7 +106,6 @@ class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
|
@ -7,8 +7,8 @@ from dcim.forms import INTERFACE_MODE_HELP_TEXT
|
||||
from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, Site
|
||||
from extras.forms import (
|
||||
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm, CustomFieldFilterForm,
|
||||
TagField,
|
||||
)
|
||||
from extras.models import Tag
|
||||
from ipam.models import IPAddress, VLAN
|
||||
from tenancy.forms import TenancyFilterForm, TenancyForm
|
||||
from tenancy.models import Tenant
|
||||
@ -83,7 +83,8 @@ class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField()
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
@ -312,13 +313,14 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
queryset=Platform.objects.all(),
|
||||
required=False
|
||||
)
|
||||
tags = TagField(
|
||||
required=False
|
||||
)
|
||||
local_context_data = JSONField(
|
||||
required=False,
|
||||
label=''
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VirtualMachine
|
||||
@ -590,7 +592,8 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
|
||||
},
|
||||
)
|
||||
)
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
@ -697,7 +700,8 @@ class InterfaceCreateForm(BootstrapMixin, forms.Form):
|
||||
},
|
||||
)
|
||||
)
|
||||
tags = TagField(
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
|
@ -97,7 +97,7 @@ class ClusterTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'tenant': None,
|
||||
'site': sites[1].pk,
|
||||
'comments': 'Some comments',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': cls.create_tags('Alpha', 'Bravo', 'Charlie'),
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
@ -161,7 +161,7 @@ class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'memory': 32768,
|
||||
'disk': 4000,
|
||||
'comments': 'Some comments',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': cls.create_tags('Alpha', 'Bravo', 'Charlie'),
|
||||
'local_context_data': None,
|
||||
}
|
||||
|
||||
@ -228,6 +228,8 @@ class InterfaceTestCase(
|
||||
)
|
||||
VLAN.objects.bulk_create(vlans)
|
||||
|
||||
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'virtual_machine': virtualmachines[1].pk,
|
||||
'name': 'Interface X',
|
||||
@ -240,7 +242,7 @@ class InterfaceTestCase(
|
||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||
'untagged_vlan': vlans[0].pk,
|
||||
'tagged_vlans': [v.pk for v in vlans[1:4]],
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': tags,
|
||||
}
|
||||
|
||||
cls.bulk_create_data = {
|
||||
@ -255,7 +257,7 @@ class InterfaceTestCase(
|
||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||
'untagged_vlan': vlans[0].pk,
|
||||
'tagged_vlans': [v.pk for v in vlans[1:4]],
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
'tags': tags,
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
|
@ -9,7 +9,6 @@ django-prometheus==2.0.0
|
||||
django-rq==2.3.2
|
||||
django-tables2==2.3.1
|
||||
django-taggit==1.2.0
|
||||
django-taggit-serializer==0.1.7
|
||||
django-timezone-field==4.0
|
||||
djangorestframework==3.11.0
|
||||
drf-yasg[validation]==1.17.1
|
||||
|
Loading…
Reference in New Issue
Block a user