Explorar o código

Initial work on implementing django-taggit for #132

Jeremy Stretch %!s(int64=7) %!d(string=hai) anos
pai
achega
b0dafcf50f

+ 12 - 6
netbox/dcim/api/serializers.py

@@ -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',
-            'count_circuits',
+            'tags', 'custom_fields', 'created', 'last_updated', 'count_prefixes', 'count_vlans', 'count_racks',
+            '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 = []

+ 13 - 5
netbox/dcim/forms.py

@@ -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',
-                  'is_pdu', 'is_network_device', 'subdevice_role', 'interface_ordering', 'comments']
+        fields = [
+            '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',
-            'platform', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant', 'comments',
+            'name', 'device_role', 'tags', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'position', 'face',
+            '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",

+ 6 - 0
netbox/dcim/models.py

@@ -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',

+ 1 - 0
netbox/netbox/settings.py

@@ -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',

+ 4 - 0
netbox/templates/dcim/device.html

@@ -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 %}

+ 1 - 0
netbox/templates/dcim/device_edit.html

@@ -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">

+ 4 - 0
netbox/templates/dcim/devicetype.html

@@ -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>

+ 1 - 0
netbox/templates/dcim/devicetype_edit.html

@@ -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">

+ 4 - 0
netbox/templates/dcim/rack.html

@@ -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>

+ 1 - 0
netbox/templates/dcim/rack_edit.html

@@ -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">

+ 4 - 0
netbox/templates/dcim/site.html

@@ -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">

+ 1 - 0
netbox/templates/dcim/site_edit.html

@@ -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">

+ 16 - 1
netbox/utilities/api.py

@@ -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>}.