Initial work on #11541

This commit is contained in:
Jeremy Stretch 2023-06-22 16:41:48 -04:00
parent 69b818ed33
commit 9b189231c3
10 changed files with 106 additions and 10 deletions

View File

@ -15,3 +15,11 @@ A unique URL-friendly identifier. (This value will be used for filtering.) This
### Color ### Color
The color to use when displaying the tag in the NetBox UI. The color to use when displaying the tag in the NetBox UI.
### Object Types
!!! info "This feature was introduced in NetBox v3.6."
The assignment of a tag may be limited to a prescribed set of objects. For example, it may be desirable to limit the application of a specific tag to only devices and virtual machines.
If no object types are specified, the tag will be assignable to any type of object.

View File

@ -196,12 +196,18 @@ class SavedFilterSerializer(ValidatedModelSerializer):
class TagSerializer(ValidatedModelSerializer): class TagSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail') url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail')
object_types = ContentTypeField(
queryset=ContentType.objects.filter(FeatureQuery('custom_fields').get_query()),
many=True,
required=False
)
tagged_items = serializers.IntegerField(read_only=True) tagged_items = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = Tag model = Tag
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tagged_items', 'created', 'last_updated', 'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'object_types', 'tagged_items', 'created',
'last_updated',
] ]

View File

@ -258,10 +258,14 @@ class TagFilterSet(ChangeLoggedModelFilterSet):
content_type_id = MultiValueNumberFilter( content_type_id = MultiValueNumberFilter(
method='_content_type_id' method='_content_type_id'
) )
object_type_id = MultiValueNumberFilter(
field_name='object_types__id'
)
object_types = ContentTypeFilter()
class Meta: class Meta:
model = Tag model = Tag
fields = ['id', 'name', 'slug', 'color', 'description'] fields = ['id', 'name', 'slug', 'color', 'description', 'object_types']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():

View File

@ -245,6 +245,11 @@ class TagFilterForm(SavedFiltersMixin, FilterForm):
required=False, required=False,
label=_('Tagged object type') label=_('Tagged object type')
) )
object_type_id = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.filter(FeatureQuery('tags').get_query()),
required=False,
label=_('Allowed object type')
)
class ConfigContextFilterForm(SavedFiltersMixin, FilterForm): class ConfigContextFilterForm(SavedFiltersMixin, FilterForm):

View File

@ -204,15 +204,20 @@ class WebhookForm(BootstrapMixin, forms.ModelForm):
class TagForm(BootstrapMixin, forms.ModelForm): class TagForm(BootstrapMixin, forms.ModelForm):
slug = SlugField() slug = SlugField()
object_types = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('tags'),
required=False
)
fieldsets = ( fieldsets = (
('Tag', ('name', 'slug', 'color', 'description')), ('Tag', ('name', 'slug', 'color', 'description', 'object_types')),
) )
class Meta: class Meta:
model = Tag model = Tag
fields = [ fields = [
'name', 'slug', 'color', 'description' 'name', 'slug', 'color', 'description', 'object_types',
] ]

View File

@ -0,0 +1,20 @@
# Generated by Django 4.1.9 on 2023-06-22 19:33
from django.db import migrations, models
import extras.utils
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('extras', '0093_configrevision_ordering'),
]
operations = [
migrations.AddField(
model_name='tag',
name='object_types',
field=models.ManyToManyField(blank=True, limit_choices_to=extras.utils.FeatureQuery('tags'), related_name='+', to='contenttypes.contenttype'),
),
]

View File

@ -1,9 +1,13 @@
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.text import slugify from django.utils.text import slugify
from django.utils.translation import gettext as _
from taggit.models import TagBase, GenericTaggedItemBase from taggit.models import TagBase, GenericTaggedItemBase
from extras.utils import FeatureQuery
from netbox.models import ChangeLoggedModel from netbox.models import ChangeLoggedModel
from netbox.models.features import CloningMixin, ExportTemplatesMixin from netbox.models.features import CloningMixin, ExportTemplatesMixin
from utilities.choices import ColorChoices from utilities.choices import ColorChoices
@ -30,9 +34,16 @@ class Tag(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel, TagBase):
max_length=200, max_length=200,
blank=True, blank=True,
) )
object_types = models.ManyToManyField(
to=ContentType,
related_name='+',
limit_choices_to=FeatureQuery('tags'),
blank=True,
help_text=_("The object type(s) to which this this tag can be applied.")
)
clone_fields = ( clone_fields = (
'color', 'description', 'color', 'description', 'object_types',
) )
class Meta: class Meta:

View File

@ -10,8 +10,9 @@ from extras.validators import CustomValidator
from netbox.config import get_config from netbox.config import get_config
from netbox.context import current_request, webhooks_queue from netbox.context import current_request, webhooks_queue
from netbox.signals import post_clean from netbox.signals import post_clean
from utilities.exceptions import AbortRequest
from .choices import ObjectChangeActionChoices from .choices import ObjectChangeActionChoices
from .models import ConfigRevision, CustomField, ObjectChange from .models import ConfigRevision, CustomField, ObjectChange, TaggedItem
from .webhooks import enqueue_object, get_snapshots, serialize_for_webhook from .webhooks import enqueue_object, get_snapshots, serialize_for_webhook
# #
@ -207,3 +208,21 @@ def update_config(sender, instance, **kwargs):
Update the cached NetBox configuration when a new ConfigRevision is created. Update the cached NetBox configuration when a new ConfigRevision is created.
""" """
instance.activate() instance.activate()
#
# Tags
#
@receiver(m2m_changed, sender=TaggedItem)
def validate_assigned_tags(sender, instance, action, model, pk_set, **kwargs):
"""
Validate that any Tags being assigned to the instance are not restricted to non-applicable object types.
"""
if action != 'pre_add':
return
ct = ContentType.objects.get_for_model(instance)
# Retrieve any applied Tags that are restricted to certain object_types
for tag in model.objects.filter(pk__in=pk_set, object_types__isnull=False).prefetch_related('object_types'):
if ct not in tag.object_types.all():
raise AbortRequest(f"Tag {tag} cannot be assigned to {ct.model} objects.")

View File

@ -210,10 +210,14 @@ class TagTable(NetBoxTable):
linkify=True linkify=True
) )
color = columns.ColorColumn() color = columns.ColorColumn()
object_types = columns.ContentTypesColumn()
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Tag model = Tag
fields = ('pk', 'id', 'name', 'items', 'slug', 'color', 'description', 'created', 'last_updated', 'actions') fields = (
'pk', 'id', 'name', 'items', 'slug', 'color', 'description', 'object_types', 'created', 'last_updated',
'actions',
)
default_columns = ('pk', 'name', 'items', 'slug', 'color', 'description') default_columns = ('pk', 'name', 'items', 'slug', 'color', 'description')

View File

@ -43,9 +43,23 @@
</div> </div>
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
<h5 class="card-header"> <h5 class="card-header">Allowed Object Types</h5>
Tagged Item Types <div class="card-body">
</h5> <table class="table table-hover attr-table">
{% for ct in object.object_types.all %}
<tr>
<td>{{ ct }}</td>
</tr>
{% empty %}
<tr>
<td class="text-muted">Any</td>
</tr>
{% endfor %}
</table>
</div>
</div>
<div class="card">
<h5 class="card-header">Tagged Item Types</h5>
<div class="card-body"> <div class="card-body">
<table class="table table-hover panel-body attr-table"> <table class="table table-hover panel-body attr-table">
{% for object_type in object_types %} {% for object_type in object_types %}