diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 45689a397..4c3e81861 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -4,6 +4,7 @@ from collections import OrderedDict from rest_framework import serializers from rest_framework.validators import UniqueTogetherValidator +from taggit.models import Tag from circuits.models import Circuit, CircuitTermination from dcim.constants import ( @@ -21,7 +22,8 @@ from ipam.models import IPAddress, VLAN from tenancy.api.serializers import NestedTenantSerializer from users.api.serializers import NestedUserSerializer from utilities.api import ( - ChoiceFieldSerializer, SerializedPKRelatedField, TimeZoneField, ValidatedModelSerializer, WritableNestedSerializer, + ChoiceFieldSerializer, SerializedPKRelatedField, TagField, TimeZoneField, ValidatedModelSerializer, + WritableNestedSerializer, ) from virtualization.models import Cluster @@ -55,14 +57,15 @@ class SiteSerializer(CustomFieldModelSerializer): region = NestedRegionSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True) time_zone = TimeZoneField(required=False) + tags = TagField(queryset=Tag.objects.all(), required=False, many=True) class Meta: model = Site fields = [ 'id', 'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', '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) type = ChoiceFieldSerializer(choices=RACK_TYPE_CHOICES, required=False) width = ChoiceFieldSerializer(choices=RACK_WIDTH_CHOICES, required=False) + tags = TagField(queryset=Tag.objects.all(), required=False, many=True) class Meta: model = Rack fields = [ '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 # 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) subdevice_role = ChoiceFieldSerializer(choices=SUBDEVICE_ROLE_CHOICES, required=False) instance_count = serializers.IntegerField(source='instances.count', read_only=True) + tags = TagField(queryset=Tag.objects.all(), required=False, many=True) class Meta: model = DeviceType fields = [ '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', ] @@ -401,13 +406,14 @@ class DeviceSerializer(CustomFieldModelSerializer): parent_device = serializers.SerializerMethodField() cluster = NestedClusterSerializer(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: model = Device fields = [ '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', - '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', ] validators = [] diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 05dc0ea6f..1d836028e 100644 --- a/netbox/dcim/forms.py +++ b/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.db.models import Count, Q from mptt.forms import TreeNodeChoiceField +from taggit.forms import TagField from timezone_field import TimeZoneFormField from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm @@ -108,12 +109,14 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm): region = TreeNodeChoiceField(queryset=Region.objects.all(), required=False) slug = SlugField() comments = CommentField() + tags = TagField(required=False) class Meta: model = Site fields = [ 'name', 'slug', 'status', 'region', 'tenant_group', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address', 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments', + 'tags', ] widgets = { 'physical_address': SmallTextarea(attrs={'rows': 3}), @@ -274,12 +277,13 @@ class RackForm(BootstrapMixin, TenancyForm, CustomFieldForm): ) ) comments = CommentField() + tags = TagField(required=False) class Meta: model = Rack fields = [ '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 = { 'site': "The site at which the rack exists", @@ -485,11 +489,14 @@ class ManufacturerCSVForm(forms.ModelForm): class DeviceTypeForm(BootstrapMixin, CustomFieldForm): slug = SlugField(slug_source='model') + tags = TagField(required=False) class Meta: 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 = { 'interface_ordering': 'Order interfaces by', } @@ -772,12 +779,13 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm): ) ) comments = CommentField() + tags = TagField(required=False) class Meta: model = Device 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 = { 'device_role': "The function this device serves", diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 8c47c7ba6..27d752352 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -14,6 +14,7 @@ from django.db.models import Count, Q, ObjectDoesNotExist from django.urls import reverse from django.utils.encoding import python_2_unicode_compatible from mptt.models import MPTTModel, TreeForeignKey +from taggit.managers import TaggableManager from timezone_field import TimeZoneField from circuits.models import Circuit @@ -161,6 +162,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel): ) objects = SiteManager() + tags = TaggableManager() csv_headers = [ 'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address', @@ -388,6 +390,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel): ) objects = RackManager() + tags = TaggableManager() csv_headers = [ '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' ) + tags = TaggableManager() + csv_headers = [ 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'interface_ordering', 'comments', @@ -1231,6 +1236,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel): ) objects = DeviceManager() + tags = TaggableManager() csv_headers = [ 'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status', diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 7a90506b4..ad313a593 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -133,6 +133,7 @@ INSTALLED_APPS = ( 'django_tables2', 'mptt', 'rest_framework', + 'taggit', 'timezone_field', 'circuits', 'dcim', diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index e2253d4f4..69c408494 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -96,6 +96,10 @@ {% endif %} +