mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-23 04:22:01 -06:00
Initial work on implementing django-taggit for #132
This commit is contained in:
parent
57f6d22c64
commit
b0dafcf50f
@ -4,6 +4,7 @@ from collections import OrderedDict
|
|||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.validators import UniqueTogetherValidator
|
from rest_framework.validators import UniqueTogetherValidator
|
||||||
|
from taggit.models import Tag
|
||||||
|
|
||||||
from circuits.models import Circuit, CircuitTermination
|
from circuits.models import Circuit, CircuitTermination
|
||||||
from dcim.constants import (
|
from dcim.constants import (
|
||||||
@ -21,7 +22,8 @@ from ipam.models import IPAddress, VLAN
|
|||||||
from tenancy.api.serializers import NestedTenantSerializer
|
from tenancy.api.serializers import NestedTenantSerializer
|
||||||
from users.api.serializers import NestedUserSerializer
|
from users.api.serializers import NestedUserSerializer
|
||||||
from utilities.api import (
|
from utilities.api import (
|
||||||
ChoiceFieldSerializer, SerializedPKRelatedField, TimeZoneField, ValidatedModelSerializer, WritableNestedSerializer,
|
ChoiceFieldSerializer, SerializedPKRelatedField, TagField, TimeZoneField, ValidatedModelSerializer,
|
||||||
|
WritableNestedSerializer,
|
||||||
)
|
)
|
||||||
from virtualization.models import Cluster
|
from virtualization.models import Cluster
|
||||||
|
|
||||||
@ -55,14 +57,15 @@ class SiteSerializer(CustomFieldModelSerializer):
|
|||||||
region = NestedRegionSerializer(required=False, allow_null=True)
|
region = NestedRegionSerializer(required=False, allow_null=True)
|
||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
time_zone = TimeZoneField(required=False)
|
time_zone = TimeZoneField(required=False)
|
||||||
|
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Site
|
model = Site
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description',
|
'id', 'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description',
|
||||||
'physical_address', 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments',
|
'physical_address', 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments',
|
||||||
'custom_fields', 'created', 'last_updated', 'count_prefixes', 'count_vlans', 'count_racks', 'count_devices',
|
'tags', 'custom_fields', 'created', 'last_updated', 'count_prefixes', 'count_vlans', 'count_racks',
|
||||||
'count_circuits',
|
'count_devices', 'count_circuits',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -124,12 +127,13 @@ class RackSerializer(CustomFieldModelSerializer):
|
|||||||
role = NestedRackRoleSerializer(required=False, allow_null=True)
|
role = NestedRackRoleSerializer(required=False, allow_null=True)
|
||||||
type = ChoiceFieldSerializer(choices=RACK_TYPE_CHOICES, required=False)
|
type = ChoiceFieldSerializer(choices=RACK_TYPE_CHOICES, required=False)
|
||||||
width = ChoiceFieldSerializer(choices=RACK_WIDTH_CHOICES, required=False)
|
width = ChoiceFieldSerializer(choices=RACK_WIDTH_CHOICES, required=False)
|
||||||
|
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Rack
|
model = Rack
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'serial', 'type', 'width',
|
'id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'serial', 'type', 'width',
|
||||||
'u_height', 'desc_units', 'comments', 'custom_fields', 'created', 'last_updated',
|
'u_height', 'desc_units', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
# Omit the UniqueTogetherValidator that would be automatically added to validate (site, facility_id). This
|
# Omit the UniqueTogetherValidator that would be automatically added to validate (site, facility_id). This
|
||||||
# prevents facility_id from being interpreted as a required field.
|
# prevents facility_id from being interpreted as a required field.
|
||||||
@ -223,12 +227,13 @@ class DeviceTypeSerializer(CustomFieldModelSerializer):
|
|||||||
interface_ordering = ChoiceFieldSerializer(choices=IFACE_ORDERING_CHOICES, required=False)
|
interface_ordering = ChoiceFieldSerializer(choices=IFACE_ORDERING_CHOICES, required=False)
|
||||||
subdevice_role = ChoiceFieldSerializer(choices=SUBDEVICE_ROLE_CHOICES, required=False)
|
subdevice_role = ChoiceFieldSerializer(choices=SUBDEVICE_ROLE_CHOICES, required=False)
|
||||||
instance_count = serializers.IntegerField(source='instances.count', read_only=True)
|
instance_count = serializers.IntegerField(source='instances.count', read_only=True)
|
||||||
|
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'interface_ordering',
|
'id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'interface_ordering',
|
||||||
'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', 'custom_fields',
|
'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', 'tags', 'custom_fields',
|
||||||
'instance_count',
|
'instance_count',
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -401,13 +406,14 @@ class DeviceSerializer(CustomFieldModelSerializer):
|
|||||||
parent_device = serializers.SerializerMethodField()
|
parent_device = serializers.SerializerMethodField()
|
||||||
cluster = NestedClusterSerializer(required=False, allow_null=True)
|
cluster = NestedClusterSerializer(required=False, allow_null=True)
|
||||||
virtual_chassis = DeviceVirtualChassisSerializer(required=False, allow_null=True)
|
virtual_chassis = DeviceVirtualChassisSerializer(required=False, allow_null=True)
|
||||||
|
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Device
|
model = Device
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
|
'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
|
||||||
'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6',
|
'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6',
|
||||||
'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'custom_fields', 'created',
|
'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', 'custom_fields', 'created',
|
||||||
'last_updated',
|
'last_updated',
|
||||||
]
|
]
|
||||||
validators = []
|
validators = []
|
||||||
|
@ -7,6 +7,7 @@ from django.contrib.auth.models import User
|
|||||||
from django.contrib.postgres.forms.array import SimpleArrayField
|
from django.contrib.postgres.forms.array import SimpleArrayField
|
||||||
from django.db.models import Count, Q
|
from django.db.models import Count, Q
|
||||||
from mptt.forms import TreeNodeChoiceField
|
from mptt.forms import TreeNodeChoiceField
|
||||||
|
from taggit.forms import TagField
|
||||||
from timezone_field import TimeZoneFormField
|
from timezone_field import TimeZoneFormField
|
||||||
|
|
||||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||||
@ -108,12 +109,14 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
region = TreeNodeChoiceField(queryset=Region.objects.all(), required=False)
|
region = TreeNodeChoiceField(queryset=Region.objects.all(), required=False)
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
tags = TagField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Site
|
model = Site
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug', 'status', 'region', 'tenant_group', 'tenant', 'facility', 'asn', 'time_zone', 'description',
|
'name', 'slug', 'status', 'region', 'tenant_group', 'tenant', 'facility', 'asn', 'time_zone', 'description',
|
||||||
'physical_address', 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments',
|
'physical_address', 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments',
|
||||||
|
'tags',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'physical_address': SmallTextarea(attrs={'rows': 3}),
|
'physical_address': SmallTextarea(attrs={'rows': 3}),
|
||||||
@ -274,12 +277,13 @@ class RackForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
tags = TagField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Rack
|
model = Rack
|
||||||
fields = [
|
fields = [
|
||||||
'site', 'group', 'name', 'facility_id', 'tenant_group', 'tenant', 'role', 'serial', 'type', 'width',
|
'site', 'group', 'name', 'facility_id', 'tenant_group', 'tenant', 'role', 'serial', 'type', 'width',
|
||||||
'u_height', 'desc_units', 'comments',
|
'u_height', 'desc_units', 'comments', 'tags',
|
||||||
]
|
]
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'site': "The site at which the rack exists",
|
'site': "The site at which the rack exists",
|
||||||
@ -485,11 +489,14 @@ class ManufacturerCSVForm(forms.ModelForm):
|
|||||||
|
|
||||||
class DeviceTypeForm(BootstrapMixin, CustomFieldForm):
|
class DeviceTypeForm(BootstrapMixin, CustomFieldForm):
|
||||||
slug = SlugField(slug_source='model')
|
slug = SlugField(slug_source='model')
|
||||||
|
tags = TagField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = ['manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server',
|
fields = [
|
||||||
'is_pdu', 'is_network_device', 'subdevice_role', 'interface_ordering', 'comments']
|
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
|
||||||
|
'is_network_device', 'subdevice_role', 'interface_ordering', 'comments', 'tags',
|
||||||
|
]
|
||||||
labels = {
|
labels = {
|
||||||
'interface_ordering': 'Order interfaces by',
|
'interface_ordering': 'Order interfaces by',
|
||||||
}
|
}
|
||||||
@ -772,12 +779,13 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
tags = TagField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Device
|
model = Device
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'position', 'face', 'status',
|
'name', 'device_role', 'tags', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'position', 'face',
|
||||||
'platform', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant', 'comments',
|
'status', 'platform', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant', 'comments',
|
||||||
]
|
]
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'device_role': "The function this device serves",
|
'device_role': "The function this device serves",
|
||||||
|
@ -14,6 +14,7 @@ from django.db.models import Count, Q, ObjectDoesNotExist
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.encoding import python_2_unicode_compatible
|
from django.utils.encoding import python_2_unicode_compatible
|
||||||
from mptt.models import MPTTModel, TreeForeignKey
|
from mptt.models import MPTTModel, TreeForeignKey
|
||||||
|
from taggit.managers import TaggableManager
|
||||||
from timezone_field import TimeZoneField
|
from timezone_field import TimeZoneField
|
||||||
|
|
||||||
from circuits.models import Circuit
|
from circuits.models import Circuit
|
||||||
@ -161,6 +162,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
objects = SiteManager()
|
objects = SiteManager()
|
||||||
|
tags = TaggableManager()
|
||||||
|
|
||||||
csv_headers = [
|
csv_headers = [
|
||||||
'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address',
|
'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address',
|
||||||
@ -388,6 +390,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
objects = RackManager()
|
objects = RackManager()
|
||||||
|
tags = TaggableManager()
|
||||||
|
|
||||||
csv_headers = [
|
csv_headers = [
|
||||||
'site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'serial', 'width', 'u_height',
|
'site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'serial', 'width', 'u_height',
|
||||||
@ -746,6 +749,8 @@ class DeviceType(models.Model, CustomFieldModel):
|
|||||||
object_id_field='obj_id'
|
object_id_field='obj_id'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
tags = TaggableManager()
|
||||||
|
|
||||||
csv_headers = [
|
csv_headers = [
|
||||||
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server',
|
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server',
|
||||||
'is_pdu', 'is_network_device', 'subdevice_role', 'interface_ordering', 'comments',
|
'is_pdu', 'is_network_device', 'subdevice_role', 'interface_ordering', 'comments',
|
||||||
@ -1231,6 +1236,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
objects = DeviceManager()
|
objects = DeviceManager()
|
||||||
|
tags = TaggableManager()
|
||||||
|
|
||||||
csv_headers = [
|
csv_headers = [
|
||||||
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
|
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
|
||||||
|
@ -133,6 +133,7 @@ INSTALLED_APPS = (
|
|||||||
'django_tables2',
|
'django_tables2',
|
||||||
'mptt',
|
'mptt',
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
|
'taggit',
|
||||||
'timezone_field',
|
'timezone_field',
|
||||||
'circuits',
|
'circuits',
|
||||||
'dcim',
|
'dcim',
|
||||||
|
@ -96,6 +96,10 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Tags</td>
|
||||||
|
<td>{{ device.tags.all|join:" " }}</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% if vc_members %}
|
{% if vc_members %}
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{% render_field form.name %}
|
{% render_field form.name %}
|
||||||
{% render_field form.device_role %}
|
{% render_field form.device_role %}
|
||||||
|
{% render_field form.tags %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
|
@ -73,6 +73,10 @@
|
|||||||
<td>Interface Ordering</td>
|
<td>Interface Ordering</td>
|
||||||
<td>{{ devicetype.get_interface_ordering_display }}</td>
|
<td>{{ devicetype.get_interface_ordering_display }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Tags</td>
|
||||||
|
<td>{{ devicetype.tags.all|join:" " }}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Instances</td>
|
<td>Instances</td>
|
||||||
<td><a href="{% url 'dcim:device_list' %}?device_type_id={{ devicetype.pk }}">{{ devicetype.instances.count }}</a></td>
|
<td><a href="{% url 'dcim:device_list' %}?device_type_id={{ devicetype.pk }}">{{ devicetype.instances.count }}</a></td>
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
{% render_field form.u_height %}
|
{% render_field form.u_height %}
|
||||||
{% render_field form.is_full_depth %}
|
{% render_field form.is_full_depth %}
|
||||||
{% render_field form.interface_ordering %}
|
{% render_field form.interface_ordering %}
|
||||||
|
{% render_field form.tags %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
|
@ -114,6 +114,10 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Tags</td>
|
||||||
|
<td>{{ rack.tags.all|join:" " }}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Devices</td>
|
<td>Devices</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
{% render_field form.group %}
|
{% render_field form.group %}
|
||||||
{% render_field form.role %}
|
{% render_field form.role %}
|
||||||
{% render_field form.serial %}
|
{% render_field form.serial %}
|
||||||
|
{% render_field form.tags %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
|
@ -133,6 +133,10 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Tags</td>
|
||||||
|
<td>{{ site.tags.all|join:" " }}</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
{% render_field form.asn %}
|
{% render_field form.asn %}
|
||||||
{% render_field form.time_zone %}
|
{% render_field form.time_zone %}
|
||||||
{% render_field form.description %}
|
{% render_field form.description %}
|
||||||
|
{% render_field form.tags %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
|
@ -13,7 +13,7 @@ from rest_framework.exceptions import APIException
|
|||||||
from rest_framework.permissions import BasePermission
|
from rest_framework.permissions import BasePermission
|
||||||
from rest_framework.relations import PrimaryKeyRelatedField
|
from rest_framework.relations import PrimaryKeyRelatedField
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import Field, ModelSerializer, ValidationError
|
from rest_framework.serializers import Field, ModelSerializer, RelatedField, ValidationError
|
||||||
from rest_framework.viewsets import GenericViewSet, ViewSet
|
from rest_framework.viewsets import GenericViewSet, ViewSet
|
||||||
|
|
||||||
WRITE_OPERATIONS = ['create', 'update', 'partial_update', 'delete']
|
WRITE_OPERATIONS = ['create', 'update', 'partial_update', 'delete']
|
||||||
@ -42,6 +42,21 @@ class IsAuthenticatedOrLoginNotRequired(BasePermission):
|
|||||||
# Fields
|
# Fields
|
||||||
#
|
#
|
||||||
|
|
||||||
|
class TagField(RelatedField):
|
||||||
|
"""
|
||||||
|
Represent a writable list of Tags associated with an object (use with many=True).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
obj = self.parent.parent.instance
|
||||||
|
content_type = ContentType.objects.get_for_model(obj)
|
||||||
|
tag, _ = Tag.objects.get_or_create(content_type=content_type, object_id=obj.pk, name=data)
|
||||||
|
return tag
|
||||||
|
|
||||||
|
def to_representation(self, value):
|
||||||
|
return value.name
|
||||||
|
|
||||||
|
|
||||||
class ChoiceFieldSerializer(Field):
|
class ChoiceFieldSerializer(Field):
|
||||||
"""
|
"""
|
||||||
Represent a ChoiceField as {'value': <DB value>, 'label': <string>}.
|
Represent a ChoiceField as {'value': <DB value>, 'label': <string>}.
|
||||||
|
Loading…
Reference in New Issue
Block a user