From b0dafcf50f66dac20d3f13cc15a8d80fd3ab3113 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 8 May 2018 16:28:26 -0400 Subject: [PATCH 01/12] Initial work on implementing django-taggit for #132 --- netbox/dcim/api/serializers.py | 18 ++++++++++++------ netbox/dcim/forms.py | 18 +++++++++++++----- netbox/dcim/models.py | 6 ++++++ netbox/netbox/settings.py | 1 + netbox/templates/dcim/device.html | 4 ++++ netbox/templates/dcim/device_edit.html | 1 + netbox/templates/dcim/devicetype.html | 4 ++++ netbox/templates/dcim/devicetype_edit.html | 1 + netbox/templates/dcim/rack.html | 4 ++++ netbox/templates/dcim/rack_edit.html | 1 + netbox/templates/dcim/site.html | 4 ++++ netbox/templates/dcim/site_edit.html | 1 + netbox/utilities/api.py | 17 ++++++++++++++++- 13 files changed, 68 insertions(+), 12 deletions(-) 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 %} + + Tags + {{ device.tags.all|join:" " }} + {% if vc_members %} diff --git a/netbox/templates/dcim/device_edit.html b/netbox/templates/dcim/device_edit.html index 07206ca27..1b7a8a9a8 100644 --- a/netbox/templates/dcim/device_edit.html +++ b/netbox/templates/dcim/device_edit.html @@ -7,6 +7,7 @@
{% render_field form.name %} {% render_field form.device_role %} + {% render_field form.tags %}
diff --git a/netbox/templates/dcim/devicetype.html b/netbox/templates/dcim/devicetype.html index 12281734b..f169d9c8c 100644 --- a/netbox/templates/dcim/devicetype.html +++ b/netbox/templates/dcim/devicetype.html @@ -73,6 +73,10 @@ Interface Ordering {{ devicetype.get_interface_ordering_display }} + + Tags + {{ devicetype.tags.all|join:" " }} + Instances {{ devicetype.instances.count }} diff --git a/netbox/templates/dcim/devicetype_edit.html b/netbox/templates/dcim/devicetype_edit.html index d2a107607..2d7a5b132 100644 --- a/netbox/templates/dcim/devicetype_edit.html +++ b/netbox/templates/dcim/devicetype_edit.html @@ -12,6 +12,7 @@ {% render_field form.u_height %} {% render_field form.is_full_depth %} {% render_field form.interface_ordering %} + {% render_field form.tags %}
diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index 28a9dfb6f..3acd0ad46 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -114,6 +114,10 @@ {% endif %} + + Tags + {{ rack.tags.all|join:" " }} + Devices diff --git a/netbox/templates/dcim/rack_edit.html b/netbox/templates/dcim/rack_edit.html index 4ab129a1d..0e50e5b8b 100644 --- a/netbox/templates/dcim/rack_edit.html +++ b/netbox/templates/dcim/rack_edit.html @@ -11,6 +11,7 @@ {% render_field form.group %} {% render_field form.role %} {% render_field form.serial %} + {% render_field form.tags %}
diff --git a/netbox/templates/dcim/site.html b/netbox/templates/dcim/site.html index b14c2019d..4eaaf08cf 100644 --- a/netbox/templates/dcim/site.html +++ b/netbox/templates/dcim/site.html @@ -133,6 +133,10 @@ {% endif %} + + Tags + {{ site.tags.all|join:" " }} +
diff --git a/netbox/templates/dcim/site_edit.html b/netbox/templates/dcim/site_edit.html index 399551434..49a3f7241 100644 --- a/netbox/templates/dcim/site_edit.html +++ b/netbox/templates/dcim/site_edit.html @@ -13,6 +13,7 @@ {% render_field form.asn %} {% render_field form.time_zone %} {% render_field form.description %} + {% render_field form.tags %}
diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py index 40d111269..61be3bc63 100644 --- a/netbox/utilities/api.py +++ b/netbox/utilities/api.py @@ -13,7 +13,7 @@ from rest_framework.exceptions import APIException from rest_framework.permissions import BasePermission from rest_framework.relations import PrimaryKeyRelatedField 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 WRITE_OPERATIONS = ['create', 'update', 'partial_update', 'delete'] @@ -42,6 +42,21 @@ class IsAuthenticatedOrLoginNotRequired(BasePermission): # 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): """ Represent a ChoiceField as {'value': , 'label': }. From 9b3869790d49fa7f9a372dbefeca150520de6728 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 10 May 2018 12:53:11 -0400 Subject: [PATCH 02/12] Implemented tags for all primary models --- netbox/circuits/api/serializers.py | 9 +++-- netbox/circuits/forms.py | 7 ++-- netbox/circuits/models.py | 5 +++ netbox/dcim/forms.py | 4 +-- netbox/ipam/api/serializers.py | 23 +++++++++---- netbox/ipam/forms.py | 19 ++++++++--- netbox/ipam/models.py | 9 +++++ netbox/secrets/api/serializers.py | 6 ++-- netbox/secrets/forms.py | 4 ++- netbox/secrets/models.py | 3 ++ netbox/templates/circuits/circuit.html | 4 +++ netbox/templates/circuits/circuit_edit.html | 6 ++++ netbox/templates/circuits/provider.html | 4 +++ netbox/templates/circuits/provider_edit.html | 6 ++++ netbox/templates/dcim/device_edit.html | 6 ++++ netbox/templates/dcim/devicetype_edit.html | 7 +++- netbox/templates/dcim/rack_edit.html | 7 +++- netbox/templates/dcim/site_edit.html | 7 +++- netbox/templates/ipam/aggregate.html | 4 +++ netbox/templates/ipam/aggregate_edit.html | 6 ++++ netbox/templates/ipam/ipaddress.html | 4 +++ netbox/templates/ipam/ipaddress_edit.html | 6 ++++ netbox/templates/ipam/prefix.html | 4 +++ netbox/templates/ipam/prefix_edit.html | 6 ++++ netbox/templates/ipam/vlan.html | 4 +++ netbox/templates/ipam/vlan_edit.html | 6 ++++ netbox/templates/ipam/vrf.html | 4 +++ netbox/templates/ipam/vrf_edit.html | 6 ++++ netbox/templates/secrets/secret.html | 4 +++ netbox/templates/secrets/secret_edit.html | 6 ++++ netbox/templates/tenancy/tenant.html | 4 +++ netbox/templates/tenancy/tenant_edit.html | 6 ++++ netbox/templates/virtualization/cluster.html | 4 +++ .../virtualization/cluster_edit.html | 34 +++++++++++++++++++ .../virtualization/virtualmachine.html | 4 +++ .../virtualization/virtualmachine_edit.html | 6 ++++ netbox/tenancy/api/serializers.py | 9 +++-- netbox/tenancy/forms.py | 4 ++- netbox/tenancy/models.py | 3 ++ netbox/virtualization/api/serializers.py | 13 ++++--- netbox/virtualization/forms.py | 7 ++-- netbox/virtualization/models.py | 5 +++ netbox/virtualization/views.py | 1 + 43 files changed, 262 insertions(+), 34 deletions(-) create mode 100644 netbox/templates/virtualization/cluster_edit.html diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index ded67c934..c42edb5ae 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -1,13 +1,14 @@ from __future__ import unicode_literals from rest_framework import serializers +from taggit.models import Tag from circuits.constants import CIRCUIT_STATUS_CHOICES from circuits.models import Provider, Circuit, CircuitTermination, CircuitType from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer from extras.api.customfields import CustomFieldModelSerializer from tenancy.api.serializers import NestedTenantSerializer -from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer, WritableNestedSerializer +from utilities.api import ChoiceFieldSerializer, TagField, ValidatedModelSerializer, WritableNestedSerializer # @@ -15,11 +16,12 @@ from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer, Writa # class ProviderSerializer(CustomFieldModelSerializer): + tags = TagField(queryset=Tag.objects.all(), required=False, many=True) class Meta: model = Provider fields = [ - 'id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', + 'id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', ] @@ -60,12 +62,13 @@ class CircuitSerializer(CustomFieldModelSerializer): status = ChoiceFieldSerializer(choices=CIRCUIT_STATUS_CHOICES, required=False) type = NestedCircuitTypeSerializer() tenant = NestedTenantSerializer(required=False, allow_null=True) + tags = TagField(queryset=Tag.objects.all(), required=False, many=True) class Meta: model = Circuit fields = [ 'id', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', - 'comments', 'custom_fields', 'created', 'last_updated', + 'comments', 'tags', 'custom_fields', 'created', 'last_updated', ] diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index bfcfa7187..7207e7648 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from django import forms from django.db.models import Count +from taggit.forms import TagField from dcim.models import Site, Device, Interface, Rack from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm @@ -22,10 +23,11 @@ from .models import Circuit, CircuitTermination, CircuitType, Provider class ProviderForm(BootstrapMixin, CustomFieldForm): slug = SlugField() comments = CommentField() + tags = TagField(required=False) class Meta: model = Provider - fields = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments'] + fields = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags'] widgets = { 'noc_contact': SmallTextarea(attrs={'rows': 5}), 'admin_contact': SmallTextarea(attrs={'rows': 5}), @@ -102,12 +104,13 @@ class CircuitTypeCSVForm(forms.ModelForm): class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm): comments = CommentField() + tags = TagField(required=False) class Meta: model = Circuit fields = [ 'cid', 'type', 'provider', 'status', 'install_date', 'commit_rate', 'description', 'tenant_group', 'tenant', - 'comments', + 'comments', 'tags', ] help_texts = { 'cid': "Unique circuit ID", diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index 4df845bd8..cb79b35a4 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -4,6 +4,7 @@ from django.contrib.contenttypes.fields import GenericRelation from django.db import models from django.urls import reverse from django.utils.encoding import python_2_unicode_compatible +from taggit.managers import TaggableManager from dcim.constants import STATUS_CLASSES from dcim.fields import ASNField @@ -56,6 +57,8 @@ class Provider(CreatedUpdatedModel, CustomFieldModel): object_id_field='obj_id' ) + tags = TaggableManager() + csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments'] class Meta: @@ -166,6 +169,8 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel): object_id_field='obj_id' ) + tags = TaggableManager() + csv_headers = [ 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments', ] diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 1d836028e..fe8476d72 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -784,8 +784,8 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm): class Meta: model = Device fields = [ - 'name', 'device_role', 'tags', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'position', 'face', - 'status', 'platform', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant', 'comments', + 'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'position', 'face', + 'status', 'platform', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant', 'comments', 'tags', ] help_texts = { 'device_role': "The function this device serves", diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 6fb9d3ba4..f7969fbc3 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -5,6 +5,7 @@ from collections import OrderedDict from rest_framework import serializers from rest_framework.reverse import reverse from rest_framework.validators import UniqueTogetherValidator +from taggit.models import Tag from dcim.api.serializers import NestedDeviceSerializer, InterfaceSerializer, NestedSiteSerializer from dcim.models import Interface @@ -14,7 +15,9 @@ from ipam.constants import ( ) from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF from tenancy.api.serializers import NestedTenantSerializer -from utilities.api import ChoiceFieldSerializer, SerializedPKRelatedField, ValidatedModelSerializer, WritableNestedSerializer +from utilities.api import ( + ChoiceFieldSerializer, SerializedPKRelatedField, TagField, ValidatedModelSerializer, WritableNestedSerializer, +) from virtualization.api.serializers import NestedVirtualMachineSerializer @@ -24,12 +27,13 @@ from virtualization.api.serializers import NestedVirtualMachineSerializer class VRFSerializer(CustomFieldModelSerializer): tenant = NestedTenantSerializer(required=False, allow_null=True) + tags = TagField(queryset=Tag.objects.all(), required=False, many=True) class Meta: model = VRF fields = [ - 'id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'display_name', 'custom_fields', 'created', - 'last_updated', + 'id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'tags', 'display_name', 'custom_fields', + 'created', 'last_updated', ] @@ -85,11 +89,13 @@ class NestedRIRSerializer(WritableNestedSerializer): class AggregateSerializer(CustomFieldModelSerializer): rir = NestedRIRSerializer() + tags = TagField(queryset=Tag.objects.all(), required=False, many=True) class Meta: model = Aggregate fields = [ - 'id', 'family', 'prefix', 'rir', 'date_added', 'description', 'custom_fields', 'created', 'last_updated', + 'id', 'family', 'prefix', 'rir', 'date_added', 'description', 'tags', 'custom_fields', 'created', + 'last_updated', ] read_only_fields = ['family'] @@ -147,11 +153,12 @@ class VLANSerializer(CustomFieldModelSerializer): tenant = NestedTenantSerializer(required=False, allow_null=True) status = ChoiceFieldSerializer(choices=VLAN_STATUS_CHOICES, required=False) role = NestedRoleSerializer(required=False, allow_null=True) + tags = TagField(queryset=Tag.objects.all(), required=False, many=True) class Meta: model = VLAN fields = [ - 'id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'display_name', + 'id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'tags', 'display_name', 'custom_fields', 'created', 'last_updated', ] validators = [] @@ -190,12 +197,13 @@ class PrefixSerializer(CustomFieldModelSerializer): vlan = NestedVLANSerializer(required=False, allow_null=True) status = ChoiceFieldSerializer(choices=PREFIX_STATUS_CHOICES, required=False) role = NestedRoleSerializer(required=False, allow_null=True) + tags = TagField(queryset=Tag.objects.all(), required=False, many=True) class Meta: model = Prefix fields = [ 'id', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description', - 'custom_fields', 'created', 'last_updated', + 'tags', 'custom_fields', 'created', 'last_updated', ] read_only_fields = ['family'] @@ -252,12 +260,13 @@ class IPAddressSerializer(CustomFieldModelSerializer): status = ChoiceFieldSerializer(choices=IPADDRESS_STATUS_CHOICES, required=False) role = ChoiceFieldSerializer(choices=IPADDRESS_ROLE_CHOICES, required=False) interface = IPAddressInterfaceSerializer(required=False, allow_null=True) + tags = TagField(queryset=Tag.objects.all(), required=False, many=True) class Meta: model = IPAddress fields = [ 'id', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'interface', 'description', 'nat_inside', - 'nat_outside', 'custom_fields', 'created', 'last_updated', + 'nat_outside', 'tags', 'custom_fields', 'created', 'last_updated', ] read_only_fields = ['family'] diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 3353d981f..82ebfe724 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals from django import forms from django.core.exceptions import MultipleObjectsReturned from django.db.models import Count +from taggit.forms import TagField from dcim.models import Site, Rack, Device, Interface from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm @@ -32,10 +33,11 @@ IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([(i, i) for i in range(1, 129)] # class VRFForm(BootstrapMixin, TenancyForm, CustomFieldForm): + tags = TagField(required=False) class Meta: model = VRF - fields = ['name', 'rd', 'enforce_unique', 'description', 'tenant_group', 'tenant'] + fields = ['name', 'rd', 'enforce_unique', 'description', 'tenant_group', 'tenant', 'tags'] labels = { 'rd': "RD", } @@ -121,10 +123,11 @@ class RIRFilterForm(BootstrapMixin, forms.Form): # class AggregateForm(BootstrapMixin, CustomFieldForm): + tags = TagField(required=False) class Meta: model = Aggregate - fields = ['prefix', 'rir', 'date_added', 'description'] + fields = ['prefix', 'rir', 'date_added', 'description', 'tags'] help_texts = { 'prefix': "IPv4 or IPv6 network", 'rir': "Regional Internet Registry responsible for this prefix", @@ -228,10 +231,14 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm): api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}', display_field='display_name' ) ) + tags = TagField(required=False) class Meta: model = Prefix - fields = ['prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'description', 'tenant_group', 'tenant'] + fields = [ + 'prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'description', 'tenant_group', 'tenant', + 'tags', + ] def __init__(self, *args, **kwargs): @@ -455,12 +462,13 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm) ) ) primary_for_parent = forms.BooleanField(required=False, label='Make this the primary IP for the device/VM') + tags = TagField(required=False) class Meta: model = IPAddress fields = [ 'address', 'vrf', 'status', 'role', 'description', 'interface', 'primary_for_parent', 'nat_site', - 'nat_rack', 'nat_inside', 'tenant_group', 'tenant', + 'nat_rack', 'nat_inside', 'tenant_group', 'tenant', 'tags', ] def __init__(self, *args, **kwargs): @@ -780,10 +788,11 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm): api_url='/api/ipam/vlan-groups/?site_id={{site}}', ) ) + tags = TagField(required=False) class Meta: model = VLAN - fields = ['site', 'group', 'vid', 'name', 'status', 'role', 'description', 'tenant_group', 'tenant'] + fields = ['site', 'group', 'vid', 'name', 'status', 'role', 'description', 'tenant_group', 'tenant', 'tags'] help_texts = { 'site': "Leave blank if this VLAN spans multiple sites", 'group': "VLAN group (optional)", diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index 2f83bb0f2..65a9cce55 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -10,6 +10,7 @@ from django.db.models import Q from django.db.models.expressions import RawSQL from django.urls import reverse from django.utils.encoding import python_2_unicode_compatible +from taggit.managers import TaggableManager from dcim.models import Interface from extras.models import CustomFieldModel @@ -56,6 +57,8 @@ class VRF(CreatedUpdatedModel, CustomFieldModel): object_id_field='obj_id' ) + tags = TaggableManager() + csv_headers = ['name', 'rd', 'tenant', 'enforce_unique', 'description'] class Meta: @@ -155,6 +158,8 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel): object_id_field='obj_id' ) + tags = TaggableManager() + csv_headers = ['prefix', 'rir', 'date_added', 'description'] class Meta: @@ -325,6 +330,7 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel): ) objects = PrefixQuerySet.as_manager() + tags = TaggableManager() csv_headers = [ 'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description', @@ -564,6 +570,7 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel): ) objects = IPAddressManager() + tags = TaggableManager() csv_headers = [ 'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface_name', 'is_primary', @@ -759,6 +766,8 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel): object_id_field='obj_id' ) + tags = TaggableManager() + csv_headers = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description'] class Meta: diff --git a/netbox/secrets/api/serializers.py b/netbox/secrets/api/serializers.py index aca91920a..0e24281bb 100644 --- a/netbox/secrets/api/serializers.py +++ b/netbox/secrets/api/serializers.py @@ -2,10 +2,11 @@ from __future__ import unicode_literals from rest_framework import serializers from rest_framework.validators import UniqueTogetherValidator +from taggit.models import Tag from dcim.api.serializers import NestedDeviceSerializer from secrets.models import Secret, SecretRole -from utilities.api import ValidatedModelSerializer, WritableNestedSerializer +from utilities.api import TagField, ValidatedModelSerializer, WritableNestedSerializer # @@ -35,10 +36,11 @@ class SecretSerializer(ValidatedModelSerializer): device = NestedDeviceSerializer() role = NestedSecretRoleSerializer() plaintext = serializers.CharField() + tags = TagField(queryset=Tag.objects.all(), required=False, many=True) class Meta: model = Secret - fields = ['id', 'device', 'role', 'name', 'plaintext', 'hash', 'created', 'last_updated'] + fields = ['id', 'device', 'role', 'name', 'plaintext', 'hash', 'tags', 'created', 'last_updated'] validators = [] def validate(self, data): diff --git a/netbox/secrets/forms.py b/netbox/secrets/forms.py index 8f8107805..863d1dfde 100644 --- a/netbox/secrets/forms.py +++ b/netbox/secrets/forms.py @@ -4,6 +4,7 @@ from Crypto.Cipher import PKCS1_OAEP from Crypto.PublicKey import RSA from django import forms from django.db.models import Count +from taggit.forms import TagField from dcim.models import Device from utilities.forms import BootstrapMixin, BulkEditForm, FilterChoiceField, FlexibleModelChoiceField, SlugField @@ -70,10 +71,11 @@ class SecretForm(BootstrapMixin, forms.ModelForm): label='Plaintext (verify)', widget=forms.PasswordInput() ) + tags = TagField(required=False) class Meta: model = Secret - fields = ['role', 'name', 'plaintext', 'plaintext2'] + fields = ['role', 'name', 'plaintext', 'plaintext2', 'tags'] def __init__(self, *args, **kwargs): diff --git a/netbox/secrets/models.py b/netbox/secrets/models.py index e39d46eef..dcb38db70 100644 --- a/netbox/secrets/models.py +++ b/netbox/secrets/models.py @@ -12,6 +12,7 @@ from django.core.exceptions import ValidationError from django.db import models from django.urls import reverse from django.utils.encoding import force_bytes, python_2_unicode_compatible +from taggit.managers import TaggableManager from utilities.models import CreatedUpdatedModel from .exceptions import InvalidKey @@ -336,6 +337,8 @@ class Secret(CreatedUpdatedModel): editable=False ) + tags = TaggableManager() + plaintext = None csv_headers = ['device', 'role', 'name', 'plaintext'] diff --git a/netbox/templates/circuits/circuit.html b/netbox/templates/circuits/circuit.html index 1133f41f3..34d467c89 100644 --- a/netbox/templates/circuits/circuit.html +++ b/netbox/templates/circuits/circuit.html @@ -110,6 +110,10 @@ {% endif %} + + Tags + {{ circuit.tags.all|join:" " }} +
{% with circuit.get_custom_fields as custom_fields %} diff --git a/netbox/templates/circuits/circuit_edit.html b/netbox/templates/circuits/circuit_edit.html index 8503e68f6..06ad65241 100644 --- a/netbox/templates/circuits/circuit_edit.html +++ b/netbox/templates/circuits/circuit_edit.html @@ -44,6 +44,12 @@ {% render_field form.comments %} +
+
Tags
+
+ {% render_field form.tags %} +
+
{% endblock %} {% block javascript %} diff --git a/netbox/templates/circuits/provider.html b/netbox/templates/circuits/provider.html index 6dcccfd8d..583d1da4d 100644 --- a/netbox/templates/circuits/provider.html +++ b/netbox/templates/circuits/provider.html @@ -102,6 +102,10 @@ {% endif %} + + Tags + {{ provider.tags.all|join:" " }} + Circuits diff --git a/netbox/templates/circuits/provider_edit.html b/netbox/templates/circuits/provider_edit.html index 4fb3889b1..dfa239e40 100644 --- a/netbox/templates/circuits/provider_edit.html +++ b/netbox/templates/circuits/provider_edit.html @@ -33,4 +33,10 @@ {% render_field form.comments %} +
+
Tags
+
+ {% render_field form.tags %} +
+
{% endblock %} diff --git a/netbox/templates/dcim/device_edit.html b/netbox/templates/dcim/device_edit.html index 1b7a8a9a8..460d96423 100644 --- a/netbox/templates/dcim/device_edit.html +++ b/netbox/templates/dcim/device_edit.html @@ -84,4 +84,10 @@ {% render_field form.comments %} +
+
Tags
+
+ {% render_field form.tags %} +
+
{% endblock %} diff --git a/netbox/templates/dcim/devicetype_edit.html b/netbox/templates/dcim/devicetype_edit.html index 2d7a5b132..e69077ad9 100644 --- a/netbox/templates/dcim/devicetype_edit.html +++ b/netbox/templates/dcim/devicetype_edit.html @@ -12,7 +12,6 @@ {% render_field form.u_height %} {% render_field form.is_full_depth %} {% render_field form.interface_ordering %} - {% render_field form.tags %}
@@ -38,4 +37,10 @@ {% render_field form.comments %}
+
+
Tags
+
+ {% render_field form.tags %} +
+
{% endblock %} diff --git a/netbox/templates/dcim/rack_edit.html b/netbox/templates/dcim/rack_edit.html index 0e50e5b8b..b9526a3ac 100644 --- a/netbox/templates/dcim/rack_edit.html +++ b/netbox/templates/dcim/rack_edit.html @@ -11,7 +11,6 @@ {% render_field form.group %} {% render_field form.role %} {% render_field form.serial %} - {% render_field form.tags %}
@@ -44,4 +43,10 @@ {% render_field form.comments %}
+
+
Tags
+
+ {% render_field form.tags %} +
+
{% endblock %} diff --git a/netbox/templates/dcim/site_edit.html b/netbox/templates/dcim/site_edit.html index 49a3f7241..ad7932642 100644 --- a/netbox/templates/dcim/site_edit.html +++ b/netbox/templates/dcim/site_edit.html @@ -13,7 +13,6 @@ {% render_field form.asn %} {% render_field form.time_zone %} {% render_field form.description %} - {% render_field form.tags %}
@@ -47,4 +46,10 @@ {% render_field form.comments %}
+
+
Tags
+
+ {% render_field form.tags %} +
+
{% endblock %} diff --git a/netbox/templates/ipam/aggregate.html b/netbox/templates/ipam/aggregate.html index 63731755c..de32e9c00 100644 --- a/netbox/templates/ipam/aggregate.html +++ b/netbox/templates/ipam/aggregate.html @@ -81,6 +81,10 @@ {% endif %} + + Tags + {{ aggregate.tags.all|join:" " }} + diff --git a/netbox/templates/ipam/aggregate_edit.html b/netbox/templates/ipam/aggregate_edit.html index be499a509..3cb83ab54 100644 --- a/netbox/templates/ipam/aggregate_edit.html +++ b/netbox/templates/ipam/aggregate_edit.html @@ -19,4 +19,10 @@ {% endif %} +
+
Tags
+
+ {% render_field form.tags %} +
+
{% endblock %} diff --git a/netbox/templates/ipam/ipaddress.html b/netbox/templates/ipam/ipaddress.html index 1509f35cb..c6002eb02 100644 --- a/netbox/templates/ipam/ipaddress.html +++ b/netbox/templates/ipam/ipaddress.html @@ -133,6 +133,10 @@ {% endif %} + + Tags + {{ ipaddress.tags.all|join:" " }} + {% with ipaddress.get_custom_fields as custom_fields %} diff --git a/netbox/templates/ipam/ipaddress_edit.html b/netbox/templates/ipam/ipaddress_edit.html index d0dad69ee..72fc02a1e 100644 --- a/netbox/templates/ipam/ipaddress_edit.html +++ b/netbox/templates/ipam/ipaddress_edit.html @@ -66,6 +66,12 @@ {% render_field form.nat_inside %} +
+
Tags
+
+ {% render_field form.tags %} +
+
{% if form.custom_fields %}
Custom Fields
diff --git a/netbox/templates/ipam/prefix.html b/netbox/templates/ipam/prefix.html index 11c5fc405..466fcc92d 100644 --- a/netbox/templates/ipam/prefix.html +++ b/netbox/templates/ipam/prefix.html @@ -121,6 +121,10 @@ {% endif %} + + Tags + {{ prefix.tags.all|join:" " }} + Utilization {% utilization_graph prefix.get_utilization %} diff --git a/netbox/templates/ipam/prefix_edit.html b/netbox/templates/ipam/prefix_edit.html index 938a75da3..333cf1229 100644 --- a/netbox/templates/ipam/prefix_edit.html +++ b/netbox/templates/ipam/prefix_edit.html @@ -28,6 +28,12 @@ {% render_field form.tenant %}
+
+
Tags
+
+ {% render_field form.tags %} +
+
{% if form.custom_fields %}
Custom Fields
diff --git a/netbox/templates/ipam/vlan.html b/netbox/templates/ipam/vlan.html index 971c3359f..817f0e6b5 100644 --- a/netbox/templates/ipam/vlan.html +++ b/netbox/templates/ipam/vlan.html @@ -80,6 +80,10 @@ N/A {% endif %} + + + Tags + {{ vlan.tags.all|join:" " }}
diff --git a/netbox/templates/ipam/vlan_edit.html b/netbox/templates/ipam/vlan_edit.html index 3bfb7783e..7862d4de9 100644 --- a/netbox/templates/ipam/vlan_edit.html +++ b/netbox/templates/ipam/vlan_edit.html @@ -21,6 +21,12 @@ {% render_field form.tenant %} +
+
Tags
+
+ {% render_field form.tags %} +
+
{% if form.custom_fields %}
Custom Fields
diff --git a/netbox/templates/ipam/vrf.html b/netbox/templates/ipam/vrf.html index e041ce73a..51088a0ec 100644 --- a/netbox/templates/ipam/vrf.html +++ b/netbox/templates/ipam/vrf.html @@ -77,6 +77,10 @@ N/A {% endif %} + + + Tags + {{ vrf.tags.all|join:" " }}
diff --git a/netbox/templates/ipam/vrf_edit.html b/netbox/templates/ipam/vrf_edit.html index 63052129c..95a89a6ca 100644 --- a/netbox/templates/ipam/vrf_edit.html +++ b/netbox/templates/ipam/vrf_edit.html @@ -18,6 +18,12 @@ {% render_field form.tenant %} +
+
Tags
+
+ {% render_field form.tags %} +
+
{% if form.custom_fields %}
Custom Fields
diff --git a/netbox/templates/secrets/secret.html b/netbox/templates/secrets/secret.html index 66c844ebf..e9e333ee7 100644 --- a/netbox/templates/secrets/secret.html +++ b/netbox/templates/secrets/secret.html @@ -55,6 +55,10 @@ {% endif %} + + Tags + {{ secret.tags.all|join:" " }} +
diff --git a/netbox/templates/secrets/secret_edit.html b/netbox/templates/secrets/secret_edit.html index 920409177..87ee3b426 100644 --- a/netbox/templates/secrets/secret_edit.html +++ b/netbox/templates/secrets/secret_edit.html @@ -54,6 +54,12 @@ {% render_field form.plaintext2 %} +
+
Tags
+
+ {% render_field form.tags %} +
+
diff --git a/netbox/templates/tenancy/tenant.html b/netbox/templates/tenancy/tenant.html index d5eb7df98..bf7f5ed67 100644 --- a/netbox/templates/tenancy/tenant.html +++ b/netbox/templates/tenancy/tenant.html @@ -68,6 +68,10 @@ {% endif %} + + Tags + {{ tenant.tags.all|join:" " }} +
{% with tenant.get_custom_fields as custom_fields %} diff --git a/netbox/templates/tenancy/tenant_edit.html b/netbox/templates/tenancy/tenant_edit.html index b2c472a1c..9cc0aa53b 100644 --- a/netbox/templates/tenancy/tenant_edit.html +++ b/netbox/templates/tenancy/tenant_edit.html @@ -26,4 +26,10 @@ {% render_field form.comments %} +
+
Tags
+
+ {% render_field form.tags %} +
+
{% endblock %} diff --git a/netbox/templates/virtualization/cluster.html b/netbox/templates/virtualization/cluster.html index 08251e2fa..05031dff0 100644 --- a/netbox/templates/virtualization/cluster.html +++ b/netbox/templates/virtualization/cluster.html @@ -76,6 +76,10 @@ {% endif %} + + Tags + {{ cluster.tags.all|join:" " }} + Virtual Machines {{ cluster.virtual_machines.count }} diff --git a/netbox/templates/virtualization/cluster_edit.html b/netbox/templates/virtualization/cluster_edit.html new file mode 100644 index 000000000..93fe197ec --- /dev/null +++ b/netbox/templates/virtualization/cluster_edit.html @@ -0,0 +1,34 @@ +{% extends 'utilities/obj_edit.html' %} +{% load form_helpers %} + +{% block form %} +
+
Cluster
+
+ {% render_field form.name %} + {% render_field form.type %} + {% render_field form.group %} + {% render_field form.site %} +
+
+ {% if form.custom_fields %} +
+
Custom Fields
+
+ {% render_custom_fields form %} +
+
+ {% endif %} +
+
Comments
+
+ {% render_field form.comments %} +
+
+
+
Tags
+
+ {% render_field form.tags %} +
+
+{% endblock %} diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html index 944792705..c8d119528 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -121,6 +121,10 @@ {% endif %} + + Tags + {{ vm.tags.all|join:" " }} + {% include 'inc/custom_fields_panel.html' with custom_fields=vm.get_custom_fields %} diff --git a/netbox/templates/virtualization/virtualmachine_edit.html b/netbox/templates/virtualization/virtualmachine_edit.html index 706591ab4..0fa7e07fb 100644 --- a/netbox/templates/virtualization/virtualmachine_edit.html +++ b/netbox/templates/virtualization/virtualmachine_edit.html @@ -54,4 +54,10 @@ {% render_field form.comments %} +
+
Tags
+
+ {% render_field form.tags %} +
+
{% endblock %} diff --git a/netbox/tenancy/api/serializers.py b/netbox/tenancy/api/serializers.py index 3a6e1fb4b..c7b94e7e9 100644 --- a/netbox/tenancy/api/serializers.py +++ b/netbox/tenancy/api/serializers.py @@ -1,10 +1,11 @@ from __future__ import unicode_literals from rest_framework import serializers +from taggit.models import Tag from extras.api.customfields import CustomFieldModelSerializer from tenancy.models import Tenant, TenantGroup -from utilities.api import ValidatedModelSerializer, WritableNestedSerializer +from utilities.api import TagField, ValidatedModelSerializer, WritableNestedSerializer # @@ -32,10 +33,14 @@ class NestedTenantGroupSerializer(WritableNestedSerializer): class TenantSerializer(CustomFieldModelSerializer): group = NestedTenantGroupSerializer(required=False) + tags = TagField(queryset=Tag.objects.all(), required=False, many=True) class Meta: model = Tenant - fields = ['id', 'name', 'slug', 'group', 'description', 'comments', 'custom_fields', 'created', 'last_updated'] + fields = [ + 'id', 'name', 'slug', 'group', 'description', 'comments', 'tags', 'custom_fields', 'created', + 'last_updated', + ] class NestedTenantSerializer(WritableNestedSerializer): diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index 4ea6c57ba..123b2bc24 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from django import forms from django.db.models import Count +from taggit.forms import TagField from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from utilities.forms import ( @@ -40,10 +41,11 @@ class TenantGroupCSVForm(forms.ModelForm): class TenantForm(BootstrapMixin, CustomFieldForm): slug = SlugField() comments = CommentField() + tags = TagField(required=False) class Meta: model = Tenant - fields = ['name', 'slug', 'group', 'description', 'comments'] + fields = ['name', 'slug', 'group', 'description', 'comments', 'tags'] class TenantCSVForm(forms.ModelForm): diff --git a/netbox/tenancy/models.py b/netbox/tenancy/models.py index 9df714680..f006e512d 100644 --- a/netbox/tenancy/models.py +++ b/netbox/tenancy/models.py @@ -4,6 +4,7 @@ from django.contrib.contenttypes.fields import GenericRelation from django.db import models from django.urls import reverse from django.utils.encoding import python_2_unicode_compatible +from taggit.managers import TaggableManager from extras.models import CustomFieldModel from utilities.models import CreatedUpdatedModel @@ -74,6 +75,8 @@ class Tenant(CreatedUpdatedModel, CustomFieldModel): object_id_field='obj_id' ) + tags = TaggableManager() + csv_headers = ['name', 'slug', 'group', 'description', 'comments'] class Meta: diff --git a/netbox/virtualization/api/serializers.py b/netbox/virtualization/api/serializers.py index 8cee708ba..15ed39abf 100644 --- a/netbox/virtualization/api/serializers.py +++ b/netbox/virtualization/api/serializers.py @@ -1,14 +1,15 @@ from __future__ import unicode_literals from rest_framework import serializers +from taggit.models import Tag from dcim.api.serializers import NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer -from dcim.constants import IFACE_FF_VIRTUAL, IFACE_MODE_CHOICES +from dcim.constants import IFACE_MODE_CHOICES from dcim.models import Interface from extras.api.customfields import CustomFieldModelSerializer from ipam.models import IPAddress, VLAN from tenancy.api.serializers import NestedTenantSerializer -from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer, WritableNestedSerializer +from utilities.api import ChoiceFieldSerializer, TagField, ValidatedModelSerializer, WritableNestedSerializer from virtualization.constants import VM_STATUS_CHOICES from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -59,10 +60,13 @@ class ClusterSerializer(CustomFieldModelSerializer): type = NestedClusterTypeSerializer() group = NestedClusterGroupSerializer(required=False, allow_null=True) site = NestedSiteSerializer(required=False, allow_null=True) + tags = TagField(queryset=Tag.objects.all(), required=False, many=True) class Meta: model = Cluster - fields = ['id', 'name', 'type', 'group', 'site', 'comments', 'custom_fields', 'created', 'last_updated'] + fields = [ + 'id', 'name', 'type', 'group', 'site', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + ] class NestedClusterSerializer(WritableNestedSerializer): @@ -95,12 +99,13 @@ class VirtualMachineSerializer(CustomFieldModelSerializer): primary_ip = VirtualMachineIPAddressSerializer(read_only=True) primary_ip4 = VirtualMachineIPAddressSerializer(required=False, allow_null=True) primary_ip6 = VirtualMachineIPAddressSerializer(required=False, allow_null=True) + tags = TagField(queryset=Tag.objects.all(), required=False, many=True) class Meta: model = VirtualMachine fields = [ 'id', 'name', 'status', 'cluster', 'role', 'tenant', 'platform', 'primary_ip', 'primary_ip4', 'primary_ip6', - 'vcpus', 'memory', 'disk', 'comments', 'custom_fields', 'created', 'last_updated', + 'vcpus', 'memory', 'disk', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', ] diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 4dfea1b42..b973ed5cb 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -4,6 +4,7 @@ from django import forms from django.core.exceptions import ValidationError from django.db.models import Count from mptt.forms import TreeNodeChoiceField +from taggit.forms import TagField from dcim.constants import IFACE_FF_VIRTUAL, IFACE_MODE_ACCESS, IFACE_MODE_TAGGED_ALL from dcim.forms import INTERFACE_MODE_HELP_TEXT @@ -78,10 +79,11 @@ class ClusterGroupCSVForm(forms.ModelForm): class ClusterForm(BootstrapMixin, CustomFieldForm): comments = CommentField(widget=SmallTextarea) + tags = TagField(required=False) class Meta: model = Cluster - fields = ['name', 'type', 'group', 'site', 'comments'] + fields = ['name', 'type', 'group', 'site', 'comments', 'tags'] class ClusterCSVForm(forms.ModelForm): @@ -244,12 +246,13 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm): api_url='/api/virtualization/clusters/?group_id={{cluster_group}}' ) ) + tags = TagField(required=False) class Meta: model = VirtualMachine fields = [ 'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', - 'vcpus', 'memory', 'disk', 'comments', + 'vcpus', 'memory', 'disk', 'comments', 'tags', ] def __init__(self, *args, **kwargs): diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index b58cf93e8..e34512410 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -6,6 +6,7 @@ from django.core.exceptions import ValidationError from django.db import models from django.urls import reverse from django.utils.encoding import python_2_unicode_compatible +from taggit.managers import TaggableManager from dcim.models import Device from extras.models import CustomFieldModel @@ -124,6 +125,8 @@ class Cluster(CreatedUpdatedModel, CustomFieldModel): object_id_field='obj_id' ) + tags = TaggableManager() + csv_headers = ['name', 'type', 'group', 'site', 'comments'] class Meta: @@ -242,6 +245,8 @@ class VirtualMachine(CreatedUpdatedModel, CustomFieldModel): object_id_field='obj_id' ) + tags = TaggableManager() + csv_headers = [ 'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments', ] diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index 6de6b86c7..96c57c29b 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -126,6 +126,7 @@ class ClusterView(View): class ClusterCreateView(PermissionRequiredMixin, ObjectEditView): permission_required = 'virtualization.add_cluster' + template_name = 'virtualization/cluster_edit.html' model = Cluster model_form = forms.ClusterForm From 5247f10d7e0924b089187f420f9156e2e1acb350 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 18 May 2018 10:14:56 -0400 Subject: [PATCH 03/12] Removed redundant tags field --- netbox/templates/dcim/device_edit.html | 1 - 1 file changed, 1 deletion(-) diff --git a/netbox/templates/dcim/device_edit.html b/netbox/templates/dcim/device_edit.html index 460d96423..d39c01482 100644 --- a/netbox/templates/dcim/device_edit.html +++ b/netbox/templates/dcim/device_edit.html @@ -7,7 +7,6 @@
{% render_field form.name %} {% render_field form.device_role %} - {% render_field form.tags %}
From e6b3983a4e37ab2e2ed96c410cad844b9282c821 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 18 May 2018 11:09:11 -0400 Subject: [PATCH 04/12] Added template tag for tag links --- netbox/templates/circuits/circuit.html | 8 +++++++- netbox/templates/circuits/provider.html | 8 +++++++- netbox/templates/dcim/device.html | 8 +++++++- netbox/templates/dcim/devicetype.html | 8 +++++++- netbox/templates/dcim/rack.html | 8 +++++++- netbox/templates/dcim/site.html | 8 +++++++- netbox/templates/ipam/aggregate.html | 9 ++++++++- netbox/templates/ipam/ipaddress.html | 9 ++++++++- netbox/templates/ipam/prefix.html | 8 +++++++- netbox/templates/ipam/vlan.html | 9 ++++++++- netbox/templates/ipam/vrf.html | 9 ++++++++- netbox/templates/secrets/secret.html | 9 ++++++++- netbox/templates/tenancy/tenant.html | 8 +++++++- netbox/templates/utilities/templatetags/tag.html | 1 + netbox/templates/virtualization/cluster.html | 8 +++++++- netbox/templates/virtualization/virtualmachine.html | 8 +++++++- netbox/utilities/templatetags/helpers.py | 12 +++++++++++- 17 files changed, 122 insertions(+), 16 deletions(-) create mode 100644 netbox/templates/utilities/templatetags/tag.html diff --git a/netbox/templates/circuits/circuit.html b/netbox/templates/circuits/circuit.html index 34d467c89..509c6da89 100644 --- a/netbox/templates/circuits/circuit.html +++ b/netbox/templates/circuits/circuit.html @@ -112,7 +112,13 @@ Tags - {{ circuit.tags.all|join:" " }} + + {% for tag in circuit.tags.all %} + {% tag 'circuits:circuit_list' tag %} + {% empty %} + N/A + {% endfor %} +
diff --git a/netbox/templates/circuits/provider.html b/netbox/templates/circuits/provider.html index 583d1da4d..e19175c7f 100644 --- a/netbox/templates/circuits/provider.html +++ b/netbox/templates/circuits/provider.html @@ -104,7 +104,13 @@ Tags - {{ provider.tags.all|join:" " }} + + {% for tag in provider.tags.all %} + {% tag 'circuits:provider_list' tag %} + {% empty %} + N/A + {% endfor %} + Circuits diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 69c408494..1b1d3d23a 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -98,7 +98,13 @@ Tags - {{ device.tags.all|join:" " }} + + {% for tag in device.tags.all %} + {% tag 'dcim:device_list' tag %} + {% empty %} + N/A + {% endfor %} + diff --git a/netbox/templates/dcim/devicetype.html b/netbox/templates/dcim/devicetype.html index f169d9c8c..27d2e3694 100644 --- a/netbox/templates/dcim/devicetype.html +++ b/netbox/templates/dcim/devicetype.html @@ -75,7 +75,13 @@ Tags - {{ devicetype.tags.all|join:" " }} + + {% for tag in devicetype.tags.all %} + {% tag 'dcim:devicetype_list' tag %} + {% empty %} + N/A + {% endfor %} + Instances diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index 3acd0ad46..82348e6fe 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -116,7 +116,13 @@ Tags - {{ rack.tags.all|join:" " }} + + {% for tag in rack.tags.all %} + {% tag 'dcim:rack_list' tag %} + {% empty %} + N/A + {% endfor %} + Devices diff --git a/netbox/templates/dcim/site.html b/netbox/templates/dcim/site.html index 4eaaf08cf..a882d77c8 100644 --- a/netbox/templates/dcim/site.html +++ b/netbox/templates/dcim/site.html @@ -135,7 +135,13 @@ Tags - {{ site.tags.all|join:" " }} + + {% for tag in site.tags.all %} + {% tag 'dcim:site_list' tag %} + {% empty %} + N/A + {% endfor %} + diff --git a/netbox/templates/ipam/aggregate.html b/netbox/templates/ipam/aggregate.html index de32e9c00..a7711354d 100644 --- a/netbox/templates/ipam/aggregate.html +++ b/netbox/templates/ipam/aggregate.html @@ -1,4 +1,5 @@ {% extends '_base.html' %} +{% load helpers %} {% block content %}
@@ -83,7 +84,13 @@ Tags - {{ aggregate.tags.all|join:" " }} + + {% for tag in aggregate.tags.all %} + {% tag 'ipam:aggregate_list' tag %} + {% empty %} + N/A + {% endfor %} +
diff --git a/netbox/templates/ipam/ipaddress.html b/netbox/templates/ipam/ipaddress.html index c6002eb02..da0fc6923 100644 --- a/netbox/templates/ipam/ipaddress.html +++ b/netbox/templates/ipam/ipaddress.html @@ -1,4 +1,5 @@ {% extends '_base.html' %} +{% load helpers %} {% block content %}
@@ -135,7 +136,13 @@ Tags - {{ ipaddress.tags.all|join:" " }} + + {% for tag in ipaddress.tags.all %} + {% tag 'ipam:ipaddress_list' tag %} + {% empty %} + N/A + {% endfor %} +
diff --git a/netbox/templates/ipam/prefix.html b/netbox/templates/ipam/prefix.html index 466fcc92d..29e9c07a0 100644 --- a/netbox/templates/ipam/prefix.html +++ b/netbox/templates/ipam/prefix.html @@ -123,7 +123,13 @@ Tags - {{ prefix.tags.all|join:" " }} + + {% for tag in prefix.tags.all %} + {% tag 'ipam:prefix_list' tag %} + {% empty %} + N/A + {% endfor %} + Utilization diff --git a/netbox/templates/ipam/vlan.html b/netbox/templates/ipam/vlan.html index 817f0e6b5..ac874282f 100644 --- a/netbox/templates/ipam/vlan.html +++ b/netbox/templates/ipam/vlan.html @@ -1,4 +1,5 @@ {% extends '_base.html' %} +{% load helpers %} {% block content %} {% include 'ipam/inc/vlan_header.html' with active_tab='vlan' %} @@ -83,7 +84,13 @@ Tags - {{ vlan.tags.all|join:" " }} + + {% for tag in vlan.tags.all %} + {% tag 'ipam:vlan_list' tag %} + {% empty %} + N/A + {% endfor %} + diff --git a/netbox/templates/ipam/vrf.html b/netbox/templates/ipam/vrf.html index 51088a0ec..fa51a18f8 100644 --- a/netbox/templates/ipam/vrf.html +++ b/netbox/templates/ipam/vrf.html @@ -1,4 +1,5 @@ {% extends '_base.html' %} +{% load helpers %} {% block content %}
@@ -80,7 +81,13 @@ Tags - {{ vrf.tags.all|join:" " }} + + {% for tag in vrf.tags.all %} + {% tag 'ipam:vrf_list' tag %} + {% empty %} + N/A + {% endfor %} +
diff --git a/netbox/templates/secrets/secret.html b/netbox/templates/secrets/secret.html index e9e333ee7..4863fdeb1 100644 --- a/netbox/templates/secrets/secret.html +++ b/netbox/templates/secrets/secret.html @@ -1,5 +1,6 @@ {% extends '_base.html' %} {% load static from staticfiles %} +{% load helpers %} {% load secret_helpers %} {% block content %} @@ -57,7 +58,13 @@ Tags - {{ secret.tags.all|join:" " }} + + {% for tag in secret.tags.all %} + {% tag 'secrets:secret_list' tag %} + {% empty %} + N/A + {% endfor %} + diff --git a/netbox/templates/tenancy/tenant.html b/netbox/templates/tenancy/tenant.html index bf7f5ed67..fbbac175a 100644 --- a/netbox/templates/tenancy/tenant.html +++ b/netbox/templates/tenancy/tenant.html @@ -70,7 +70,13 @@ Tags - {{ tenant.tags.all|join:" " }} + + {% for tag in tenant.tags.all %} + {% tag 'tenancy:tenant_list' tag %} + {% empty %} + N/A + {% endfor %} + diff --git a/netbox/templates/utilities/templatetags/tag.html b/netbox/templates/utilities/templatetags/tag.html new file mode 100644 index 000000000..79e1627db --- /dev/null +++ b/netbox/templates/utilities/templatetags/tag.html @@ -0,0 +1 @@ +{{ tag }} diff --git a/netbox/templates/virtualization/cluster.html b/netbox/templates/virtualization/cluster.html index 05031dff0..9b1621530 100644 --- a/netbox/templates/virtualization/cluster.html +++ b/netbox/templates/virtualization/cluster.html @@ -78,7 +78,13 @@ Tags - {{ cluster.tags.all|join:" " }} + + {% for tag in cluster.tags.all %} + {% tag 'virtualization:cluster_list' tag %} + {% empty %} + N/A + {% endfor %} + Virtual Machines diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html index c8d119528..430caafca 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -123,7 +123,13 @@ Tags - {{ vm.tags.all|join:" " }} + + {% for tag in vm.tags.all %} + {% tag 'virtualization:vm_list' tag %} + {% empty %} + N/A + {% endfor %} + diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index 7d79a5f2a..1380941b3 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -1,7 +1,6 @@ from __future__ import unicode_literals import datetime -import pytz from django import template from django.utils.safestring import mark_safe @@ -160,3 +159,14 @@ def utilization_graph(utilization, warning_threshold=75, danger_threshold=90): 'warning_threshold': warning_threshold, 'danger_threshold': danger_threshold, } + + +@register.inclusion_tag('utilities/templatetags/tag.html') +def tag(url_name, tag): + """ + Display a link to the given object list filtered by a specific Tag slug. + """ + return { + 'url_name': url_name, + 'tag': tag, + } From 01896091379255025cf743279f0056d3fd119e2a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 18 May 2018 12:35:04 -0400 Subject: [PATCH 05/12] Fixes URL name --- netbox/templates/virtualization/virtualmachine.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html index 430caafca..3d8d0d05a 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -125,7 +125,7 @@ Tags {% for tag in vm.tags.all %} - {% tag 'virtualization:vm_list' tag %} + {% tag 'virtualization:virtualmachine_list' tag %} {% empty %} N/A {% endfor %} From 1d1553275e92bf00ca2dd4b69b69d86a478bba6b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 18 May 2018 15:43:21 -0400 Subject: [PATCH 06/12] Added tags panel to object list view --- netbox/templates/circuits/circuit_list.html | 1 + netbox/templates/circuits/provider_list.html | 1 + netbox/templates/dcim/device_list.html | 1 + netbox/templates/dcim/devicetype_list.html | 1 + netbox/templates/dcim/rack_list.html | 1 + netbox/templates/dcim/site_list.html | 1 + netbox/templates/inc/tags_panel.html | 13 +++++++++++++ netbox/templates/ipam/aggregate_list.html | 1 + netbox/templates/ipam/ipaddress_list.html | 1 + netbox/templates/ipam/prefix_list.html | 1 + netbox/templates/ipam/vlan_list.html | 1 + netbox/templates/ipam/vrf_list.html | 1 + netbox/templates/secrets/secret_list.html | 1 + netbox/templates/tenancy/tenant_list.html | 1 + netbox/templates/virtualization/cluster_list.html | 1 + .../virtualization/virtualmachine_list.html | 1 + netbox/utilities/views.py | 9 ++++++++- 17 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 netbox/templates/inc/tags_panel.html diff --git a/netbox/templates/circuits/circuit_list.html b/netbox/templates/circuits/circuit_list.html index f05552f7d..81e09c32b 100644 --- a/netbox/templates/circuits/circuit_list.html +++ b/netbox/templates/circuits/circuit_list.html @@ -16,6 +16,7 @@
{% include 'inc/search_panel.html' %} + {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/circuits/provider_list.html b/netbox/templates/circuits/provider_list.html index cb7aab406..a0036f46c 100644 --- a/netbox/templates/circuits/provider_list.html +++ b/netbox/templates/circuits/provider_list.html @@ -16,6 +16,7 @@
{% include 'inc/search_panel.html' %} + {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/dcim/device_list.html b/netbox/templates/dcim/device_list.html index f96b27309..4bae11781 100644 --- a/netbox/templates/dcim/device_list.html +++ b/netbox/templates/dcim/device_list.html @@ -16,6 +16,7 @@
{% include 'inc/search_panel.html' %} + {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/dcim/devicetype_list.html b/netbox/templates/dcim/devicetype_list.html index 91745082a..eb901f5a0 100644 --- a/netbox/templates/dcim/devicetype_list.html +++ b/netbox/templates/dcim/devicetype_list.html @@ -16,6 +16,7 @@
{% include 'inc/search_panel.html' %} + {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/dcim/rack_list.html b/netbox/templates/dcim/rack_list.html index d5734ee2b..e61f4eadf 100644 --- a/netbox/templates/dcim/rack_list.html +++ b/netbox/templates/dcim/rack_list.html @@ -16,6 +16,7 @@
{% include 'inc/search_panel.html' %} + {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/dcim/site_list.html b/netbox/templates/dcim/site_list.html index 7baa76dad..50066186d 100644 --- a/netbox/templates/dcim/site_list.html +++ b/netbox/templates/dcim/site_list.html @@ -16,6 +16,7 @@
{% include 'inc/search_panel.html' %} + {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/inc/tags_panel.html b/netbox/templates/inc/tags_panel.html new file mode 100644 index 000000000..baeee72ac --- /dev/null +++ b/netbox/templates/inc/tags_panel.html @@ -0,0 +1,13 @@ +{% load helpers %} + +
+
+ + Tags +
+
+ {% for tag in tags %} + {{ tag }} {{ tag.count }} + {% endfor %} +
+
diff --git a/netbox/templates/ipam/aggregate_list.html b/netbox/templates/ipam/aggregate_list.html index 73da9695d..33db74e5c 100644 --- a/netbox/templates/ipam/aggregate_list.html +++ b/netbox/templates/ipam/aggregate_list.html @@ -17,6 +17,7 @@
{% include 'inc/search_panel.html' %} + {% include 'inc/tags_panel.html' %}
Statistics diff --git a/netbox/templates/ipam/ipaddress_list.html b/netbox/templates/ipam/ipaddress_list.html index 5f8fdeb88..418b807bd 100644 --- a/netbox/templates/ipam/ipaddress_list.html +++ b/netbox/templates/ipam/ipaddress_list.html @@ -16,6 +16,7 @@
{% include 'inc/search_panel.html' %} + {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/ipam/prefix_list.html b/netbox/templates/ipam/prefix_list.html index d65904595..3ce9d4a9c 100644 --- a/netbox/templates/ipam/prefix_list.html +++ b/netbox/templates/ipam/prefix_list.html @@ -21,6 +21,7 @@
{% include 'inc/search_panel.html' %} + {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/ipam/vlan_list.html b/netbox/templates/ipam/vlan_list.html index 24e12595b..d734db8d2 100644 --- a/netbox/templates/ipam/vlan_list.html +++ b/netbox/templates/ipam/vlan_list.html @@ -16,6 +16,7 @@
{% include 'inc/search_panel.html' %} + {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/ipam/vrf_list.html b/netbox/templates/ipam/vrf_list.html index 23bd16495..670f0ee5d 100644 --- a/netbox/templates/ipam/vrf_list.html +++ b/netbox/templates/ipam/vrf_list.html @@ -16,6 +16,7 @@
{% include 'inc/search_panel.html' %} + {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/secrets/secret_list.html b/netbox/templates/secrets/secret_list.html index 6dd92cd89..0a70e1087 100644 --- a/netbox/templates/secrets/secret_list.html +++ b/netbox/templates/secrets/secret_list.html @@ -14,6 +14,7 @@
{% include 'inc/search_panel.html' %} + {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/tenancy/tenant_list.html b/netbox/templates/tenancy/tenant_list.html index e6fd61c37..176231507 100644 --- a/netbox/templates/tenancy/tenant_list.html +++ b/netbox/templates/tenancy/tenant_list.html @@ -16,6 +16,7 @@
{% include 'inc/search_panel.html' %} + {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/virtualization/cluster_list.html b/netbox/templates/virtualization/cluster_list.html index 08f62e6ba..84513dbb1 100644 --- a/netbox/templates/virtualization/cluster_list.html +++ b/netbox/templates/virtualization/cluster_list.html @@ -16,6 +16,7 @@
{% include 'inc/search_panel.html' %} + {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/virtualization/virtualmachine_list.html b/netbox/templates/virtualization/virtualmachine_list.html index 30ed76dae..bf2961fd8 100644 --- a/netbox/templates/virtualization/virtualmachine_list.html +++ b/netbox/templates/virtualization/virtualmachine_list.html @@ -16,6 +16,7 @@
{% include 'inc/search_panel.html' %} + {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index d060e53d7..769954fea 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -8,7 +8,7 @@ from django.contrib import messages from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError from django.db import transaction, IntegrityError -from django.db.models import ProtectedError +from django.db.models import Count, ProtectedError from django.forms import CharField, Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea from django.shortcuts import get_object_or_404, redirect, render from django.template.exceptions import TemplateSyntaxError @@ -119,6 +119,12 @@ class ObjectListView(View): if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']): table.columns.show('pk') + # Construct queryset for tags list + if hasattr(model, 'tags'): + tags = model.tags.annotate(count=Count('taggit_taggeditem_items')) + else: + tags = None + # Apply the request context paginate = { 'klass': EnhancedPaginator, @@ -131,6 +137,7 @@ class ObjectListView(View): 'table': table, 'permissions': permissions, 'filter_form': self.filter_form(request.GET, label_suffix='') if self.filter_form else None, + 'tags': tags, } context.update(self.extra_context()) From b3350490e7d6d0f6e47f1db3762509627846d8fe Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 18 May 2018 16:24:15 -0400 Subject: [PATCH 07/12] Implemented tag filtering --- netbox/circuits/filters.py | 6 ++++++ netbox/dcim/filters.py | 12 ++++++++++++ netbox/ipam/filters.py | 15 +++++++++++++++ netbox/secrets/filters.py | 3 +++ netbox/templates/inc/tags_panel.html | 2 +- netbox/tenancy/filters.py | 3 +++ netbox/virtualization/filters.py | 6 ++++++ 7 files changed, 46 insertions(+), 1 deletion(-) diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index ca66be406..79efdc950 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -28,6 +28,9 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Site (slug)', ) + tag = django_filters.CharFilter( + name='tags__slug', + ) class Meta: model = Provider @@ -103,6 +106,9 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Site (slug)', ) + tag = django_filters.CharFilter( + name='tags__slug', + ) class Meta: model = Circuit diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 0d5455aa0..63091c2a8 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -82,6 +82,9 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Tenant (slug)', ) + tag = django_filters.CharFilter( + name='tags__slug', + ) class Meta: model = Site @@ -179,6 +182,9 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Role (slug)', ) + tag = django_filters.CharFilter( + name='tags__slug', + ) class Meta: model = Rack @@ -286,6 +292,9 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Manufacturer (slug)', ) + tag = django_filters.CharFilter( + name='tags__slug', + ) class Meta: model = DeviceType @@ -497,6 +506,9 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet): queryset=VirtualChassis.objects.all(), label='Virtual chassis (ID)', ) + tag = django_filters.CharFilter( + name='tags__slug', + ) class Meta: model = Device diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index 005d44a84..db2806b77 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -30,6 +30,9 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Tenant (slug)', ) + tag = django_filters.CharFilter( + name='tags__slug', + ) def search(self, queryset, name, value): if not value.strip(): @@ -69,6 +72,9 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='RIR (slug)', ) + tag = django_filters.CharFilter( + name='tags__slug', + ) class Meta: model = Aggregate @@ -167,6 +173,9 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet): choices=PREFIX_STATUS_CHOICES, null_value=None ) + tag = django_filters.CharFilter( + name='tags__slug', + ) class Meta: model = Prefix @@ -289,6 +298,9 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet): role = django_filters.MultipleChoiceFilter( choices=IPADDRESS_ROLE_CHOICES ) + tag = django_filters.CharFilter( + name='tags__slug', + ) class Meta: model = IPAddress @@ -394,6 +406,9 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet): choices=VLAN_STATUS_CHOICES, null_value=None ) + tag = django_filters.CharFilter( + name='tags__slug', + ) class Meta: model = VLAN diff --git a/netbox/secrets/filters.py b/netbox/secrets/filters.py index 6578eb4b8..2499fa2bb 100644 --- a/netbox/secrets/filters.py +++ b/netbox/secrets/filters.py @@ -41,6 +41,9 @@ class SecretFilter(django_filters.FilterSet): to_field_name='name', label='Device (name)', ) + tag = django_filters.CharFilter( + name='tags__slug', + ) class Meta: model = Secret diff --git a/netbox/templates/inc/tags_panel.html b/netbox/templates/inc/tags_panel.html index baeee72ac..a7923fbed 100644 --- a/netbox/templates/inc/tags_panel.html +++ b/netbox/templates/inc/tags_panel.html @@ -7,7 +7,7 @@
{% for tag in tags %} - {{ tag }} {{ tag.count }} + {{ tag }} {{ tag.count }} {% endfor %}
diff --git a/netbox/tenancy/filters.py b/netbox/tenancy/filters.py index 330ab7f56..7eccff5d3 100644 --- a/netbox/tenancy/filters.py +++ b/netbox/tenancy/filters.py @@ -31,6 +31,9 @@ class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Group (slug)', ) + tag = django_filters.CharFilter( + name='tags__slug', + ) class Meta: model = Tenant diff --git a/netbox/virtualization/filters.py b/netbox/virtualization/filters.py index 53c3f18d9..6af4e4a22 100644 --- a/netbox/virtualization/filters.py +++ b/netbox/virtualization/filters.py @@ -63,6 +63,9 @@ class ClusterFilter(CustomFieldFilterSet): to_field_name='slug', label='Site (slug)', ) + tag = django_filters.CharFilter( + name='tags__slug', + ) class Meta: model = Cluster @@ -154,6 +157,9 @@ class VirtualMachineFilter(CustomFieldFilterSet): to_field_name='slug', label='Platform (slug)', ) + tag = django_filters.CharFilter( + name='tags__slug', + ) class Meta: model = VirtualMachine From 601fb418b5ca9d30250da0754bf18a3be6941533 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 22 May 2018 10:51:40 -0400 Subject: [PATCH 08/12] Tweaked ordering of tags list --- netbox/utilities/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 769954fea..5d913a706 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -121,7 +121,7 @@ class ObjectListView(View): # Construct queryset for tags list if hasattr(model, 'tags'): - tags = model.tags.annotate(count=Count('taggit_taggeditem_items')) + tags = model.tags.annotate(count=Count('taggit_taggeditem_items')).order_by('-count', 'name') else: tags = None From 918339cfa8a6e8f85f24d17fad7eb152daabc962 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 22 May 2018 11:19:47 -0400 Subject: [PATCH 09/12] Tweak formatting of message to handle translation strings --- netbox/utilities/views.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 5d913a706..fd085a1e7 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -202,13 +202,16 @@ class ObjectEditView(GetReturnURLMixin, View): obj_created = not form.instance.pk obj = form.save() - msg = 'Created ' if obj_created else 'Modified ' - msg += self.model._meta.verbose_name + msg = '{} {}'.format( + 'Created' if obj_created else 'Modified', + self.model._meta.verbose_name + ) if hasattr(obj, 'get_absolute_url'): msg = '{} {}'.format(msg, obj.get_absolute_url(), escape(obj)) else: msg = '{} {}'.format(msg, escape(obj)) messages.success(request, mark_safe(msg)) + if obj_created: UserAction.objects.log_create(request.user, obj, msg) else: From 03a1c48b54d0bc4e62ef452b0919548f0241edfb Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 22 May 2018 12:22:46 -0400 Subject: [PATCH 10/12] Added list and utility views for tags --- netbox/extras/forms.py | 24 ++++++++++++++- netbox/extras/tables.py | 28 ++++++++++++++++++ netbox/extras/urls.py | 6 ++++ netbox/extras/views.py | 42 +++++++++++++++++++++++++-- netbox/templates/extras/tag_list.html | 11 +++++++ netbox/templates/inc/nav_menu.html | 5 +++- 6 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 netbox/extras/tables.py create mode 100644 netbox/templates/extras/tag_list.html diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index a923ae596..9088d1b3d 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -4,12 +4,17 @@ from collections import OrderedDict from django import forms from django.contrib.contenttypes.models import ContentType +from taggit.models import Tag -from utilities.forms import BootstrapMixin, BulkEditForm, LaxURLField +from utilities.forms import BootstrapMixin, BulkEditForm, LaxURLField, SlugField from .constants import CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL from .models import CustomField, CustomFieldValue, ImageAttachment +# +# Custom fields +# + def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=False): """ Retrieve all CustomFields applicable to the given ContentType @@ -162,6 +167,23 @@ class CustomFieldFilterForm(forms.Form): self.fields[name] = field +# +# Tags +# +# + +class TagForm(BootstrapMixin, forms.ModelForm): + slug = SlugField() + + class Meta: + model = Tag + fields = ['name', 'slug'] + + +# +# Image attachments +# + class ImageAttachmentForm(BootstrapMixin, forms.ModelForm): class Meta: diff --git a/netbox/extras/tables.py b/netbox/extras/tables.py new file mode 100644 index 000000000..921b9f273 --- /dev/null +++ b/netbox/extras/tables.py @@ -0,0 +1,28 @@ +from __future__ import unicode_literals + +import django_tables2 as tables +from taggit.models import Tag + +from utilities.tables import BaseTable, ToggleColumn + +TAG_ACTIONS = """ +{% if perms.taggit.change_tag %} + +{% endif %} +{% if perms.taggit.delete_tag %} + +{% endif %} +""" + + +class TagTable(BaseTable): + pk = ToggleColumn() + actions = tables.TemplateColumn( + template_code=TAG_ACTIONS, + attrs={'td': {'class': 'text-right'}}, + verbose_name='' + ) + + class Meta(BaseTable.Meta): + model = Tag + fields = ('pk', 'name', 'items') diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py index 13e50a229..d3c200334 100644 --- a/netbox/extras/urls.py +++ b/netbox/extras/urls.py @@ -7,6 +7,12 @@ from extras import views app_name = 'extras' urlpatterns = [ + # Tags + url(r'^tags/$', views.TagListView.as_view(), name='tag_list'), + url(r'^tags/(?P[\w-]+)/edit/$', views.TagEditView.as_view(), name='tag_edit'), + url(r'^tags/(?P[\w-]+)/delete/$', views.TagDeleteView.as_view(), name='tag_delete'), + url(r'^tags/delete/$', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'), + # Image attachments url(r'^image-attachments/(?P\d+)/edit/$', views.ImageAttachmentEditView.as_view(), name='imageattachment_edit'), url(r'^image-attachments/(?P\d+)/delete/$', views.ImageAttachmentDeleteView.as_view(), name='imageattachment_delete'), diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 3f7c0435b..130437356 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -2,16 +2,52 @@ from __future__ import unicode_literals from django.contrib import messages from django.contrib.auth.mixins import PermissionRequiredMixin +from django.db.models import Count from django.http import Http404 -from django.shortcuts import get_object_or_404, redirect, render +from django.shortcuts import get_object_or_404, redirect, render, reverse from django.utils.safestring import mark_safe from django.views.generic import View +from taggit.models import Tag from utilities.forms import ConfirmationForm -from utilities.views import ObjectDeleteView, ObjectEditView -from .forms import ImageAttachmentForm +from utilities.views import BulkDeleteView, ObjectDeleteView, ObjectEditView, ObjectListView +from .forms import ImageAttachmentForm, TagForm from .models import ImageAttachment, ReportResult, UserAction from .reports import get_report, get_reports +from .tables import TagTable + + +# +# Tags +# + +class TagListView(ObjectListView): + queryset = Tag.objects.annotate(items=Count('taggit_taggeditem_items')).order_by('name') + table = TagTable + template_name = 'extras/tag_list.html' + + +class TagEditView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'taggit.change_tag' + model = Tag + model_form = TagForm + + def get_return_url(self, request, obj): + return reverse('extras:tag', kwargs={'slug': obj.slug}) + + +class TagDeleteView(PermissionRequiredMixin, ObjectDeleteView): + permission_required = 'taggit.delete_tag' + model = Tag + default_return_url = 'extras:tag_list' + + +class TagBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): + permission_required = 'circuits.delete_circuittype' + cls = Tag + queryset = Tag.objects.annotate(items=Count('taggit_taggeditem_items')).order_by('name') + table = TagTable + default_return_url = 'extras:tag_list' # diff --git a/netbox/templates/extras/tag_list.html b/netbox/templates/extras/tag_list.html new file mode 100644 index 000000000..3136991a0 --- /dev/null +++ b/netbox/templates/extras/tag_list.html @@ -0,0 +1,11 @@ +{% extends '_base.html' %} +{% load buttons %} + +{% block content %} +

{% block title %}Tags{% endblock %}

+
+
+ {% include 'utilities/obj_table.html' with bulk_delete_url='extras:tag_bulk_delete' %} +
+
+{% endblock %} diff --git a/netbox/templates/inc/nav_menu.html b/netbox/templates/inc/nav_menu.html index a85647993..2c47ad85b 100644 --- a/netbox/templates/inc/nav_menu.html +++ b/netbox/templates/inc/nav_menu.html @@ -16,7 +16,7 @@