diff --git a/docs/models/extras/tag.md b/docs/models/extras/tag.md index 39de48261..c4bc91b5a 100644 --- a/docs/models/extras/tag.md +++ b/docs/models/extras/tag.md @@ -16,6 +16,12 @@ A unique URL-friendly identifier. (This value will be used for filtering.) This The color to use when displaying the tag in the NetBox UI. +### Weight + +A numeric weight employed to influence the ordering of tags. Tags with a lower weight will be listed before those with higher weights. Values must be within the range **0** to **32767**. + +!!! info "This field was introduced in NetBox v4.3." + ### Object Types 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. diff --git a/netbox/extras/api/serializers_/tags.py b/netbox/extras/api/serializers_/tags.py index ea964ff52..5dc39584f 100644 --- a/netbox/extras/api/serializers_/tags.py +++ b/netbox/extras/api/serializers_/tags.py @@ -27,8 +27,8 @@ class TagSerializer(ValidatedModelSerializer): class Meta: model = Tag fields = [ - 'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'object_types', - 'tagged_items', 'created', 'last_updated', + 'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'weight', + 'object_types', 'tagged_items', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'name', 'slug', 'color', 'description') diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index 98302d0f4..635102be2 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -450,7 +450,7 @@ class TagFilterSet(ChangeLoggedModelFilterSet): class Meta: model = Tag - fields = ('id', 'name', 'slug', 'color', 'description', 'object_types') + fields = ('id', 'name', 'slug', 'color', 'weight', 'description', 'object_types') def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/extras/forms/bulk_edit.py b/netbox/extras/forms/bulk_edit.py index 30d06683b..7adc303f5 100644 --- a/netbox/extras/forms/bulk_edit.py +++ b/netbox/extras/forms/bulk_edit.py @@ -275,6 +275,10 @@ class TagBulkEditForm(BulkEditForm): max_length=200, required=False ) + weight = forms.IntegerField( + label=_('Weight'), + required=False + ) nullable_fields = ('description',) diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py index 655a5d6ca..b680382f6 100644 --- a/netbox/extras/forms/bulk_import.py +++ b/netbox/extras/forms/bulk_import.py @@ -232,10 +232,14 @@ class EventRuleImportForm(NetBoxModelImportForm): class TagImportForm(CSVModelForm): slug = SlugField() + weight = forms.IntegerField( + label=_('Weight'), + required=False + ) class Meta: model = Tag - fields = ('name', 'slug', 'color', 'description') + fields = ('name', 'slug', 'color', 'weight', 'description') class JournalEntryImportForm(NetBoxModelImportForm): diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index a45daaf70..5591de754 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -490,15 +490,19 @@ class TagForm(forms.ModelForm): queryset=ObjectType.objects.with_feature('tags'), required=False ) + weight = forms.IntegerField( + label=_('Weight'), + required=False + ) fieldsets = ( - FieldSet('name', 'slug', 'color', 'description', 'object_types', name=_('Tag')), + FieldSet('name', 'slug', 'color', 'weight', 'description', 'object_types', name=_('Tag')), ) class Meta: model = Tag fields = [ - 'name', 'slug', 'color', 'description', 'object_types', + 'name', 'slug', 'color', 'weight', 'description', 'object_types', ] diff --git a/netbox/extras/migrations/0124_alter_tag_options_tag_weight.py b/netbox/extras/migrations/0124_alter_tag_options_tag_weight.py new file mode 100644 index 000000000..86fc71fd5 --- /dev/null +++ b/netbox/extras/migrations/0124_alter_tag_options_tag_weight.py @@ -0,0 +1,22 @@ +# Generated by Django 5.2b1 on 2025-03-17 14:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0123_remove_staging'), + ] + + operations = [ + migrations.AlterModelOptions( + name='tag', + options={'ordering': ('weight', 'name')}, + ), + migrations.AddField( + model_name='tag', + name='weight', + field=models.PositiveSmallIntegerField(default=0), + ), + ] diff --git a/netbox/extras/models/tags.py b/netbox/extras/models/tags.py index baf72baa1..4c6396172 100644 --- a/netbox/extras/models/tags.py +++ b/netbox/extras/models/tags.py @@ -40,13 +40,17 @@ class Tag(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel, TagBase): blank=True, help_text=_("The object type(s) to which this tag can be applied.") ) + weight = models.PositiveSmallIntegerField( + verbose_name=_('weight'), + default=0, + ) clone_fields = ( 'color', 'description', 'object_types', ) class Meta: - ordering = ['name'] + ordering = ('weight', 'name') verbose_name = _('tag') verbose_name_plural = _('tags') diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py index e538c488e..a14356c1c 100644 --- a/netbox/extras/tables/tables.py +++ b/netbox/extras/tables/tables.py @@ -449,8 +449,8 @@ class TagTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = Tag fields = ( - 'pk', 'id', 'name', 'items', 'slug', 'color', 'description', 'object_types', 'created', 'last_updated', - 'actions', + 'pk', 'id', 'name', 'items', 'slug', 'color', 'weight', 'description', 'object_types', + 'created', 'last_updated', 'actions', ) default_columns = ('pk', 'name', 'items', 'slug', 'color', 'description') diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index 17f03350d..1d6dfac6d 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -513,6 +513,7 @@ class TagTest(APIViewTestCases.APIViewTestCase): { 'name': 'Tag 4', 'slug': 'tag-4', + 'weight': 1000, }, { 'name': 'Tag 5', @@ -533,7 +534,7 @@ class TagTest(APIViewTestCases.APIViewTestCase): tags = ( Tag(name='Tag 1', slug='tag-1'), Tag(name='Tag 2', slug='tag-2'), - Tag(name='Tag 3', slug='tag-3'), + Tag(name='Tag 3', slug='tag-3', weight=26), ) Tag.objects.bulk_create(tags) diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index 9684b3dbe..c6c53bfcb 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -1196,7 +1196,7 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests): tags = ( Tag(name='Tag 1', slug='tag-1', color='ff0000', description='foobar1'), Tag(name='Tag 2', slug='tag-2', color='00ff00', description='foobar2'), - Tag(name='Tag 3', slug='tag-3', color='0000ff'), + Tag(name='Tag 3', slug='tag-3', color='0000ff', weight=1000), ) Tag.objects.bulk_create(tags) tags[0].object_types.add(object_types['site']) @@ -1249,6 +1249,13 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests): ['Tag 2', 'Tag 3'] ) + def test_weight(self): + params = {'weight': [1000]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + params = {'weight': [0]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) + class TaggedItemFilterSetTestCase(TestCase): queryset = TaggedItem.objects.all() diff --git a/netbox/extras/tests/test_models.py b/netbox/extras/tests/test_models.py index c90390dd1..bf05a8c18 100644 --- a/netbox/extras/tests/test_models.py +++ b/netbox/extras/tests/test_models.py @@ -10,6 +10,40 @@ from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMac class TagTest(TestCase): + def test_default_ordering_weight_then_name_is_set(self): + Tag.objects.create(name='Tag 1', slug='tag-1', weight=100) + Tag.objects.create(name='Tag 2', slug='tag-2') + Tag.objects.create(name='Tag 3', slug='tag-3', weight=10) + Tag.objects.create(name='Tag 4', slug='tag-4', weight=10) + + tags = Tag.objects.all() + + self.assertEqual(tags[0].slug, 'tag-2') + self.assertEqual(tags[1].slug, 'tag-3') + self.assertEqual(tags[2].slug, 'tag-4') + self.assertEqual(tags[3].slug, 'tag-1') + + def test_tag_related_manager_ordering_weight_then_name(self): + tags = [ + Tag.objects.create(name='Tag 1', slug='tag-1', weight=100), + Tag.objects.create(name='Tag 2', slug='tag-2'), + Tag.objects.create(name='Tag 3', slug='tag-3', weight=10), + Tag.objects.create(name='Tag 4', slug='tag-4', weight=10), + ] + + site = Site.objects.create(name='Site 1') + for tag in tags: + site.tags.add(tag) + site.save() + + site = Site.objects.first() + tags = site.tags.all() + + self.assertEqual(tags[0].slug, 'tag-2') + self.assertEqual(tags[1].slug, 'tag-3') + self.assertEqual(tags[2].slug, 'tag-4') + self.assertEqual(tags[3].slug, 'tag-1') + def test_create_tag_unicode(self): tag = Tag(name='Testing Unicode: 台灣') tag.save() diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py index 5d82fae4c..be8d80b2b 100644 --- a/netbox/extras/tests/test_views.py +++ b/netbox/extras/tests/test_views.py @@ -441,8 +441,8 @@ class TagTestCase(ViewTestCases.OrganizationalObjectViewTestCase): tags = ( Tag(name='Tag 1', slug='tag-1'), - Tag(name='Tag 2', slug='tag-2'), - Tag(name='Tag 3', slug='tag-3'), + Tag(name='Tag 2', slug='tag-2', weight=1), + Tag(name='Tag 3', slug='tag-3', weight=32767), ) Tag.objects.bulk_create(tags) @@ -451,13 +451,14 @@ class TagTestCase(ViewTestCases.OrganizationalObjectViewTestCase): 'slug': 'tag-x', 'color': 'c0c0c0', 'comments': 'Some comments', + 'weight': 11, } cls.csv_data = ( - "name,slug,color,description", - "Tag 4,tag-4,ff0000,Fourth tag", - "Tag 5,tag-5,00ff00,Fifth tag", - "Tag 6,tag-6,0000ff,Sixth tag", + "name,slug,color,description,weight", + "Tag 4,tag-4,ff0000,Fourth tag,0", + "Tag 5,tag-5,00ff00,Fifth tag,1111", + "Tag 6,tag-6,0000ff,Sixth tag,0", ) cls.csv_update_data = ( diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py index e58037b85..a2fb8d615 100644 --- a/netbox/netbox/models/features.py +++ b/netbox/netbox/models/features.py @@ -455,7 +455,8 @@ class TagsMixin(models.Model): which is a `TaggableManager` instance. """ tags = TaggableManager( - through='extras.TaggedItem' + through='extras.TaggedItem', + ordering=('weight', 'name'), ) class Meta: diff --git a/netbox/templates/extras/tag.html b/netbox/templates/extras/tag.html index 4e1379fed..8c5eb13cd 100644 --- a/netbox/templates/extras/tag.html +++ b/netbox/templates/extras/tag.html @@ -28,6 +28,10 @@ +