Pārlūkot izejas kodu

Closes #17841 Allows Tags to be displayed in specified order (#18930)

Jason Novinger 11 mēneši atpakaļ
vecāks
revīzija
6b7d23d684

+ 6 - 0
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.
 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
 ### 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.
 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.

+ 2 - 2
netbox/extras/api/serializers_/tags.py

@@ -27,8 +27,8 @@ class TagSerializer(ValidatedModelSerializer):
     class Meta:
     class Meta:
         model = Tag
         model = Tag
         fields = [
         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')
         brief_fields = ('id', 'url', 'display', 'name', 'slug', 'color', 'description')
 
 

+ 1 - 1
netbox/extras/filtersets.py

@@ -450,7 +450,7 @@ class TagFilterSet(ChangeLoggedModelFilterSet):
 
 
     class Meta:
     class Meta:
         model = Tag
         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):
     def search(self, queryset, name, value):
         if not value.strip():
         if not value.strip():

+ 4 - 0
netbox/extras/forms/bulk_edit.py

@@ -275,6 +275,10 @@ class TagBulkEditForm(BulkEditForm):
         max_length=200,
         max_length=200,
         required=False
         required=False
     )
     )
+    weight = forms.IntegerField(
+        label=_('Weight'),
+        required=False
+    )
 
 
     nullable_fields = ('description',)
     nullable_fields = ('description',)
 
 

+ 5 - 1
netbox/extras/forms/bulk_import.py

@@ -232,10 +232,14 @@ class EventRuleImportForm(NetBoxModelImportForm):
 
 
 class TagImportForm(CSVModelForm):
 class TagImportForm(CSVModelForm):
     slug = SlugField()
     slug = SlugField()
+    weight = forms.IntegerField(
+        label=_('Weight'),
+        required=False
+    )
 
 
     class Meta:
     class Meta:
         model = Tag
         model = Tag
-        fields = ('name', 'slug', 'color', 'description')
+        fields = ('name', 'slug', 'color', 'weight', 'description')
 
 
 
 
 class JournalEntryImportForm(NetBoxModelImportForm):
 class JournalEntryImportForm(NetBoxModelImportForm):

+ 6 - 2
netbox/extras/forms/model_forms.py

@@ -490,15 +490,19 @@ class TagForm(forms.ModelForm):
         queryset=ObjectType.objects.with_feature('tags'),
         queryset=ObjectType.objects.with_feature('tags'),
         required=False
         required=False
     )
     )
+    weight = forms.IntegerField(
+        label=_('Weight'),
+        required=False
+    )
 
 
     fieldsets = (
     fieldsets = (
-        FieldSet('name', 'slug', 'color', 'description', 'object_types', name=_('Tag')),
+        FieldSet('name', 'slug', 'color', 'weight', 'description', 'object_types', name=_('Tag')),
     )
     )
 
 
     class Meta:
     class Meta:
         model = Tag
         model = Tag
         fields = [
         fields = [
-            'name', 'slug', 'color', 'description', 'object_types',
+            'name', 'slug', 'color', 'weight', 'description', 'object_types',
         ]
         ]
 
 
 
 

+ 22 - 0
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),
+        ),
+    ]

+ 5 - 1
netbox/extras/models/tags.py

@@ -40,13 +40,17 @@ class Tag(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel, TagBase):
         blank=True,
         blank=True,
         help_text=_("The object type(s) to which this tag can be applied.")
         help_text=_("The object type(s) to which this tag can be applied.")
     )
     )
+    weight = models.PositiveSmallIntegerField(
+        verbose_name=_('weight'),
+        default=0,
+    )
 
 
     clone_fields = (
     clone_fields = (
         'color', 'description', 'object_types',
         'color', 'description', 'object_types',
     )
     )
 
 
     class Meta:
     class Meta:
-        ordering = ['name']
+        ordering = ('weight', 'name')
         verbose_name = _('tag')
         verbose_name = _('tag')
         verbose_name_plural = _('tags')
         verbose_name_plural = _('tags')
 
 

+ 2 - 2
netbox/extras/tables/tables.py

@@ -449,8 +449,8 @@ class TagTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         model = Tag
         model = Tag
         fields = (
         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')
         default_columns = ('pk', 'name', 'items', 'slug', 'color', 'description')
 
 

+ 2 - 1
netbox/extras/tests/test_api.py

@@ -513,6 +513,7 @@ class TagTest(APIViewTestCases.APIViewTestCase):
         {
         {
             'name': 'Tag 4',
             'name': 'Tag 4',
             'slug': 'tag-4',
             'slug': 'tag-4',
+            'weight': 1000,
         },
         },
         {
         {
             'name': 'Tag 5',
             'name': 'Tag 5',
@@ -533,7 +534,7 @@ class TagTest(APIViewTestCases.APIViewTestCase):
         tags = (
         tags = (
             Tag(name='Tag 1', slug='tag-1'),
             Tag(name='Tag 1', slug='tag-1'),
             Tag(name='Tag 2', slug='tag-2'),
             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)
         Tag.objects.bulk_create(tags)
 
 

+ 8 - 1
netbox/extras/tests/test_filtersets.py

@@ -1196,7 +1196,7 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
         tags = (
         tags = (
             Tag(name='Tag 1', slug='tag-1', color='ff0000', description='foobar1'),
             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 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)
         Tag.objects.bulk_create(tags)
         tags[0].object_types.add(object_types['site'])
         tags[0].object_types.add(object_types['site'])
@@ -1249,6 +1249,13 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
             ['Tag 2', 'Tag 3']
             ['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):
 class TaggedItemFilterSetTestCase(TestCase):
     queryset = TaggedItem.objects.all()
     queryset = TaggedItem.objects.all()

+ 34 - 0
netbox/extras/tests/test_models.py

@@ -10,6 +10,40 @@ from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMac
 
 
 class TagTest(TestCase):
 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):
     def test_create_tag_unicode(self):
         tag = Tag(name='Testing Unicode: 台灣')
         tag = Tag(name='Testing Unicode: 台灣')
         tag.save()
         tag.save()

+ 7 - 6
netbox/extras/tests/test_views.py

@@ -441,8 +441,8 @@ class TagTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
 
 
         tags = (
         tags = (
             Tag(name='Tag 1', slug='tag-1'),
             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)
         Tag.objects.bulk_create(tags)
 
 
@@ -451,13 +451,14 @@ class TagTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
             'slug': 'tag-x',
             'slug': 'tag-x',
             'color': 'c0c0c0',
             'color': 'c0c0c0',
             'comments': 'Some comments',
             'comments': 'Some comments',
+            'weight': 11,
         }
         }
 
 
         cls.csv_data = (
         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 = (
         cls.csv_update_data = (

+ 2 - 1
netbox/netbox/models/features.py

@@ -455,7 +455,8 @@ class TagsMixin(models.Model):
     which is a `TaggableManager` instance.
     which is a `TaggableManager` instance.
     """
     """
     tags = TaggableManager(
     tags = TaggableManager(
-        through='extras.TaggedItem'
+        through='extras.TaggedItem',
+        ordering=('weight', 'name'),
     )
     )
 
 
     class Meta:
     class Meta:

+ 4 - 0
netbox/templates/extras/tag.html

@@ -28,6 +28,10 @@
                 <span class="color-label" style="background-color: #{{ object.color }}">&nbsp;</span>
                 <span class="color-label" style="background-color: #{{ object.color }}">&nbsp;</span>
             </td>
             </td>
           </tr>
           </tr>
+          <tr>
+            <th scope="row">{% trans "Weight" %}</th>
+            <td>{{ object.weight }}</td>
+          </tr>
           <tr>
           <tr>
             <th scope="row">{% trans "Tagged Items" %}</th>
             <th scope="row">{% trans "Tagged Items" %}</th>
             <td>
             <td>