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

This commit is contained in:
Jason Novinger 2025-03-19 12:17:35 -05:00 committed by GitHub
parent d25605c261
commit 6b7d23d684
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 110 additions and 18 deletions

View File

@ -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.

View File

@ -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')

View File

@ -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():

View File

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

View File

@ -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):

View File

@ -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',
]

View File

@ -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),
),
]

View File

@ -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')

View File

@ -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')

View File

@ -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)

View File

@ -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()

View File

@ -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()

View File

@ -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 = (

View File

@ -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:

View File

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