Skip to content

Commit 9b18923

Browse files
committed
Initial work on #11541
1 parent 69b818e commit 9b18923

File tree

10 files changed

+106
-10
lines changed

10 files changed

+106
-10
lines changed

docs/models/extras/tag.md

+8
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,11 @@ A unique URL-friendly identifier. (This value will be used for filtering.) This
1515
### Color
1616

1717
The color to use when displaying the tag in the NetBox UI.
18+
19+
### Object Types
20+
21+
!!! info "This feature was introduced in NetBox v3.6."
22+
23+
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.
24+
25+
If no object types are specified, the tag will be assignable to any type of object.

netbox/extras/api/serializers.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -196,12 +196,18 @@ class Meta:
196196

197197
class TagSerializer(ValidatedModelSerializer):
198198
url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail')
199+
object_types = ContentTypeField(
200+
queryset=ContentType.objects.filter(FeatureQuery('custom_fields').get_query()),
201+
many=True,
202+
required=False
203+
)
199204
tagged_items = serializers.IntegerField(read_only=True)
200205

201206
class Meta:
202207
model = Tag
203208
fields = [
204-
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tagged_items', 'created', 'last_updated',
209+
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'object_types', 'tagged_items', 'created',
210+
'last_updated',
205211
]
206212

207213

netbox/extras/filtersets.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -258,10 +258,14 @@ class TagFilterSet(ChangeLoggedModelFilterSet):
258258
content_type_id = MultiValueNumberFilter(
259259
method='_content_type_id'
260260
)
261+
object_type_id = MultiValueNumberFilter(
262+
field_name='object_types__id'
263+
)
264+
object_types = ContentTypeFilter()
261265

262266
class Meta:
263267
model = Tag
264-
fields = ['id', 'name', 'slug', 'color', 'description']
268+
fields = ['id', 'name', 'slug', 'color', 'description', 'object_types']
265269

266270
def search(self, queryset, name, value):
267271
if not value.strip():

netbox/extras/forms/filtersets.py

+5
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,11 @@ class TagFilterForm(SavedFiltersMixin, FilterForm):
245245
required=False,
246246
label=_('Tagged object type')
247247
)
248+
object_type_id = ContentTypeMultipleChoiceField(
249+
queryset=ContentType.objects.filter(FeatureQuery('tags').get_query()),
250+
required=False,
251+
label=_('Allowed object type')
252+
)
248253

249254

250255
class ConfigContextFilterForm(SavedFiltersMixin, FilterForm):

netbox/extras/forms/model_forms.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -204,15 +204,20 @@ class Meta:
204204

205205
class TagForm(BootstrapMixin, forms.ModelForm):
206206
slug = SlugField()
207+
object_types = ContentTypeMultipleChoiceField(
208+
queryset=ContentType.objects.all(),
209+
limit_choices_to=FeatureQuery('tags'),
210+
required=False
211+
)
207212

208213
fieldsets = (
209-
('Tag', ('name', 'slug', 'color', 'description')),
214+
('Tag', ('name', 'slug', 'color', 'description', 'object_types')),
210215
)
211216

212217
class Meta:
213218
model = Tag
214219
fields = [
215-
'name', 'slug', 'color', 'description'
220+
'name', 'slug', 'color', 'description', 'object_types',
216221
]
217222

218223

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Generated by Django 4.1.9 on 2023-06-22 19:33
2+
3+
from django.db import migrations, models
4+
import extras.utils
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('contenttypes', '0002_remove_content_type_name'),
11+
('extras', '0093_configrevision_ordering'),
12+
]
13+
14+
operations = [
15+
migrations.AddField(
16+
model_name='tag',
17+
name='object_types',
18+
field=models.ManyToManyField(blank=True, limit_choices_to=extras.utils.FeatureQuery('tags'), related_name='+', to='contenttypes.contenttype'),
19+
),
20+
]

netbox/extras/models/tags.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
from django.conf import settings
2+
from django.contrib.contenttypes.models import ContentType
3+
from django.core.exceptions import ValidationError
24
from django.db import models
35
from django.urls import reverse
46
from django.utils.text import slugify
7+
from django.utils.translation import gettext as _
58
from taggit.models import TagBase, GenericTaggedItemBase
69

10+
from extras.utils import FeatureQuery
711
from netbox.models import ChangeLoggedModel
812
from netbox.models.features import CloningMixin, ExportTemplatesMixin
913
from utilities.choices import ColorChoices
@@ -30,9 +34,16 @@ class Tag(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel, TagBase):
3034
max_length=200,
3135
blank=True,
3236
)
37+
object_types = models.ManyToManyField(
38+
to=ContentType,
39+
related_name='+',
40+
limit_choices_to=FeatureQuery('tags'),
41+
blank=True,
42+
help_text=_("The object type(s) to which this this tag can be applied.")
43+
)
3344

3445
clone_fields = (
35-
'color', 'description',
46+
'color', 'description', 'object_types',
3647
)
3748

3849
class Meta:

netbox/extras/signals.py

+20-1
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010
from netbox.config import get_config
1111
from netbox.context import current_request, webhooks_queue
1212
from netbox.signals import post_clean
13+
from utilities.exceptions import AbortRequest
1314
from .choices import ObjectChangeActionChoices
14-
from .models import ConfigRevision, CustomField, ObjectChange
15+
from .models import ConfigRevision, CustomField, ObjectChange, TaggedItem
1516
from .webhooks import enqueue_object, get_snapshots, serialize_for_webhook
1617

1718
#
@@ -207,3 +208,21 @@ def update_config(sender, instance, **kwargs):
207208
Update the cached NetBox configuration when a new ConfigRevision is created.
208209
"""
209210
instance.activate()
211+
212+
213+
#
214+
# Tags
215+
#
216+
217+
@receiver(m2m_changed, sender=TaggedItem)
218+
def validate_assigned_tags(sender, instance, action, model, pk_set, **kwargs):
219+
"""
220+
Validate that any Tags being assigned to the instance are not restricted to non-applicable object types.
221+
"""
222+
if action != 'pre_add':
223+
return
224+
ct = ContentType.objects.get_for_model(instance)
225+
# Retrieve any applied Tags that are restricted to certain object_types
226+
for tag in model.objects.filter(pk__in=pk_set, object_types__isnull=False).prefetch_related('object_types'):
227+
if ct not in tag.object_types.all():
228+
raise AbortRequest(f"Tag {tag} cannot be assigned to {ct.model} objects.")

netbox/extras/tables/tables.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -210,10 +210,14 @@ class TagTable(NetBoxTable):
210210
linkify=True
211211
)
212212
color = columns.ColorColumn()
213+
object_types = columns.ContentTypesColumn()
213214

214215
class Meta(NetBoxTable.Meta):
215216
model = Tag
216-
fields = ('pk', 'id', 'name', 'items', 'slug', 'color', 'description', 'created', 'last_updated', 'actions')
217+
fields = (
218+
'pk', 'id', 'name', 'items', 'slug', 'color', 'description', 'object_types', 'created', 'last_updated',
219+
'actions',
220+
)
217221
default_columns = ('pk', 'name', 'items', 'slug', 'color', 'description')
218222

219223

netbox/templates/extras/tag.html

+17-3
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,23 @@ <h5 class="card-header">
4343
</div>
4444
<div class="col col-md-6">
4545
<div class="card">
46-
<h5 class="card-header">
47-
Tagged Item Types
48-
</h5>
46+
<h5 class="card-header">Allowed Object Types</h5>
47+
<div class="card-body">
48+
<table class="table table-hover attr-table">
49+
{% for ct in object.object_types.all %}
50+
<tr>
51+
<td>{{ ct }}</td>
52+
</tr>
53+
{% empty %}
54+
<tr>
55+
<td class="text-muted">Any</td>
56+
</tr>
57+
{% endfor %}
58+
</table>
59+
</div>
60+
</div>
61+
<div class="card">
62+
<h5 class="card-header">Tagged Item Types</h5>
4963
<div class="card-body">
5064
<table class="table table-hover panel-body attr-table">
5165
{% for object_type in object_types %}

0 commit comments

Comments
 (0)