diff --git a/docs/configuration/security.md b/docs/configuration/security.md
index 172034b4f..950d2df34 100644
--- a/docs/configuration/security.md
+++ b/docs/configuration/security.md
@@ -2,7 +2,10 @@
## ALLOW_TOKEN_RETRIEVAL
-Default: True
+Default: False
+
+!!! note
+ The default value of this parameter changed from true to false in NetBox v4.3.0.
If disabled, the values of API tokens will not be displayed after each token's initial creation. A user **must** record the value of a token prior to its creation, or it will be lost. Note that this affects _all_ users, regardless of assigned permissions.
diff --git a/docs/models/extras/exporttemplate.md b/docs/models/extras/exporttemplate.md
index d2f9292c6..73be522b8 100644
--- a/docs/models/extras/exporttemplate.md
+++ b/docs/models/extras/exporttemplate.md
@@ -24,6 +24,12 @@ Jinja2 template code for rendering the exported data.
The MIME type to indicate in the response when rendering the export template (optional). Defaults to `text/plain`.
+### File Name
+
+The file name to give to the rendered export file (optional).
+
+!!! info "This field was introduced in NetBox v4.3."
+
### File Extension
The file extension to append to the file name in the response (optional).
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/docs/models/tenancy/contact.md b/docs/models/tenancy/contact.md
index eac630180..f277ab499 100644
--- a/docs/models/tenancy/contact.md
+++ b/docs/models/tenancy/contact.md
@@ -4,9 +4,11 @@ A contact represents an individual or group that has been associated with an obj
## Fields
-### Group
+### Groups
-The [contact group](./contactgroup.md) to which this contact is assigned (if any).
+The [contact groups](./contactgroup.md) to which this contact is assigned (if any).
+
+!!! info "This field was renamed from `group` to `groups` in NetBox v4.3, and now supports the assignment of a contact to more than one group."
### Name
diff --git a/netbox/dcim/api/serializers_/sites.py b/netbox/dcim/api/serializers_/sites.py
index b818cd954..90f7b5d35 100644
--- a/netbox/dcim/api/serializers_/sites.py
+++ b/netbox/dcim/api/serializers_/sites.py
@@ -27,7 +27,7 @@ class RegionSerializer(NestedGroupModelSerializer):
model = Region
fields = [
'id', 'url', 'display_url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields',
- 'created', 'last_updated', 'site_count', 'prefix_count', '_depth',
+ 'created', 'last_updated', 'site_count', 'prefix_count', 'comments', '_depth',
]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth')
@@ -41,7 +41,7 @@ class SiteGroupSerializer(NestedGroupModelSerializer):
model = SiteGroup
fields = [
'id', 'url', 'display_url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields',
- 'created', 'last_updated', 'site_count', 'prefix_count', '_depth',
+ 'created', 'last_updated', 'site_count', 'prefix_count', 'comments', '_depth',
]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth')
@@ -93,6 +93,6 @@ class LocationSerializer(NestedGroupModelSerializer):
fields = [
'id', 'url', 'display_url', 'display', 'name', 'slug', 'site', 'parent', 'status', 'tenant', 'facility',
'description', 'tags', 'custom_fields', 'created', 'last_updated', 'rack_count', 'device_count',
- 'prefix_count', '_depth',
+ 'prefix_count', 'comments', '_depth',
]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count', '_depth')
diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py
index 184b46841..58be63d8a 100644
--- a/netbox/dcim/filtersets.py
+++ b/netbox/dcim/filtersets.py
@@ -11,7 +11,8 @@ from ipam.filtersets import PrimaryIPFilterSet
from ipam.models import ASN, IPAddress, VLANTranslationPolicy, VRF
from netbox.choices import ColorChoices
from netbox.filtersets import (
- BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet,
+ BaseFilterSet, ChangeLoggedModelFilterSet, NestedGroupModelFilterSet, NetBoxModelFilterSet,
+ OrganizationalModelFilterSet,
)
from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
from tenancy.models import *
@@ -81,7 +82,7 @@ __all__ = (
)
-class RegionFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
+class RegionFilterSet(NestedGroupModelFilterSet, ContactModelFilterSet):
parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=Region.objects.all(),
label=_('Parent region (ID)'),
@@ -111,7 +112,7 @@ class RegionFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
fields = ('id', 'name', 'slug', 'description')
-class SiteGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
+class SiteGroupFilterSet(NestedGroupModelFilterSet, ContactModelFilterSet):
parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
label=_('Parent site group (ID)'),
@@ -205,7 +206,7 @@ class SiteFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe
return queryset.filter(qs_filter).distinct()
-class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalModelFilterSet):
+class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, NestedGroupModelFilterSet):
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='site__region',
@@ -275,13 +276,13 @@ class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalM
fields = ('id', 'name', 'slug', 'facility', 'description')
def search(self, queryset, name, value):
- if not value.strip():
- return queryset
- return queryset.filter(
- Q(name__icontains=value) |
- Q(facility__icontains=value) |
- Q(description__icontains=value)
- )
+ # extended in order to include querying on Location.facility
+ queryset = super().search(queryset, name, value)
+
+ if value.strip():
+ queryset = queryset | queryset.model.objects.filter(facility__icontains=value)
+
+ return queryset
class RackRoleFilterSet(OrganizationalModelFilterSet):
diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py
index b0eab1d0b..d8a9c695b 100644
--- a/netbox/dcim/forms/bulk_edit.py
+++ b/netbox/dcim/forms/bulk_edit.py
@@ -78,12 +78,13 @@ class RegionBulkEditForm(NetBoxModelBulkEditForm):
max_length=200,
required=False
)
+ comments = CommentField()
model = Region
fieldsets = (
FieldSet('parent', 'description'),
)
- nullable_fields = ('parent', 'description')
+ nullable_fields = ('parent', 'description', 'comments')
class SiteGroupBulkEditForm(NetBoxModelBulkEditForm):
@@ -97,12 +98,13 @@ class SiteGroupBulkEditForm(NetBoxModelBulkEditForm):
max_length=200,
required=False
)
+ comments = CommentField()
model = SiteGroup
fieldsets = (
FieldSet('parent', 'description'),
)
- nullable_fields = ('parent', 'description')
+ nullable_fields = ('parent', 'description', 'comments')
class SiteBulkEditForm(NetBoxModelBulkEditForm):
@@ -197,12 +199,13 @@ class LocationBulkEditForm(NetBoxModelBulkEditForm):
max_length=200,
required=False
)
+ comments = CommentField()
model = Location
fieldsets = (
FieldSet('site', 'parent', 'status', 'tenant', 'description'),
)
- nullable_fields = ('parent', 'tenant', 'description')
+ nullable_fields = ('parent', 'tenant', 'description', 'comments')
class RackRoleBulkEditForm(NetBoxModelBulkEditForm):
diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py
index 3a4091473..708bc7618 100644
--- a/netbox/dcim/forms/bulk_import.py
+++ b/netbox/dcim/forms/bulk_import.py
@@ -68,7 +68,7 @@ class RegionImportForm(NetBoxModelImportForm):
class Meta:
model = Region
- fields = ('name', 'slug', 'parent', 'description', 'tags')
+ fields = ('name', 'slug', 'parent', 'description', 'tags', 'comments')
class SiteGroupImportForm(NetBoxModelImportForm):
@@ -82,7 +82,7 @@ class SiteGroupImportForm(NetBoxModelImportForm):
class Meta:
model = SiteGroup
- fields = ('name', 'slug', 'parent', 'description')
+ fields = ('name', 'slug', 'parent', 'description', 'comments', 'tags')
class SiteImportForm(NetBoxModelImportForm):
@@ -160,7 +160,10 @@ class LocationImportForm(NetBoxModelImportForm):
class Meta:
model = Location
- fields = ('site', 'parent', 'name', 'slug', 'status', 'tenant', 'facility', 'description', 'tags')
+ fields = (
+ 'site', 'parent', 'name', 'slug', 'status', 'tenant', 'facility', 'description',
+ 'tags', 'comments',
+ )
def __init__(self, data=None, *args, **kwargs):
super().__init__(data, *args, **kwargs)
diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py
index cb16ba875..8fa7e153f 100644
--- a/netbox/dcim/forms/model_forms.py
+++ b/netbox/dcim/forms/model_forms.py
@@ -78,6 +78,7 @@ class RegionForm(NetBoxModelForm):
required=False
)
slug = SlugField()
+ comments = CommentField()
fieldsets = (
FieldSet('parent', 'name', 'slug', 'description', 'tags'),
@@ -86,7 +87,7 @@ class RegionForm(NetBoxModelForm):
class Meta:
model = Region
fields = (
- 'parent', 'name', 'slug', 'description', 'tags',
+ 'parent', 'name', 'slug', 'description', 'tags', 'comments',
)
@@ -97,6 +98,7 @@ class SiteGroupForm(NetBoxModelForm):
required=False
)
slug = SlugField()
+ comments = CommentField()
fieldsets = (
FieldSet('parent', 'name', 'slug', 'description', 'tags'),
@@ -105,7 +107,7 @@ class SiteGroupForm(NetBoxModelForm):
class Meta:
model = SiteGroup
fields = (
- 'parent', 'name', 'slug', 'description', 'tags',
+ 'parent', 'name', 'slug', 'description', 'comments', 'tags',
)
@@ -179,6 +181,7 @@ class LocationForm(TenancyForm, NetBoxModelForm):
}
)
slug = SlugField()
+ comments = CommentField()
fieldsets = (
FieldSet('site', 'parent', 'name', 'slug', 'status', 'facility', 'description', 'tags', name=_('Location')),
@@ -188,7 +191,8 @@ class LocationForm(TenancyForm, NetBoxModelForm):
class Meta:
model = Location
fields = (
- 'site', 'parent', 'name', 'slug', 'status', 'description', 'tenant_group', 'tenant', 'facility', 'tags',
+ 'site', 'parent', 'name', 'slug', 'status', 'description', 'tenant_group', 'tenant',
+ 'facility', 'tags', 'comments',
)
diff --git a/netbox/dcim/migrations/0202_location_comments_region_comments_sitegroup_comments.py b/netbox/dcim/migrations/0202_location_comments_region_comments_sitegroup_comments.py
new file mode 100644
index 000000000..ffdc5ba8a
--- /dev/null
+++ b/netbox/dcim/migrations/0202_location_comments_region_comments_sitegroup_comments.py
@@ -0,0 +1,26 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dcim', '0201_add_power_outlet_status'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='location',
+ name='comments',
+ field=models.TextField(blank=True),
+ ),
+ migrations.AddField(
+ model_name='region',
+ name='comments',
+ field=models.TextField(blank=True),
+ ),
+ migrations.AddField(
+ model_name='sitegroup',
+ name='comments',
+ field=models.TextField(blank=True),
+ ),
+ ]
diff --git a/netbox/dcim/search.py b/netbox/dcim/search.py
index 5dea2a09b..a85005679 100644
--- a/netbox/dcim/search.py
+++ b/netbox/dcim/search.py
@@ -144,6 +144,7 @@ class LocationIndex(SearchIndex):
('facility', 100),
('slug', 110),
('description', 500),
+ ('comments', 5000),
)
display_attrs = ('site', 'status', 'tenant', 'facility', 'description')
@@ -317,6 +318,7 @@ class RegionIndex(SearchIndex):
('name', 100),
('slug', 110),
('description', 500),
+ ('comments', 5000),
)
display_attrs = ('parent', 'description')
@@ -343,6 +345,7 @@ class SiteGroupIndex(SearchIndex):
('name', 100),
('slug', 110),
('description', 500),
+ ('comments', 5000),
)
display_attrs = ('parent', 'description')
diff --git a/netbox/dcim/tables/sites.py b/netbox/dcim/tables/sites.py
index 77844f086..7d2f0e0cc 100644
--- a/netbox/dcim/tables/sites.py
+++ b/netbox/dcim/tables/sites.py
@@ -32,12 +32,15 @@ class RegionTable(ContactsColumnMixin, NetBoxTable):
tags = columns.TagColumn(
url_name='dcim:region_list'
)
+ comments = columns.MarkdownColumn(
+ verbose_name=_('Comments'),
+ )
class Meta(NetBoxTable.Meta):
model = Region
fields = (
- 'pk', 'id', 'name', 'slug', 'site_count', 'description', 'contacts', 'tags', 'created', 'last_updated',
- 'actions',
+ 'pk', 'id', 'name', 'slug', 'site_count', 'description', 'comments', 'contacts', 'tags',
+ 'created', 'last_updated', 'actions',
)
default_columns = ('pk', 'name', 'site_count', 'description')
@@ -59,12 +62,15 @@ class SiteGroupTable(ContactsColumnMixin, NetBoxTable):
tags = columns.TagColumn(
url_name='dcim:sitegroup_list'
)
+ comments = columns.MarkdownColumn(
+ verbose_name=_('Comments'),
+ )
class Meta(NetBoxTable.Meta):
model = SiteGroup
fields = (
- 'pk', 'id', 'name', 'slug', 'site_count', 'description', 'contacts', 'tags', 'created', 'last_updated',
- 'actions',
+ 'pk', 'id', 'name', 'slug', 'site_count', 'description', 'comments', 'contacts', 'tags',
+ 'created', 'last_updated', 'actions',
)
default_columns = ('pk', 'name', 'site_count', 'description')
@@ -153,12 +159,15 @@ class LocationTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
actions = columns.ActionsColumn(
extra_buttons=LOCATION_BUTTONS
)
+ comments = columns.MarkdownColumn(
+ verbose_name=_('Comments'),
+ )
class Meta(NetBoxTable.Meta):
model = Location
fields = (
'pk', 'id', 'name', 'site', 'status', 'facility', 'tenant', 'tenant_group', 'rack_count', 'device_count',
- 'description', 'slug', 'contacts', 'tags', 'actions', 'created', 'last_updated',
+ 'description', 'slug', 'comments', 'contacts', 'tags', 'actions', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'site', 'status', 'facility', 'tenant', 'rack_count', 'device_count', 'description'
diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py
index 08f93f6ea..807ac77d4 100644
--- a/netbox/dcim/tests/test_api.py
+++ b/netbox/dcim/tests/test_api.py
@@ -74,6 +74,7 @@ class RegionTest(APIViewTestCases.APIViewTestCase):
{
'name': 'Region 4',
'slug': 'region-4',
+ 'comments': 'this is region 4, not region 5',
},
{
'name': 'Region 5',
@@ -86,13 +87,14 @@ class RegionTest(APIViewTestCases.APIViewTestCase):
]
bulk_update_data = {
'description': 'New description',
+ 'comments': 'New comments',
}
@classmethod
def setUpTestData(cls):
Region.objects.create(name='Region 1', slug='region-1')
- Region.objects.create(name='Region 2', slug='region-2')
+ Region.objects.create(name='Region 2', slug='region-2', comments='what in the world is happening?')
Region.objects.create(name='Region 3', slug='region-3')
@@ -103,26 +105,30 @@ class SiteGroupTest(APIViewTestCases.APIViewTestCase):
{
'name': 'Site Group 4',
'slug': 'site-group-4',
+ 'comments': '',
},
{
'name': 'Site Group 5',
'slug': 'site-group-5',
+ 'comments': 'not actually empty',
},
{
'name': 'Site Group 6',
'slug': 'site-group-6',
+ 'comments': 'Do I really exist?',
},
]
bulk_update_data = {
'description': 'New description',
+ 'comments': 'I do exist!',
}
@classmethod
def setUpTestData(cls):
SiteGroup.objects.create(name='Site Group 1', slug='site-group-1')
- SiteGroup.objects.create(name='Site Group 2', slug='site-group-2')
- SiteGroup.objects.create(name='Site Group 3', slug='site-group-3')
+ SiteGroup.objects.create(name='Site Group 2', slug='site-group-2', comments='')
+ SiteGroup.objects.create(name='Site Group 3', slug='site-group-3', comments='Hi!')
class SiteTest(APIViewTestCases.APIViewTestCase):
@@ -212,12 +218,14 @@ class LocationTest(APIViewTestCases.APIViewTestCase):
name='Parent Location 1',
slug='parent-location-1',
status=LocationStatusChoices.STATUS_ACTIVE,
+ comments='First!'
),
Location.objects.create(
site=sites[1],
name='Parent Location 2',
slug='parent-location-2',
status=LocationStatusChoices.STATUS_ACTIVE,
+ comments='Second!'
),
)
@@ -227,6 +235,7 @@ class LocationTest(APIViewTestCases.APIViewTestCase):
slug='location-1',
parent=parent_locations[0],
status=LocationStatusChoices.STATUS_ACTIVE,
+ comments='Third!'
)
Location.objects.create(
site=sites[0],
@@ -250,6 +259,7 @@ class LocationTest(APIViewTestCases.APIViewTestCase):
'site': sites[1].pk,
'parent': parent_locations[1].pk,
'status': LocationStatusChoices.STATUS_PLANNED,
+ 'comments': '',
},
{
'name': 'Test Location 5',
@@ -257,6 +267,7 @@ class LocationTest(APIViewTestCases.APIViewTestCase):
'site': sites[1].pk,
'parent': parent_locations[1].pk,
'status': LocationStatusChoices.STATUS_PLANNED,
+ 'comments': 'Somebody should check on this location',
},
{
'name': 'Test Location 6',
diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py
index 640499a05..f46391310 100644
--- a/netbox/dcim/tests/test_filtersets.py
+++ b/netbox/dcim/tests/test_filtersets.py
@@ -67,9 +67,15 @@ class RegionTestCase(TestCase, ChangeLoggedFilterSetTests):
def setUpTestData(cls):
parent_regions = (
- Region(name='Region 1', slug='region-1', description='foobar1'),
- Region(name='Region 2', slug='region-2', description='foobar2'),
- Region(name='Region 3', slug='region-3', description='foobar3'),
+ Region(
+ name='Region 1', slug='region-1', description='foobar1', comments="There's nothing that",
+ ),
+ Region(
+ name='Region 2', slug='region-2', description='foobar2', comments='a hundred men or more',
+ ),
+ Region(
+ name='Region 3', slug='region-3', description='foobar3', comments='could ever do'
+ ),
)
for region in parent_regions:
region.save()
@@ -100,6 +106,13 @@ class RegionTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'q': 'foobar1'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+ def test_q_comments(self):
+ params = {'q': 'there'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+ params = {'q': 'hundred men could'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
+
def test_name(self):
params = {'name': ['Region 1', 'Region 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -148,13 +161,17 @@ class SiteGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
SiteGroup(name='Site Group 2A', slug='site-group-2a', parent=parent_groups[1]),
SiteGroup(name='Site Group 2B', slug='site-group-2b', parent=parent_groups[1]),
SiteGroup(name='Site Group 3A', slug='site-group-3a', parent=parent_groups[2]),
- SiteGroup(name='Site Group 3B', slug='site-group-3b', parent=parent_groups[2]),
+ SiteGroup(
+ name='Site Group 3B', slug='site-group-3b', parent=parent_groups[2], comments='this is a parent group',
+ ),
)
for site_group in groups:
site_group.save()
child_groups = (
- SiteGroup(name='Site Group 1A1', slug='site-group-1a1', parent=groups[0]),
+ SiteGroup(
+ name='Site Group 1A1', slug='site-group-1a1', parent=groups[0], comments='this is a child group',
+ ),
SiteGroup(name='Site Group 1B1', slug='site-group-1b1', parent=groups[1]),
SiteGroup(name='Site Group 2A1', slug='site-group-2a1', parent=groups[2]),
SiteGroup(name='Site Group 2B1', slug='site-group-2b1', parent=groups[3]),
@@ -168,6 +185,13 @@ class SiteGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'q': 'foobar1'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+ def test_q_comments(self):
+ params = {'q': 'this'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ params = {'q': 'child'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['Site Group 1', 'Site Group 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -401,6 +425,7 @@ class LocationTestCase(TestCase, ChangeLoggedFilterSetTests):
status=LocationStatusChoices.STATUS_PLANNED,
facility='Facility 1',
description='foobar1',
+ comments='',
),
Location(
name='Location 2A',
@@ -410,6 +435,7 @@ class LocationTestCase(TestCase, ChangeLoggedFilterSetTests):
status=LocationStatusChoices.STATUS_STAGING,
facility='Facility 2',
description='foobar2',
+ comments='First comment!',
),
Location(
name='Location 3A',
@@ -419,6 +445,7 @@ class LocationTestCase(TestCase, ChangeLoggedFilterSetTests):
status=LocationStatusChoices.STATUS_DECOMMISSIONING,
facility='Facility 3',
description='foobar3',
+ comments='_This_ is a **bold comment**',
),
)
for location in locations:
@@ -436,6 +463,13 @@ class LocationTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'q': 'foobar1'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+ def test_q_comments(self):
+ params = {'q': 'this'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+ params = {'q': 'comment'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
def test_name(self):
params = {'name': ['Location 1', 'Location 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py
index 4dea94c7d..83effa188 100644
--- a/netbox/dcim/tests/test_views.py
+++ b/netbox/dcim/tests/test_views.py
@@ -25,8 +25,10 @@ class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
# Create three Regions
regions = (
- Region(name='Region 1', slug='region-1'),
- Region(name='Region 2', slug='region-2'),
+ Region(name='Region 1', slug='region-1', comments=''),
+ Region(
+ name='Region 2', slug='region-2', comments="It's going to take a lot to drag me away from you"
+ ),
Region(name='Region 3', slug='region-3'),
)
for region in regions:
@@ -40,13 +42,14 @@ class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
'parent': regions[2].pk,
'description': 'A new region',
'tags': [t.pk for t in tags],
+ 'comments': 'This comment is really exciting!',
}
cls.csv_data = (
- "name,slug,description",
- "Region 4,region-4,Fourth region",
- "Region 5,region-5,Fifth region",
- "Region 6,region-6,Sixth region",
+ "name,slug,description,comments",
+ "Region 4,region-4,Fourth region,",
+ "Region 5,region-5,Fifth region,hi guys",
+ "Region 6,region-6,Sixth region,bye guys",
)
cls.csv_update_data = (
@@ -58,6 +61,7 @@ class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
cls.bulk_edit_data = {
'description': 'New description',
+ 'comments': 'This comment is super exciting!!!',
}
@@ -69,7 +73,7 @@ class SiteGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
# Create three SiteGroups
sitegroups = (
- SiteGroup(name='Site Group 1', slug='site-group-1'),
+ SiteGroup(name='Site Group 1', slug='site-group-1', comments='Still here'),
SiteGroup(name='Site Group 2', slug='site-group-2'),
SiteGroup(name='Site Group 3', slug='site-group-3'),
)
@@ -84,24 +88,26 @@ class SiteGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
'parent': sitegroups[2].pk,
'description': 'A new site group',
'tags': [t.pk for t in tags],
+ 'comments': 'still here',
}
cls.csv_data = (
- "name,slug,description",
- "Site Group 4,site-group-4,Fourth site group",
- "Site Group 5,site-group-5,Fifth site group",
- "Site Group 6,site-group-6,Sixth site group",
+ "name,slug,description,comments",
+ "Site Group 4,site-group-4,Fourth site group,",
+ "Site Group 5,site-group-5,Fifth site group,still hear",
+ "Site Group 6,site-group-6,Sixth site group,"
)
cls.csv_update_data = (
- "id,name,description",
- f"{sitegroups[0].pk},Site Group 7,Fourth site group7",
- f"{sitegroups[1].pk},Site Group 8,Fifth site group8",
- f"{sitegroups[2].pk},Site Group 0,Sixth site group9",
+ "id,name,description,comments",
+ f"{sitegroups[0].pk},Site Group 7,Fourth site group7,",
+ f"{sitegroups[1].pk},Site Group 8,Fifth site group8,when will it end",
+ f"{sitegroups[2].pk},Site Group 0,Sixth site group9,",
)
cls.bulk_edit_data = {
'description': 'New description',
+ 'comments': 'the end',
}
@@ -202,6 +208,7 @@ class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
site=site,
status=LocationStatusChoices.STATUS_ACTIVE,
tenant=tenant,
+ comments='',
),
Location(
name='Location 2',
@@ -209,6 +216,7 @@ class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
site=site,
status=LocationStatusChoices.STATUS_ACTIVE,
tenant=tenant,
+ comments='First comment!',
),
Location(
name='Location 3',
@@ -216,6 +224,7 @@ class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
site=site,
status=LocationStatusChoices.STATUS_ACTIVE,
tenant=tenant,
+ comments='_This_ is a **bold comment**',
),
)
for location in locations:
@@ -232,24 +241,26 @@ class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
'tenant': tenant.pk,
'description': 'A new location',
'tags': [t.pk for t in tags],
+ 'comments': 'This comment is really boring',
}
cls.csv_data = (
- "site,tenant,name,slug,status,description",
- "Site 1,Tenant 1,Location 4,location-4,planned,Fourth location",
- "Site 1,Tenant 1,Location 5,location-5,planned,Fifth location",
- "Site 1,Tenant 1,Location 6,location-6,planned,Sixth location",
+ "site,tenant,name,slug,status,description,comments",
+ "Site 1,Tenant 1,Location 4,location-4,planned,Fourth location,",
+ "Site 1,Tenant 1,Location 5,location-5,planned,Fifth location,",
+ "Site 1,Tenant 1,Location 6,location-6,planned,Sixth location,hi!",
)
cls.csv_update_data = (
- "id,name,description",
- f"{locations[0].pk},Location 7,Fourth location7",
- f"{locations[1].pk},Location 8,Fifth location8",
- f"{locations[2].pk},Location 0,Sixth location9",
+ "id,name,description,comments",
+ f"{locations[0].pk},Location 7,Fourth location7,Useful comment",
+ f"{locations[1].pk},Location 8,Fifth location8,unuseful comment",
+ f"{locations[2].pk},Location 0,Sixth location9,",
)
cls.bulk_edit_data = {
'description': 'New description',
+ 'comments': 'This comment is also really boring',
}
diff --git a/netbox/extras/api/serializers_/exporttemplates.py b/netbox/extras/api/serializers_/exporttemplates.py
index 11f502a02..ad77cd1f7 100644
--- a/netbox/extras/api/serializers_/exporttemplates.py
+++ b/netbox/extras/api/serializers_/exporttemplates.py
@@ -27,7 +27,7 @@ class ExportTemplateSerializer(ValidatedModelSerializer):
model = ExportTemplate
fields = [
'id', 'url', 'display_url', 'display', 'object_types', 'name', 'description', 'template_code', 'mime_type',
- 'file_extension', 'as_attachment', 'data_source', 'data_path', 'data_file', 'data_synced', 'created',
- 'last_updated',
+ 'file_name', 'file_extension', 'as_attachment', 'data_source', 'data_path', 'data_file', 'data_synced',
+ 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
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..e63b6d673 100644
--- a/netbox/extras/filtersets.py
+++ b/netbox/extras/filtersets.py
@@ -258,8 +258,8 @@ class ExportTemplateFilterSet(ChangeLoggedModelFilterSet):
class Meta:
model = ExportTemplate
fields = (
- 'id', 'name', 'description', 'mime_type', 'file_extension', 'as_attachment', 'auto_sync_enabled',
- 'data_synced',
+ 'id', 'name', 'description', 'mime_type', 'file_name', 'file_extension', 'as_attachment',
+ 'auto_sync_enabled', 'data_synced',
)
def search(self, queryset, name, value):
@@ -267,7 +267,8 @@ class ExportTemplateFilterSet(ChangeLoggedModelFilterSet):
return queryset
return queryset.filter(
Q(name__icontains=value) |
- Q(description__icontains=value)
+ Q(description__icontains=value) |
+ Q(file_name__icontains=value)
)
@@ -450,7 +451,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..6891edc5d 100644
--- a/netbox/extras/forms/bulk_edit.py
+++ b/netbox/extras/forms/bulk_edit.py
@@ -155,6 +155,10 @@ class ExportTemplateBulkEditForm(BulkEditForm):
max_length=50,
required=False
)
+ file_name = forms.CharField(
+ label=_('File name'),
+ required=False
+ )
file_extension = forms.CharField(
label=_('File extension'),
max_length=15,
@@ -166,7 +170,7 @@ class ExportTemplateBulkEditForm(BulkEditForm):
widget=BulkEditNullBooleanSelect()
)
- nullable_fields = ('description', 'mime_type', 'file_extension')
+ nullable_fields = ('description', 'mime_type', 'file_name', 'file_extension')
class SavedFilterBulkEditForm(BulkEditForm):
@@ -275,6 +279,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..fb522bd7b 100644
--- a/netbox/extras/forms/bulk_import.py
+++ b/netbox/extras/forms/bulk_import.py
@@ -144,7 +144,8 @@ class ExportTemplateImportForm(CSVModelForm):
class Meta:
model = ExportTemplate
fields = (
- 'name', 'object_types', 'description', 'mime_type', 'file_extension', 'as_attachment', 'template_code',
+ 'name', 'object_types', 'description', 'mime_type', 'file_name', 'file_extension', 'as_attachment',
+ 'template_code',
)
@@ -232,10 +233,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/filtersets.py b/netbox/extras/forms/filtersets.py
index 05dcf96c4..1691559f9 100644
--- a/netbox/extras/forms/filtersets.py
+++ b/netbox/extras/forms/filtersets.py
@@ -162,7 +162,7 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
FieldSet('q', 'filter_id'),
FieldSet('data_source_id', 'data_file_id', name=_('Data')),
- FieldSet('object_type_id', 'mime_type', 'file_extension', 'as_attachment', name=_('Attributes')),
+ FieldSet('object_type_id', 'mime_type', 'file_name', 'file_extension', 'as_attachment', name=_('Attributes')),
)
data_source_id = DynamicModelMultipleChoiceField(
queryset=DataSource.objects.all(),
@@ -186,6 +186,10 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
required=False,
label=_('MIME type')
)
+ file_name = forms.CharField(
+ label=_('File name'),
+ required=False
+ )
file_extension = forms.CharField(
label=_('File extension'),
required=False
diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py
index a45daaf70..b5bc06b40 100644
--- a/netbox/extras/forms/model_forms.py
+++ b/netbox/extras/forms/model_forms.py
@@ -246,7 +246,7 @@ class ExportTemplateForm(SyncedDataMixin, forms.ModelForm):
fieldsets = (
FieldSet('name', 'object_types', 'description', 'template_code', name=_('Export Template')),
FieldSet('data_source', 'data_file', 'auto_sync_enabled', name=_('Data Source')),
- FieldSet('mime_type', 'file_extension', 'as_attachment', name=_('Rendering')),
+ FieldSet('mime_type', 'file_name', 'file_extension', 'as_attachment', name=_('Rendering')),
)
class Meta:
@@ -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..759ad1595
--- /dev/null
+++ b/netbox/extras/migrations/0124_alter_tag_options_tag_weight.py
@@ -0,0 +1,20 @@
+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/migrations/0125_exporttemplate_file_name.py b/netbox/extras/migrations/0125_exporttemplate_file_name.py
new file mode 100644
index 000000000..2c8ac118b
--- /dev/null
+++ b/netbox/extras/migrations/0125_exporttemplate_file_name.py
@@ -0,0 +1,16 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('extras', '0124_alter_tag_options_tag_weight'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='exporttemplate',
+ name='file_name',
+ field=models.CharField(blank=True, max_length=200),
+ ),
+ ]
diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py
index d3e443b14..3cae54f29 100644
--- a/netbox/extras/models/models.py
+++ b/netbox/extras/models/models.py
@@ -16,7 +16,7 @@ from core.models import ObjectType
from extras.choices import *
from extras.conditions import ConditionSet
from extras.constants import *
-from extras.utils import image_upload
+from extras.utils import filename_from_model, image_upload
from netbox.config import get_config
from netbox.events import get_event_type_choices
from netbox.models import ChangeLoggedModel
@@ -409,6 +409,11 @@ class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, Change
verbose_name=_('MIME type'),
help_text=_('Defaults to text/plain; charset=utf-8
')
)
+ file_name = models.CharField(
+ max_length=200,
+ blank=True,
+ help_text=_('Filename to give to the rendered export file')
+ )
file_extension = models.CharField(
verbose_name=_('file extension'),
max_length=15,
@@ -422,7 +427,7 @@ class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, Change
)
clone_fields = (
- 'object_types', 'template_code', 'mime_type', 'file_extension', 'as_attachment',
+ 'object_types', 'template_code', 'mime_type', 'file_name', 'file_extension', 'as_attachment',
)
class Meta:
@@ -480,10 +485,10 @@ class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, Change
response = HttpResponse(output, content_type=mime_type)
if self.as_attachment:
- basename = queryset.model._meta.verbose_name_plural.replace(' ', '_')
extension = f'.{self.file_extension}' if self.file_extension else ''
- filename = f'netbox_{basename}{extension}'
- response['Content-Disposition'] = f'attachment; filename="{filename}"'
+ filename = self.file_name or filename_from_model(queryset.model)
+ full_filename = f'{filename}{extension}'
+ response['Content-Disposition'] = f'attachment; filename="{full_filename}"'
return response
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..7a6e79cab 100644
--- a/netbox/extras/tables/tables.py
+++ b/netbox/extras/tables/tables.py
@@ -203,11 +203,12 @@ class ExportTemplateTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = ExportTemplate
fields = (
- 'pk', 'id', 'name', 'object_types', 'description', 'mime_type', 'file_extension', 'as_attachment',
- 'data_source', 'data_file', 'data_synced', 'created', 'last_updated',
+ 'pk', 'id', 'name', 'object_types', 'description', 'mime_type', 'file_name', 'file_extension',
+ 'as_attachment', 'data_source', 'data_file', 'data_synced', 'created', 'last_updated',
)
default_columns = (
- 'pk', 'name', 'object_types', 'description', 'mime_type', 'file_extension', 'as_attachment', 'is_synced',
+ 'pk', 'name', 'object_types', 'description', 'mime_type', 'file_name', 'file_extension',
+ 'as_attachment', 'is_synced',
)
@@ -449,8 +450,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..7a4d63549 100644
--- a/netbox/extras/tests/test_api.py
+++ b/netbox/extras/tests/test_api.py
@@ -479,6 +479,7 @@ class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
'object_types': ['dcim.device'],
'name': 'Test Export Template 6',
'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
+ 'file_name': 'test_export_template_6',
},
]
bulk_update_data = {
@@ -494,7 +495,9 @@ class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
),
ExportTemplate(
name='Export Template 2',
- template_code='{% for obj in queryset %}{{ obj.name }}\n{% endfor %}'
+ template_code='{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
+ file_name='export_template_2',
+ file_extension='test',
),
ExportTemplate(
name='Export Template 3',
@@ -502,8 +505,10 @@ class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
),
)
ExportTemplate.objects.bulk_create(export_templates)
+
+ device_object_type = ObjectType.objects.get_for_model(Device)
for et in export_templates:
- et.object_types.set([ObjectType.objects.get_for_model(Device)])
+ et.object_types.set([device_object_type])
class TagTest(APIViewTestCases.APIViewTestCase):
@@ -513,6 +518,7 @@ class TagTest(APIViewTestCases.APIViewTestCase):
{
'name': 'Tag 4',
'slug': 'tag-4',
+ 'weight': 1000,
},
{
'name': 'Tag 5',
@@ -533,7 +539,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..ff4543bd2 100644
--- a/netbox/extras/tests/test_filtersets.py
+++ b/netbox/extras/tests/test_filtersets.py
@@ -624,8 +624,11 @@ class ExportTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
export_templates = (
ExportTemplate(name='Export Template 1', template_code='TESTING', description='foobar1'),
- ExportTemplate(name='Export Template 2', template_code='TESTING', description='foobar2'),
- ExportTemplate(name='Export Template 3', template_code='TESTING'),
+ ExportTemplate(
+ name='Export Template 2', template_code='TESTING', description='foobar2',
+ file_name='export_template_2', file_extension='nagios',
+ ),
+ ExportTemplate(name='Export Template 3', template_code='TESTING', file_name='export_filename'),
)
ExportTemplate.objects.bulk_create(export_templates)
for i, et in enumerate(export_templates):
@@ -635,6 +638,9 @@ class ExportTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'q': 'foobar1'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+ params = {'q': 'export_filename'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['Export Template 1', 'Export Template 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -649,6 +655,20 @@ class ExportTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ def test_file_name(self):
+ params = {'file_name': ['export_filename']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+ def test_file_extension(self):
+ params = {'file_extension': ['nagios']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+ params = {'file_name': ['export_template_2'], 'file_extension': ['nagios']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+ params = {'file_name': 'export_filename', 'file_extension': ['nagios']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
+
class ImageAttachmentTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = ImageAttachment.objects.all()
@@ -1196,7 +1216,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 +1269,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_utils.py b/netbox/extras/tests/test_utils.py
new file mode 100644
index 000000000..b851acab8
--- /dev/null
+++ b/netbox/extras/tests/test_utils.py
@@ -0,0 +1,19 @@
+from django.test import TestCase
+
+from extras.models import ExportTemplate
+from extras.utils import filename_from_model
+from tenancy.models import ContactGroup, TenantGroup
+from wireless.models import WirelessLANGroup
+
+
+class FilenameFromModelTests(TestCase):
+ def test_expected_output(self):
+ cases = (
+ (ExportTemplate, 'netbox_export_templates'),
+ (ContactGroup, 'netbox_contact_groups'),
+ (TenantGroup, 'netbox_tenant_groups'),
+ (WirelessLANGroup, 'netbox_wireless_lan_groups'),
+ )
+
+ for model, expected in cases:
+ self.assertEqual(filename_from_model(model), expected)
diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py
index 5d82fae4c..0688cd2c2 100644
--- a/netbox/extras/tests/test_views.py
+++ b/netbox/extras/tests/test_views.py
@@ -305,7 +305,7 @@ class ExportTemplateTestCase(ViewTestCases.PrimaryObjectViewTestCase):
export_templates = (
ExportTemplate(name='Export Template 1', template_code=TEMPLATE_CODE),
ExportTemplate(name='Export Template 2', template_code=TEMPLATE_CODE),
- ExportTemplate(name='Export Template 3', template_code=TEMPLATE_CODE),
+ ExportTemplate(name='Export Template 3', template_code=TEMPLATE_CODE, file_name='export_template_3')
)
ExportTemplate.objects.bulk_create(export_templates)
for et in export_templates:
@@ -315,13 +315,14 @@ class ExportTemplateTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'name': 'Export Template X',
'object_types': [site_type.pk],
'template_code': TEMPLATE_CODE,
+ 'file_name': 'template_x',
}
cls.csv_data = (
- "name,object_types,template_code",
- f"Export Template 4,dcim.site,{TEMPLATE_CODE}",
- f"Export Template 5,dcim.site,{TEMPLATE_CODE}",
- f"Export Template 6,dcim.site,{TEMPLATE_CODE}",
+ "name,object_types,template_code,file_name",
+ f"Export Template 4,dcim.site,{TEMPLATE_CODE},",
+ f"Export Template 5,dcim.site,{TEMPLATE_CODE},template_5",
+ f"Export Template 6,dcim.site,{TEMPLATE_CODE},",
)
cls.csv_update_data = (
@@ -441,8 +442,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 +452,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/extras/utils.py b/netbox/extras/utils.py
index efe7ada5b..411d80f78 100644
--- a/netbox/extras/utils.py
+++ b/netbox/extras/utils.py
@@ -1,6 +1,7 @@
import importlib
from django.core.exceptions import ImproperlyConfigured
+from django.db import models
from taggit.managers import _TaggableManager
from netbox.context import current_request
@@ -15,6 +16,12 @@ __all__ = (
)
+def filename_from_model(model: models.Model) -> str:
+ """Standardises how we generate filenames from model class for exports"""
+ base = model._meta.verbose_name_plural.lower().replace(' ', '_')
+ return f'netbox_{base}'
+
+
def is_taggable(obj):
"""
Return True if the instance can have Tags assigned to it; False otherwise.
diff --git a/netbox/netbox/configuration_testing.py b/netbox/netbox/configuration_testing.py
index 142b50bb0..52973e94d 100644
--- a/netbox/netbox/configuration_testing.py
+++ b/netbox/netbox/configuration_testing.py
@@ -43,6 +43,8 @@ SECRET_KEY = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
DEFAULT_PERMISSIONS = {}
+ALLOW_TOKEN_RETRIEVAL = True
+
LOGGING = {
'version': 1,
'disable_existing_loggers': True
diff --git a/netbox/netbox/filtersets.py b/netbox/netbox/filtersets.py
index b8fbe7ad5..d80b07e90 100644
--- a/netbox/netbox/filtersets.py
+++ b/netbox/netbox/filtersets.py
@@ -329,3 +329,19 @@ class OrganizationalModelFilterSet(NetBoxModelFilterSet):
models.Q(slug__icontains=value) |
models.Q(description__icontains=value)
)
+
+
+class NestedGroupModelFilterSet(NetBoxModelFilterSet):
+ """
+ A base FilterSet for models that inherit from NestedGroupModel
+ """
+ def search(self, queryset, name, value):
+ if value.strip():
+ queryset = queryset.filter(
+ models.Q(name__icontains=value) |
+ models.Q(slug__icontains=value) |
+ models.Q(description__icontains=value) |
+ models.Q(comments__icontains=value)
+ )
+
+ return queryset
diff --git a/netbox/netbox/models/__init__.py b/netbox/netbox/models/__init__.py
index b1f7cfd48..3ad0ac556 100644
--- a/netbox/netbox/models/__init__.py
+++ b/netbox/netbox/models/__init__.py
@@ -150,6 +150,10 @@ class NestedGroupModel(NetBoxFeatureSet, MPTTModel):
max_length=200,
blank=True
)
+ comments = models.TextField(
+ verbose_name=_('comments'),
+ blank=True
+ )
objects = TreeManager()
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/netbox/settings.py b/netbox/netbox/settings.py
index 3b47ab541..9f9b25689 100644
--- a/netbox/netbox/settings.py
+++ b/netbox/netbox/settings.py
@@ -64,7 +64,7 @@ elif hasattr(configuration, 'DATABASE') and hasattr(configuration, 'DATABASES'):
# Set static config parameters
ADMINS = getattr(configuration, 'ADMINS', [])
-ALLOW_TOKEN_RETRIEVAL = getattr(configuration, 'ALLOW_TOKEN_RETRIEVAL', True)
+ALLOW_TOKEN_RETRIEVAL = getattr(configuration, 'ALLOW_TOKEN_RETRIEVAL', False)
ALLOWED_HOSTS = getattr(configuration, 'ALLOWED_HOSTS') # Required
AUTH_PASSWORD_VALIDATORS = getattr(configuration, 'AUTH_PASSWORD_VALIDATORS', [
{
diff --git a/netbox/templates/dcim/location.html b/netbox/templates/dcim/location.html
index 97dcc20f0..02e02a1ed 100644
--- a/netbox/templates/dcim/location.html
+++ b/netbox/templates/dcim/location.html
@@ -62,6 +62,7 @@
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/custom_fields.html' %}
+ {% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
{% trans "Group" %} | -{{ object.group|linkify|placeholder }} | +{% trans "Groups" %} | +
+ {% if object.groups.all|length > 0 %}
+
|
---|---|---|---|
{% trans "Name" %} | diff --git a/netbox/templates/tenancy/contactgroup.html b/netbox/templates/tenancy/contactgroup.html index bf6928c15..25b1da440 100644 --- a/netbox/templates/tenancy/contactgroup.html +++ b/netbox/templates/tenancy/contactgroup.html @@ -32,6 +32,7 @@