nautobot 2.4.4__py3-none-any.whl → 2.4.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nautobot/__init__.py +19 -3
- nautobot/core/api/mixins.py +10 -0
- nautobot/core/celery/__init__.py +5 -3
- nautobot/core/celery/encoders.py +2 -2
- nautobot/core/forms/fields.py +21 -5
- nautobot/core/forms/utils.py +1 -0
- nautobot/core/jobs/__init__.py +3 -2
- nautobot/core/jobs/bulk_actions.py +1 -1
- nautobot/core/management/commands/generate_test_data.py +1 -1
- nautobot/core/models/name_color_content_types.py +9 -0
- nautobot/core/models/validators.py +7 -0
- nautobot/core/settings.py +0 -14
- nautobot/core/settings.yaml +0 -28
- nautobot/core/tables.py +6 -1
- nautobot/core/templates/generic/object_retrieve.html +1 -1
- nautobot/core/testing/__init__.py +2 -0
- nautobot/core/testing/api.py +18 -0
- nautobot/core/testing/mixins.py +9 -0
- nautobot/core/tests/nautobot_config.py +0 -2
- nautobot/core/tests/runner.py +17 -140
- nautobot/core/tests/test_api.py +4 -4
- nautobot/core/tests/test_authentication.py +83 -4
- nautobot/core/tests/test_forms.py +11 -8
- nautobot/core/tests/test_graphql.py +9 -0
- nautobot/core/tests/test_jobs.py +33 -27
- nautobot/core/ui/object_detail.py +31 -0
- nautobot/dcim/factory.py +2 -0
- nautobot/dcim/filters/__init__.py +5 -0
- nautobot/dcim/forms.py +17 -1
- nautobot/dcim/migrations/0068_alter_softwareimagefile_download_url.py +19 -0
- nautobot/dcim/migrations/0069_softwareimagefile_external_integration.py +25 -0
- nautobot/dcim/models/devices.py +9 -2
- nautobot/dcim/tables/devices.py +1 -0
- nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +4 -0
- nautobot/dcim/tests/test_api.py +74 -31
- nautobot/dcim/tests/test_filters.py +2 -0
- nautobot/dcim/tests/test_jobs.py +4 -6
- nautobot/dcim/tests/test_models.py +65 -0
- nautobot/dcim/tests/test_views.py +3 -0
- nautobot/extras/choices.py +8 -3
- nautobot/extras/forms/forms.py +7 -3
- nautobot/extras/jobs.py +181 -103
- nautobot/extras/management/utils.py +13 -2
- nautobot/extras/models/datasources.py +4 -1
- nautobot/extras/models/jobs.py +20 -17
- nautobot/extras/plugins/marketplace_manifest.yml +18 -0
- nautobot/extras/tables.py +29 -34
- nautobot/extras/templates/extras/inc/panel_changelog.html +1 -1
- nautobot/extras/templates/extras/inc/panel_jobhistory.html +1 -1
- nautobot/extras/templates/extras/status.html +1 -37
- nautobot/extras/test_jobs/atomic_transaction.py +6 -6
- nautobot/extras/test_jobs/fail.py +75 -1
- nautobot/extras/tests/integration/test_notes.py +1 -1
- nautobot/extras/tests/test_api.py +23 -8
- nautobot/extras/tests/test_changelog.py +4 -4
- nautobot/extras/tests/test_customfields.py +3 -0
- nautobot/extras/tests/test_datasources.py +64 -54
- nautobot/extras/tests/test_jobs.py +69 -62
- nautobot/extras/tests/test_models.py +1 -1
- nautobot/extras/tests/test_plugins.py +19 -13
- nautobot/extras/tests/test_relationships.py +14 -5
- nautobot/extras/tests/test_tags.py +2 -2
- nautobot/extras/tests/test_views.py +15 -6
- nautobot/extras/urls.py +1 -30
- nautobot/extras/views.py +17 -55
- nautobot/ipam/forms.py +15 -0
- nautobot/ipam/querysets.py +6 -0
- nautobot/ipam/tables.py +6 -2
- nautobot/ipam/templates/ipam/namespace_retrieve.html +0 -41
- nautobot/ipam/templates/ipam/rir.html +1 -43
- nautobot/ipam/templates/ipam/service.html +2 -46
- nautobot/ipam/templates/ipam/service_edit.html +1 -17
- nautobot/ipam/templates/ipam/service_retrieve.html +7 -0
- nautobot/ipam/tests/migration/__init__.py +0 -0
- nautobot/ipam/tests/migration/test_migrations.py +510 -0
- nautobot/ipam/tests/test_api.py +66 -36
- nautobot/ipam/tests/test_filters.py +0 -10
- nautobot/ipam/tests/test_models.py +16 -0
- nautobot/ipam/tests/test_views.py +44 -2
- nautobot/ipam/urls.py +2 -67
- nautobot/ipam/utils/migrations.py +185 -152
- nautobot/ipam/utils/testing.py +177 -0
- nautobot/ipam/views.py +119 -198
- nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +43 -5
- nautobot/project-static/docs/code-reference/nautobot/apps/models.html +47 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +18 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +35 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +63 -0
- nautobot/project-static/docs/development/apps/api/testing.html +0 -87
- nautobot/project-static/docs/development/apps/migration/dependency-updates.html +1 -1
- nautobot/project-static/docs/development/core/best-practices.html +3 -3
- nautobot/project-static/docs/development/core/getting-started.html +78 -107
- nautobot/project-static/docs/development/core/release-checklist.html +1 -1
- nautobot/project-static/docs/development/core/style-guide.html +1 -1
- nautobot/project-static/docs/development/core/testing.html +24 -198
- nautobot/project-static/docs/development/jobs/index.html +27 -14
- nautobot/project-static/docs/media/user-guide/administration/getting-started/nautobot-cloud.png +0 -0
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/overview/application_stack.html +1 -1
- nautobot/project-static/docs/release-notes/version-2.4.html +409 -1
- nautobot/project-static/docs/requirements.txt +1 -1
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +290 -290
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guide/administration/configuration/settings.html +2 -48
- nautobot/project-static/docs/user-guide/administration/guides/permissions.html +71 -0
- nautobot/project-static/docs/user-guide/administration/installation/http-server.html +3 -1
- nautobot/project-static/docs/user-guide/administration/installation/index.html +257 -16
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +4 -0
- nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +11 -11
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +8 -8
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +1 -0
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +40 -25
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +4 -4
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +77 -5
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +0 -1
- nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +1 -1
- nautobot/project-static/docs/user-guide/index.html +89 -2
- nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +207 -122
- nautobot/virtualization/forms.py +20 -0
- nautobot/virtualization/templates/virtualization/clustergroup.html +1 -39
- nautobot/virtualization/templates/virtualization/clustertype.html +1 -0
- nautobot/virtualization/tests/test_api.py +14 -3
- nautobot/virtualization/tests/test_views.py +10 -2
- nautobot/virtualization/urls.py +10 -93
- nautobot/virtualization/views.py +33 -72
- {nautobot-2.4.4.dist-info → nautobot-2.4.6.dist-info}/METADATA +8 -7
- {nautobot-2.4.4.dist-info → nautobot-2.4.6.dist-info}/RECORD +137 -132
- {nautobot-2.4.4.dist-info → nautobot-2.4.6.dist-info}/WHEEL +1 -1
- nautobot/core/tests/performance_baselines.yml +0 -8900
- nautobot/ipam/tests/test_migrations.py +0 -462
- /nautobot/ipam/templates/ipam/{namespace_ipaddresses.html → namespace_ip_addresses.html} +0 -0
- {nautobot-2.4.4.dist-info → nautobot-2.4.6.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.4.dist-info → nautobot-2.4.6.dist-info}/NOTICE +0 -0
- {nautobot-2.4.4.dist-info → nautobot-2.4.6.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Generated by Django 4.2.20 on 2025-03-11 19:10
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
import django.db.models.deletion
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
dependencies = [
|
|
9
|
+
("extras", "0122_add_graphqlquery_owner_content_type"),
|
|
10
|
+
("dcim", "0068_alter_softwareimagefile_download_url"),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.AddField(
|
|
15
|
+
model_name="softwareimagefile",
|
|
16
|
+
name="external_integration",
|
|
17
|
+
field=models.ForeignKey(
|
|
18
|
+
blank=True,
|
|
19
|
+
null=True,
|
|
20
|
+
on_delete=django.db.models.deletion.PROTECT,
|
|
21
|
+
related_name="software_image_files",
|
|
22
|
+
to="extras.externalintegration",
|
|
23
|
+
),
|
|
24
|
+
),
|
|
25
|
+
]
|
nautobot/dcim/models/devices.py
CHANGED
|
@@ -14,7 +14,7 @@ import yaml
|
|
|
14
14
|
|
|
15
15
|
from nautobot.core.constants import CHARFIELD_MAX_LENGTH
|
|
16
16
|
from nautobot.core.models import BaseManager, RestrictedQuerySet
|
|
17
|
-
from nautobot.core.models.fields import JSONArrayField, NaturalOrderingField
|
|
17
|
+
from nautobot.core.models.fields import JSONArrayField, LaxURLField, NaturalOrderingField
|
|
18
18
|
from nautobot.core.models.generics import BaseModel, OrganizationalModel, PrimaryModel
|
|
19
19
|
from nautobot.core.models.tree_queries import TreeModel
|
|
20
20
|
from nautobot.core.utils.config import get_settings_or_config
|
|
@@ -1239,7 +1239,14 @@ class SoftwareImageFile(PrimaryModel):
|
|
|
1239
1239
|
verbose_name="Image File Size",
|
|
1240
1240
|
help_text="Image file size in bytes",
|
|
1241
1241
|
)
|
|
1242
|
-
download_url =
|
|
1242
|
+
download_url = LaxURLField(blank=True, verbose_name="Download URL")
|
|
1243
|
+
external_integration = models.ForeignKey(
|
|
1244
|
+
to="extras.ExternalIntegration",
|
|
1245
|
+
on_delete=models.PROTECT,
|
|
1246
|
+
related_name="software_image_files",
|
|
1247
|
+
blank=True,
|
|
1248
|
+
null=True,
|
|
1249
|
+
)
|
|
1243
1250
|
default_image = models.BooleanField(
|
|
1244
1251
|
verbose_name="Default Image", help_text="Is the default image for this software version", default=False
|
|
1245
1252
|
)
|
nautobot/dcim/tables/devices.py
CHANGED
nautobot/dcim/tests/test_api.py
CHANGED
|
@@ -68,7 +68,7 @@ from nautobot.dcim.models import (
|
|
|
68
68
|
VirtualChassis,
|
|
69
69
|
VirtualDeviceContext,
|
|
70
70
|
)
|
|
71
|
-
from nautobot.extras.models import ConfigContextSchema, Role, SecretsGroup, Status
|
|
71
|
+
from nautobot.extras.models import ConfigContextSchema, ExternalIntegration, Role, SecretsGroup, Status
|
|
72
72
|
from nautobot.ipam.models import IPAddress, Namespace, Prefix, VLAN, VLANGroup
|
|
73
73
|
from nautobot.tenancy.models import Tenant
|
|
74
74
|
from nautobot.virtualization.models import Cluster, ClusterType
|
|
@@ -187,7 +187,12 @@ class Mixins:
|
|
|
187
187
|
def test_module_device_validation(self):
|
|
188
188
|
"""Assert that a modular component can have a module or a device but not both."""
|
|
189
189
|
|
|
190
|
-
self.add_permissions(
|
|
190
|
+
self.add_permissions(
|
|
191
|
+
f"{self.model._meta.app_label}.add_{self.model._meta.model_name}",
|
|
192
|
+
"dcim.view_device",
|
|
193
|
+
"dcim.view_module",
|
|
194
|
+
"extras.view_status",
|
|
195
|
+
)
|
|
191
196
|
data = {
|
|
192
197
|
self.module_field: self.module.pk,
|
|
193
198
|
self.device_field: self.device.pk,
|
|
@@ -220,7 +225,12 @@ class Mixins:
|
|
|
220
225
|
def test_module_device_name_unique_validation(self):
|
|
221
226
|
"""Assert uniqueness constraint is enforced for (device,name) and (module,name) fields."""
|
|
222
227
|
|
|
223
|
-
self.add_permissions(
|
|
228
|
+
self.add_permissions(
|
|
229
|
+
f"{self.model._meta.app_label}.add_{self.model._meta.model_name}",
|
|
230
|
+
"dcim.view_device",
|
|
231
|
+
"dcim.view_module",
|
|
232
|
+
"extras.view_status",
|
|
233
|
+
)
|
|
224
234
|
modules = Module.objects.all()[:2]
|
|
225
235
|
data = {
|
|
226
236
|
self.module_field: modules[0].pk,
|
|
@@ -266,7 +276,11 @@ class Mixins:
|
|
|
266
276
|
def test_module_type_device_type_validation(self):
|
|
267
277
|
"""Assert that a modular component template can have a module_type or a device_type but not both."""
|
|
268
278
|
|
|
269
|
-
self.add_permissions(
|
|
279
|
+
self.add_permissions(
|
|
280
|
+
f"{self.model._meta.app_label}.add_{self.model._meta.model_name}",
|
|
281
|
+
"dcim.view_devicetype",
|
|
282
|
+
"dcim.view_moduletype",
|
|
283
|
+
)
|
|
270
284
|
data = {
|
|
271
285
|
"module_type": self.module_type.pk,
|
|
272
286
|
"device_type": self.device_type.pk,
|
|
@@ -299,7 +313,11 @@ class Mixins:
|
|
|
299
313
|
def test_module_type_device_type_name_unique_validation(self):
|
|
300
314
|
"""Assert uniqueness constraint is enforced for (device_type,name) and (module_type,name) fields."""
|
|
301
315
|
|
|
302
|
-
self.add_permissions(
|
|
316
|
+
self.add_permissions(
|
|
317
|
+
f"{self.model._meta.app_label}.add_{self.model._meta.model_name}",
|
|
318
|
+
"dcim.view_devicetype",
|
|
319
|
+
"dcim.view_moduletype",
|
|
320
|
+
)
|
|
303
321
|
module_types = ModuleType.objects.all()[:2]
|
|
304
322
|
data = {
|
|
305
323
|
"module_type": module_types[0].pk,
|
|
@@ -442,7 +460,7 @@ class LocationTest(APIViewTestCases.APIViewTestCase, APIViewTestCases.TreeModelA
|
|
|
442
460
|
Test allow_null to time_zone field on locaton.
|
|
443
461
|
"""
|
|
444
462
|
|
|
445
|
-
self.add_permissions("dcim.add_location")
|
|
463
|
+
self.add_permissions("dcim.add_location", "dcim.view_locationtype", "extras.view_status")
|
|
446
464
|
url = reverse("dcim-api:location-list")
|
|
447
465
|
location = {
|
|
448
466
|
"name": "foo",
|
|
@@ -461,7 +479,7 @@ class LocationTest(APIViewTestCases.APIViewTestCase, APIViewTestCases.TreeModelA
|
|
|
461
479
|
Test disallowed blank time_zone field on location.
|
|
462
480
|
"""
|
|
463
481
|
|
|
464
|
-
self.add_permissions("dcim.add_location")
|
|
482
|
+
self.add_permissions("dcim.add_location", "dcim.view_locationtype", "extras.view_status")
|
|
465
483
|
url = reverse("dcim-api:location-list")
|
|
466
484
|
location = {
|
|
467
485
|
"name": "foo",
|
|
@@ -480,7 +498,7 @@ class LocationTest(APIViewTestCases.APIViewTestCase, APIViewTestCases.TreeModelA
|
|
|
480
498
|
Test valid time_zone field on location.
|
|
481
499
|
"""
|
|
482
500
|
|
|
483
|
-
self.add_permissions("dcim.add_location")
|
|
501
|
+
self.add_permissions("dcim.add_location", "dcim.view_locationtype", "extras.view_status")
|
|
484
502
|
url = reverse("dcim-api:location-list")
|
|
485
503
|
time_zone = "UTC"
|
|
486
504
|
location = {
|
|
@@ -500,7 +518,7 @@ class LocationTest(APIViewTestCases.APIViewTestCase, APIViewTestCases.TreeModelA
|
|
|
500
518
|
Test invalid time_zone field on location.
|
|
501
519
|
"""
|
|
502
520
|
|
|
503
|
-
self.add_permissions("dcim.add_location")
|
|
521
|
+
self.add_permissions("dcim.add_location", "dcim.view_locationtype", "extras.view_status")
|
|
504
522
|
url = reverse("dcim-api:location-list")
|
|
505
523
|
time_zone = "IDONOTEXIST"
|
|
506
524
|
location = {
|
|
@@ -593,7 +611,7 @@ class RackGroupTest(APIViewTestCases.APIViewTestCase, APIViewTestCases.TreeModel
|
|
|
593
611
|
|
|
594
612
|
def test_child_group_location_valid(self):
|
|
595
613
|
"""A child group with a location may fall within the parent group's location."""
|
|
596
|
-
self.add_permissions("dcim.add_rackgroup")
|
|
614
|
+
self.add_permissions("dcim.add_rackgroup", "dcim.view_rackgroup", "dcim.view_location")
|
|
597
615
|
url = reverse("dcim-api:rackgroup-list")
|
|
598
616
|
|
|
599
617
|
parent_group = RackGroup.objects.filter(location=self.locations[0]).first()
|
|
@@ -615,7 +633,7 @@ class RackGroupTest(APIViewTestCases.APIViewTestCase, APIViewTestCases.TreeModel
|
|
|
615
633
|
|
|
616
634
|
def test_child_group_location_invalid(self):
|
|
617
635
|
"""A child group with a location must not fall outside its parent group's location."""
|
|
618
|
-
self.add_permissions("dcim.add_rackgroup")
|
|
636
|
+
self.add_permissions("dcim.add_rackgroup", "dcim.view_location", "dcim.view_rackgroup")
|
|
619
637
|
url = reverse("dcim-api:rackgroup-list")
|
|
620
638
|
|
|
621
639
|
parent_group = RackGroup.objects.filter(location=self.locations[0]).first()
|
|
@@ -1225,7 +1243,9 @@ class FrontPortTemplateTest(Mixins.BasePortTemplateTestMixin):
|
|
|
1225
1243
|
def test_module_type_device_type_validation(self):
|
|
1226
1244
|
"""Assert that a modular component template can have a module_type or a device_type but not both."""
|
|
1227
1245
|
|
|
1228
|
-
self.add_permissions(
|
|
1246
|
+
self.add_permissions(
|
|
1247
|
+
"dcim.add_frontporttemplate", "dcim.view_rearporttemplate", "dcim.view_devicetype", "dcim.view_moduletype"
|
|
1248
|
+
)
|
|
1229
1249
|
data = {
|
|
1230
1250
|
"module_type": self.module_type.pk,
|
|
1231
1251
|
"device_type": self.device_type.pk,
|
|
@@ -1253,7 +1273,9 @@ class FrontPortTemplateTest(Mixins.BasePortTemplateTestMixin):
|
|
|
1253
1273
|
def test_module_type_device_type_name_unique_validation(self):
|
|
1254
1274
|
"""Assert uniqueness constraint is enforced for (device_type,name) and (module_type,name) fields."""
|
|
1255
1275
|
|
|
1256
|
-
self.add_permissions(
|
|
1276
|
+
self.add_permissions(
|
|
1277
|
+
"dcim.add_frontporttemplate", "dcim.view_rearporttemplate", "dcim.view_moduletype", "dcim.view_devicetype"
|
|
1278
|
+
)
|
|
1257
1279
|
data = {
|
|
1258
1280
|
"module_type": self.module_type.pk,
|
|
1259
1281
|
"name": "test modular device_type component parent validation",
|
|
@@ -1617,7 +1639,7 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
|
|
|
1617
1639
|
schema = ConfigContextSchema.objects.create(
|
|
1618
1640
|
name="Schema 1", data_schema={"type": "object", "properties": {"A": {"type": "integer"}}}
|
|
1619
1641
|
)
|
|
1620
|
-
self.add_permissions("dcim.change_device")
|
|
1642
|
+
self.add_permissions("dcim.change_device", "extras.view_configcontextschema")
|
|
1621
1643
|
|
|
1622
1644
|
patch_data = {"local_config_context_schema": str(schema.pk)}
|
|
1623
1645
|
|
|
@@ -1651,7 +1673,7 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
|
|
|
1651
1673
|
Validate we can set primary_ip4 on a device using a PATCH.
|
|
1652
1674
|
"""
|
|
1653
1675
|
# Add object-level permission
|
|
1654
|
-
self.add_permissions("dcim.change_device")
|
|
1676
|
+
self.add_permissions("dcim.change_device", "ipam.view_ipaddress")
|
|
1655
1677
|
|
|
1656
1678
|
dev = Device.objects.get(name="Device 3")
|
|
1657
1679
|
intf_status = Status.objects.get_for_model(Interface).first()
|
|
@@ -1677,7 +1699,7 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
|
|
|
1677
1699
|
Validate we can set device redundancy group on a device using a PATCH.
|
|
1678
1700
|
"""
|
|
1679
1701
|
# Add object-level permission
|
|
1680
|
-
self.add_permissions("dcim.change_device")
|
|
1702
|
+
self.add_permissions("dcim.change_device", "dcim.view_deviceredundancygroup")
|
|
1681
1703
|
|
|
1682
1704
|
device_redundancy_group = DeviceRedundancyGroup.objects.first()
|
|
1683
1705
|
|
|
@@ -1751,7 +1773,15 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
|
|
|
1751
1773
|
# Create test data
|
|
1752
1774
|
parent_device, device_bay_1, device_bay_2, device_type_child = self._parent_device_test_data()
|
|
1753
1775
|
|
|
1754
|
-
self.add_permissions(
|
|
1776
|
+
self.add_permissions(
|
|
1777
|
+
"dcim.add_device",
|
|
1778
|
+
"dcim.view_device",
|
|
1779
|
+
"dcim.view_devicetype",
|
|
1780
|
+
"extras.view_role",
|
|
1781
|
+
"extras.view_status",
|
|
1782
|
+
"dcim.view_location",
|
|
1783
|
+
"dcim.view_devicebay",
|
|
1784
|
+
)
|
|
1755
1785
|
url = reverse("dcim-api:device-list")
|
|
1756
1786
|
|
|
1757
1787
|
# Test creating device with parent bay by device bay data
|
|
@@ -1808,7 +1838,7 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
|
|
|
1808
1838
|
# Create test data
|
|
1809
1839
|
parent_device, device_bay_1, device_bay_2, device_type_child = self._parent_device_test_data()
|
|
1810
1840
|
|
|
1811
|
-
self.add_permissions("dcim.change_device")
|
|
1841
|
+
self.add_permissions("dcim.change_device", "dcim.view_devicebay")
|
|
1812
1842
|
|
|
1813
1843
|
child_device = Device.objects.create(
|
|
1814
1844
|
device_type=device_type_child,
|
|
@@ -1852,7 +1882,7 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
|
|
|
1852
1882
|
device_bay_1.installed_device = child_device
|
|
1853
1883
|
device_bay_1.save()
|
|
1854
1884
|
|
|
1855
|
-
self.add_permissions("dcim.change_device", "dcim.view_device", "dcim.change_devicebay")
|
|
1885
|
+
self.add_permissions("dcim.change_device", "dcim.view_device", "dcim.change_devicebay", "dcim.view_devicebay")
|
|
1856
1886
|
child_device_detail_url = self._get_detail_url(child_device)
|
|
1857
1887
|
|
|
1858
1888
|
response = self.client.get(child_device_detail_url, **self.header)
|
|
@@ -1950,7 +1980,9 @@ class ModuleTestCase(APIViewTestCases.APIViewTestCase):
|
|
|
1950
1980
|
def test_parent_module_bay_location_validation(self):
|
|
1951
1981
|
"""Assert that a module can have a parent_module_bay or a location but not both."""
|
|
1952
1982
|
|
|
1953
|
-
self.add_permissions(
|
|
1983
|
+
self.add_permissions(
|
|
1984
|
+
"dcim.add_module", "dcim.view_moduletype", "dcim.view_location", "dcim.view_modulebay", "extras.view_status"
|
|
1985
|
+
)
|
|
1954
1986
|
data = {
|
|
1955
1987
|
"module_type": self.module_type.pk,
|
|
1956
1988
|
"location": self.location.pk,
|
|
@@ -1981,7 +2013,7 @@ class ModuleTestCase(APIViewTestCases.APIViewTestCase):
|
|
|
1981
2013
|
)
|
|
1982
2014
|
|
|
1983
2015
|
def test_serial_module_type_unique_validation(self):
|
|
1984
|
-
self.add_permissions("dcim.add_module")
|
|
2016
|
+
self.add_permissions("dcim.add_module", "dcim.view_location", "dcim.view_moduletype", "extras.view_status")
|
|
1985
2017
|
data = {
|
|
1986
2018
|
"module_type": self.module_type.pk,
|
|
1987
2019
|
"location": self.location.pk,
|
|
@@ -2006,7 +2038,7 @@ class ModuleTestCase(APIViewTestCases.APIViewTestCase):
|
|
|
2006
2038
|
)
|
|
2007
2039
|
|
|
2008
2040
|
def test_asset_tag_unique_validation(self):
|
|
2009
|
-
self.add_permissions("dcim.add_module")
|
|
2041
|
+
self.add_permissions("dcim.add_module", "dcim.view_location", "dcim.view_moduletype", "extras.view_status")
|
|
2010
2042
|
data = {
|
|
2011
2043
|
"module_type": self.module_type.pk,
|
|
2012
2044
|
"location": self.location.pk,
|
|
@@ -2321,7 +2353,7 @@ class InterfaceTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin
|
|
|
2321
2353
|
|
|
2322
2354
|
def test_untagged_vlan_requires_mode(self):
|
|
2323
2355
|
"""Test that when an `untagged_vlan` is specified, `mode` is also required."""
|
|
2324
|
-
self.add_permissions("dcim.add_interface")
|
|
2356
|
+
self.add_permissions("dcim.add_interface", "dcim.view_device", "extras.view_status", "ipam.view_vlan")
|
|
2325
2357
|
|
|
2326
2358
|
# This will fail.
|
|
2327
2359
|
url = self._get_list_url()
|
|
@@ -2336,7 +2368,9 @@ class InterfaceTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin
|
|
|
2336
2368
|
)
|
|
2337
2369
|
|
|
2338
2370
|
def test_tagged_vlan_must_be_in_the_location_or_parent_locations_of_the_parent_device(self):
|
|
2339
|
-
self.add_permissions(
|
|
2371
|
+
self.add_permissions(
|
|
2372
|
+
"dcim.add_interface", "dcim.view_interface", "dcim.view_device", "extras.view_status", "ipam.view_vlan"
|
|
2373
|
+
)
|
|
2340
2374
|
|
|
2341
2375
|
interface_status = Status.objects.get_for_model(Interface).first()
|
|
2342
2376
|
location = self.devices[0].location
|
|
@@ -2365,7 +2399,7 @@ class InterfaceTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin
|
|
|
2365
2399
|
|
|
2366
2400
|
def test_interface_belonging_to_common_device_or_vc_allowed(self):
|
|
2367
2401
|
"""Test parent, bridge, and LAG interfaces belonging to common device or VC is valid"""
|
|
2368
|
-
self.add_permissions("dcim.add_interface")
|
|
2402
|
+
self.add_permissions("dcim.add_interface", "dcim.view_device", "dcim.view_interface", "extras.view_status")
|
|
2369
2403
|
|
|
2370
2404
|
response = self.client.post(
|
|
2371
2405
|
self._get_list_url(), data=self.common_device_or_vc_data[0], format="json", **self.header
|
|
@@ -2388,7 +2422,9 @@ class InterfaceTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin
|
|
|
2388
2422
|
def test_interface_not_belonging_to_common_device_or_vc_not_allowed(self):
|
|
2389
2423
|
"""Test parent, bridge, and LAG interfaces not belonging to common device or VC is invalid"""
|
|
2390
2424
|
|
|
2391
|
-
self.add_permissions(
|
|
2425
|
+
self.add_permissions(
|
|
2426
|
+
"dcim.add_interface", "dcim.view_device", "dcim.view_interface", "extras.view_status", "extras.view_role"
|
|
2427
|
+
)
|
|
2392
2428
|
|
|
2393
2429
|
for name, payload in self.interfaces_not_belonging_to_same_device_data:
|
|
2394
2430
|
response = self.client.post(self._get_list_url(), data=payload, format="json", **self.header)
|
|
@@ -2405,7 +2441,9 @@ class InterfaceTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin
|
|
|
2405
2441
|
)
|
|
2406
2442
|
|
|
2407
2443
|
def test_tagged_vlan_raise_error_if_mode_not_set_to_tagged(self):
|
|
2408
|
-
self.add_permissions(
|
|
2444
|
+
self.add_permissions(
|
|
2445
|
+
"dcim.add_interface", "dcim.change_interface", "dcim.view_device", "extras.view_status", "ipam.view_vlan"
|
|
2446
|
+
)
|
|
2409
2447
|
with self.subTest("On create, assert 400 status."):
|
|
2410
2448
|
payload = {
|
|
2411
2449
|
"device": self.devices[0].pk,
|
|
@@ -2509,7 +2547,7 @@ class FrontPortTest(Mixins.BasePortTestMixin):
|
|
|
2509
2547
|
def test_module_device_validation(self):
|
|
2510
2548
|
"""Assert that a modular component can have a module or a device but not both."""
|
|
2511
2549
|
|
|
2512
|
-
self.add_permissions("dcim.add_frontport")
|
|
2550
|
+
self.add_permissions("dcim.add_frontport", "dcim.view_device", "dcim.view_module", "dcim.view_rearport")
|
|
2513
2551
|
data = {
|
|
2514
2552
|
"module": self.module.pk,
|
|
2515
2553
|
"device": self.device.pk,
|
|
@@ -2546,7 +2584,7 @@ class FrontPortTest(Mixins.BasePortTestMixin):
|
|
|
2546
2584
|
def test_module_device_name_unique_validation(self):
|
|
2547
2585
|
"""Assert uniqueness constraint is enforced for (device,name) and (module,name) fields."""
|
|
2548
2586
|
|
|
2549
|
-
self.add_permissions("dcim.add_frontport")
|
|
2587
|
+
self.add_permissions("dcim.add_frontport", "dcim.view_module", "dcim.view_rearport", "dcim.view_device")
|
|
2550
2588
|
data = {
|
|
2551
2589
|
"module": self.module.pk,
|
|
2552
2590
|
"name": "test modular device component parent validation",
|
|
@@ -3410,22 +3448,26 @@ class SoftwareImageFileTestCase(Mixins.SoftwareImageFileRelatedModelMixin, APIVi
|
|
|
3410
3448
|
def setUpTestData(cls):
|
|
3411
3449
|
statuses = Status.objects.get_for_model(SoftwareImageFile)
|
|
3412
3450
|
software_versions = SoftwareVersion.objects.all()
|
|
3451
|
+
external_integrations = ExternalIntegration.objects.all()
|
|
3413
3452
|
|
|
3414
3453
|
cls.create_data = [
|
|
3415
3454
|
{
|
|
3416
3455
|
"software_version": software_versions[0].pk,
|
|
3417
3456
|
"status": statuses[0].pk,
|
|
3418
3457
|
"image_file_name": "software_image_file_test_case_1.bin",
|
|
3458
|
+
"external_integration": external_integrations[0].pk,
|
|
3419
3459
|
},
|
|
3420
3460
|
{
|
|
3421
3461
|
"software_version": software_versions[1].pk,
|
|
3422
3462
|
"status": statuses[1].pk,
|
|
3423
3463
|
"image_file_name": "software_image_file_test_case_2.bin",
|
|
3464
|
+
"external_integration": external_integrations[1].pk,
|
|
3424
3465
|
},
|
|
3425
3466
|
{
|
|
3426
3467
|
"software_version": software_versions[2].pk,
|
|
3427
3468
|
"status": statuses[2].pk,
|
|
3428
3469
|
"image_file_name": "software_image_file_test_case_3.bin",
|
|
3470
|
+
"external_integration": None,
|
|
3429
3471
|
},
|
|
3430
3472
|
]
|
|
3431
3473
|
cls.bulk_update_data = {
|
|
@@ -3435,6 +3477,7 @@ class SoftwareImageFileTestCase(Mixins.SoftwareImageFileRelatedModelMixin, APIVi
|
|
|
3435
3477
|
"hashing_algorithm": SoftwareImageFileHashingAlgorithmChoices.SHA512,
|
|
3436
3478
|
"image_file_size": 1234567890,
|
|
3437
3479
|
"download_url": "https://example.com/software_image_file_test_case.bin",
|
|
3480
|
+
"external_integration": external_integrations[0].pk,
|
|
3438
3481
|
}
|
|
3439
3482
|
|
|
3440
3483
|
|
|
@@ -3642,7 +3685,7 @@ class VirtualDeviceContextTestCase(APIViewTestCases.APIViewTestCase):
|
|
|
3642
3685
|
Validate we can set primary_ip on a Virtual Device Context using a PATCH.
|
|
3643
3686
|
"""
|
|
3644
3687
|
# Add object-level permission
|
|
3645
|
-
self.add_permissions("dcim.change_virtualdevicecontext")
|
|
3688
|
+
self.add_permissions("dcim.change_virtualdevicecontext", "ipam.view_ipaddress")
|
|
3646
3689
|
vdc = VirtualDeviceContext.objects.first()
|
|
3647
3690
|
device = vdc.device
|
|
3648
3691
|
intf_status = Status.objects.get_for_model(Interface).first()
|
|
@@ -3679,7 +3722,7 @@ class VirtualDeviceContextTestCase(APIViewTestCases.APIViewTestCase):
|
|
|
3679
3722
|
"""
|
|
3680
3723
|
Validate that changing device on the virutal device context is not allowed.
|
|
3681
3724
|
"""
|
|
3682
|
-
self.add_permissions("dcim.change_virtualdevicecontext")
|
|
3725
|
+
self.add_permissions("dcim.change_virtualdevicecontext", "dcim.view_device")
|
|
3683
3726
|
vdc = VirtualDeviceContext.objects.first()
|
|
3684
3727
|
old_device = vdc.device
|
|
3685
3728
|
new_device = Device.objects.exclude(pk=old_device.pk).first()
|
|
@@ -3861,6 +3861,8 @@ class SoftwareImageFileFilterSetTestCase(FilterTestCases.FilterTestCase):
|
|
|
3861
3861
|
["software_version", "software_version__version"],
|
|
3862
3862
|
["status", "status__id"],
|
|
3863
3863
|
["status", "status__name"],
|
|
3864
|
+
["external_integration", "external_integration__id"],
|
|
3865
|
+
["external_integration", "external_integration__name"],
|
|
3864
3866
|
)
|
|
3865
3867
|
|
|
3866
3868
|
@classmethod
|
nautobot/dcim/tests/test_jobs.py
CHANGED
|
@@ -71,7 +71,7 @@ def create_common_data_for_software_related_test_cases():
|
|
|
71
71
|
class TestSoftwareImageFileTestCase(TransactionTestCase):
|
|
72
72
|
def test_correct_handling_for_model_protected_error(self):
|
|
73
73
|
create_common_data_for_software_related_test_cases()
|
|
74
|
-
software_image_file = SoftwareImageFile.objects.
|
|
74
|
+
software_image_file = SoftwareImageFile.objects.get(image_file_name="software_image_file_qs_test_1.bin")
|
|
75
75
|
|
|
76
76
|
self.add_permissions("dcim.delete_softwareimagefile")
|
|
77
77
|
pk_list = [str(software_image_file.pk)]
|
|
@@ -86,9 +86,7 @@ class TestSoftwareImageFileTestCase(TransactionTestCase):
|
|
|
86
86
|
pk_list=pk_list,
|
|
87
87
|
username=self.user.username,
|
|
88
88
|
)
|
|
89
|
-
|
|
90
|
-
print([(log.message, log.log_level) for log in logs])
|
|
91
|
-
self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
|
|
89
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
92
90
|
error_log = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
|
|
93
91
|
self.assertIn("Caught ProtectedError while attempting to delete objects", error_log.message)
|
|
94
92
|
self.assertEqual(initial_count, SoftwareImageFile.objects.all().count())
|
|
@@ -97,7 +95,7 @@ class TestSoftwareImageFileTestCase(TransactionTestCase):
|
|
|
97
95
|
class TestSoftwareVersionTestCase(TransactionTestCase):
|
|
98
96
|
def test_correct_handling_for_model_protected_error(self):
|
|
99
97
|
create_common_data_for_software_related_test_cases()
|
|
100
|
-
software_version = SoftwareVersion.objects.
|
|
98
|
+
software_version = SoftwareVersion.objects.get(version="Test version 1.0.0")
|
|
101
99
|
|
|
102
100
|
initial_count = SoftwareVersion.objects.all().count()
|
|
103
101
|
self.add_permissions("dcim.delete_softwareversion")
|
|
@@ -112,7 +110,7 @@ class TestSoftwareVersionTestCase(TransactionTestCase):
|
|
|
112
110
|
pk_list=pk_list,
|
|
113
111
|
username=self.user.username,
|
|
114
112
|
)
|
|
115
|
-
self.
|
|
113
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
116
114
|
error_log = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
|
|
117
115
|
self.assertIn("Caught ProtectedError while attempting to delete objects", error_log.message)
|
|
118
116
|
self.assertEqual(initial_count, SoftwareVersion.objects.all().count())
|
|
@@ -9,6 +9,7 @@ from django.test import TestCase
|
|
|
9
9
|
from django.test.utils import override_settings
|
|
10
10
|
|
|
11
11
|
from nautobot.circuits.models import Circuit, CircuitTermination, CircuitType, Provider, ProviderNetwork
|
|
12
|
+
from nautobot.core import settings
|
|
12
13
|
from nautobot.core.testing.models import ModelTestCases
|
|
13
14
|
from nautobot.dcim.choices import (
|
|
14
15
|
CableStatusChoices,
|
|
@@ -2610,6 +2611,70 @@ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
|
|
|
2610
2611
|
class SoftwareImageFileTestCase(ModelTestCases.BaseModelTestCase):
|
|
2611
2612
|
model = SoftwareImageFile
|
|
2612
2613
|
|
|
2614
|
+
def test_download_url_validation_behaviour(self):
|
|
2615
|
+
"""
|
|
2616
|
+
Test that the `download_url` property behaves as expected in relation to laxURLField validation and
|
|
2617
|
+
the ALLOWED_URL_SCHEMES setting.
|
|
2618
|
+
"""
|
|
2619
|
+
# Prepare prerequisite objects
|
|
2620
|
+
platform = Platform.objects.first()
|
|
2621
|
+
software_version_status = Status.objects.get_for_model(SoftwareVersion).first()
|
|
2622
|
+
software_image_file_status = Status.objects.get_for_model(SoftwareImageFile).first()
|
|
2623
|
+
software_version = SoftwareVersion.objects.create(
|
|
2624
|
+
platform=platform, version="Test version 1.0.0", status=software_version_status
|
|
2625
|
+
)
|
|
2626
|
+
|
|
2627
|
+
# Test that the download_url field is correctly validated with the default ALLOWED_URL_SCHEMES setting
|
|
2628
|
+
for scheme in settings.ALLOWED_URL_SCHEMES:
|
|
2629
|
+
software_image = SoftwareImageFile(
|
|
2630
|
+
software_version=software_version,
|
|
2631
|
+
image_file_name=f"software_image_file_qs_test_{scheme}.bin",
|
|
2632
|
+
status=software_image_file_status,
|
|
2633
|
+
download_url=f"{scheme}://example.com/software_image_file_qs_test_1.bin",
|
|
2634
|
+
)
|
|
2635
|
+
try:
|
|
2636
|
+
software_image.validated_save()
|
|
2637
|
+
except ValidationError as e:
|
|
2638
|
+
self.fail(f"download_url Scheme {scheme} ValidationError: {e}")
|
|
2639
|
+
|
|
2640
|
+
INVALID_SCHEMES = ["httpx", "rdp", "cryptoboi"]
|
|
2641
|
+
OVERRIDE_VALID_SCHEMES = ["sftp", "tftp", "https", "http", "newfs", "customfs"]
|
|
2642
|
+
# Invalid schemes should raise a ValidationError
|
|
2643
|
+
for scheme in INVALID_SCHEMES:
|
|
2644
|
+
software_image = SoftwareImageFile(
|
|
2645
|
+
software_version=software_version,
|
|
2646
|
+
image_file_name=f"software_image_file_qs_test_{scheme}2.bin",
|
|
2647
|
+
status=software_image_file_status,
|
|
2648
|
+
download_url=f"{scheme}://example.com/software_image_file_qs_test_2.bin",
|
|
2649
|
+
)
|
|
2650
|
+
with self.assertRaises(ValidationError) as err:
|
|
2651
|
+
software_image.validated_save()
|
|
2652
|
+
self.assertEqual(err.exception.message_dict["download_url"][0], "Enter a valid URL.")
|
|
2653
|
+
|
|
2654
|
+
with override_settings(ALLOWED_URL_SCHEMES=OVERRIDE_VALID_SCHEMES):
|
|
2655
|
+
for scheme in OVERRIDE_VALID_SCHEMES:
|
|
2656
|
+
software_image = SoftwareImageFile(
|
|
2657
|
+
software_version=software_version,
|
|
2658
|
+
image_file_name=f"software_image_file_qs_test_{scheme}3.bin",
|
|
2659
|
+
status=software_image_file_status,
|
|
2660
|
+
download_url=f"{scheme}://example.com/software_image_file_qs_test_3.bin",
|
|
2661
|
+
)
|
|
2662
|
+
try:
|
|
2663
|
+
software_image.validated_save()
|
|
2664
|
+
except ValidationError as e:
|
|
2665
|
+
self.fail(f"download_url Scheme {scheme} ValidationError: {e}")
|
|
2666
|
+
|
|
2667
|
+
for scheme in INVALID_SCHEMES:
|
|
2668
|
+
software_image = SoftwareImageFile(
|
|
2669
|
+
software_version=software_version,
|
|
2670
|
+
image_file_name=f"software_image_file_qs_test_{scheme}4.bin",
|
|
2671
|
+
status=software_image_file_status,
|
|
2672
|
+
download_url=f"{scheme}://example.com/software_image_file_qs_test_4.bin",
|
|
2673
|
+
)
|
|
2674
|
+
with self.assertRaises(ValidationError) as err:
|
|
2675
|
+
software_image.validated_save()
|
|
2676
|
+
self.assertEqual(err.exception.message_dict["download_url"][0], "Enter a valid URL.")
|
|
2677
|
+
|
|
2613
2678
|
def test_queryset_get_for_object(self):
|
|
2614
2679
|
"""
|
|
2615
2680
|
Test that the queryset get_for_object method returns the expected results for Device, DeviceType, InventoryItem and VirtualMachine
|
|
@@ -4579,6 +4579,7 @@ class SoftwareImageFileTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
4579
4579
|
device_types = DeviceType.objects.all()[:2]
|
|
4580
4580
|
statuses = Status.objects.get_for_model(SoftwareImageFile)
|
|
4581
4581
|
software_versions = SoftwareVersion.objects.all()
|
|
4582
|
+
external_integration = ExternalIntegration.objects.first()
|
|
4582
4583
|
|
|
4583
4584
|
cls.form_data = {
|
|
4584
4585
|
"software_version": software_versions[0].pk,
|
|
@@ -4588,6 +4589,7 @@ class SoftwareImageFileTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
4588
4589
|
"image_file_size": 1234567890,
|
|
4589
4590
|
"hashing_algorithm": SoftwareImageFileHashingAlgorithmChoices.SHA512,
|
|
4590
4591
|
"download_url": "https://example.com/software_image_file_test_case.bin",
|
|
4592
|
+
"external_integration": external_integration.pk,
|
|
4591
4593
|
"device_types": [device_types[0].pk, device_types[1].pk],
|
|
4592
4594
|
}
|
|
4593
4595
|
|
|
@@ -4598,6 +4600,7 @@ class SoftwareImageFileTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
4598
4600
|
"hashing_algorithm": SoftwareImageFileHashingAlgorithmChoices.SHA512,
|
|
4599
4601
|
"image_file_size": 1234567890,
|
|
4600
4602
|
"download_url": "https://example.com/software_image_file_test_case.bin",
|
|
4603
|
+
"external_integration": external_integration.pk,
|
|
4601
4604
|
}
|
|
4602
4605
|
|
|
4603
4606
|
|
nautobot/extras/choices.py
CHANGED
|
@@ -251,8 +251,10 @@ class JobResultStatusChoices(ChoiceSet):
|
|
|
251
251
|
"""
|
|
252
252
|
|
|
253
253
|
STATUS_FAILURE = states.FAILURE
|
|
254
|
+
STATUS_IGNORED = states.IGNORED
|
|
254
255
|
STATUS_PENDING = states.PENDING
|
|
255
256
|
STATUS_RECEIVED = states.RECEIVED
|
|
257
|
+
STATUS_REJECTED = states.REJECTED
|
|
256
258
|
STATUS_RETRY = states.RETRY
|
|
257
259
|
STATUS_REVOKED = states.REVOKED
|
|
258
260
|
STATUS_STARTED = states.STARTED
|
|
@@ -302,27 +304,30 @@ class JobResultStatusChoices(ChoiceSet):
|
|
|
302
304
|
class LogLevelChoices(ChoiceSet):
|
|
303
305
|
LOG_DEBUG = "debug"
|
|
304
306
|
LOG_INFO = "info"
|
|
307
|
+
LOG_SUCCESS = "success"
|
|
305
308
|
LOG_WARNING = "warning"
|
|
309
|
+
LOG_FAILURE = "failure"
|
|
306
310
|
LOG_ERROR = "error"
|
|
307
311
|
LOG_CRITICAL = "critical"
|
|
308
|
-
LOG_SUCCESS = "success"
|
|
309
312
|
|
|
310
313
|
CHOICES = (
|
|
311
314
|
(LOG_DEBUG, "Debug"),
|
|
312
315
|
(LOG_INFO, "Info"),
|
|
316
|
+
(LOG_SUCCESS, "Success"),
|
|
313
317
|
(LOG_WARNING, "Warning"),
|
|
318
|
+
(LOG_FAILURE, "Failure"),
|
|
314
319
|
(LOG_ERROR, "Error"),
|
|
315
320
|
(LOG_CRITICAL, "Critical"),
|
|
316
|
-
(LOG_SUCCESS, "Success"),
|
|
317
321
|
)
|
|
318
322
|
|
|
319
323
|
CSS_CLASSES = {
|
|
320
324
|
LOG_DEBUG: "debug",
|
|
321
325
|
LOG_INFO: "info",
|
|
326
|
+
LOG_SUCCESS: "success",
|
|
322
327
|
LOG_WARNING: "warning",
|
|
328
|
+
LOG_FAILURE: "failure",
|
|
323
329
|
LOG_ERROR: "error",
|
|
324
330
|
LOG_CRITICAL: "critical",
|
|
325
|
-
LOG_SUCCESS: "success",
|
|
326
331
|
}
|
|
327
332
|
|
|
328
333
|
|
nautobot/extras/forms/forms.py
CHANGED
|
@@ -1891,8 +1891,11 @@ class RoleBulkEditForm(NautobotBulkEditForm):
|
|
|
1891
1891
|
color = forms.CharField(max_length=6, required=False, widget=ColorSelect())
|
|
1892
1892
|
description = forms.CharField(max_length=CHARFIELD_MAX_LENGTH, required=False)
|
|
1893
1893
|
weight = forms.IntegerField(required=False)
|
|
1894
|
-
|
|
1895
|
-
queryset=RoleModelsQuery().as_queryset(), required=False, label="Content Type(s)"
|
|
1894
|
+
add_content_types = MultipleContentTypeField(
|
|
1895
|
+
queryset=RoleModelsQuery().as_queryset(), required=False, label="Add Content Type(s)"
|
|
1896
|
+
)
|
|
1897
|
+
remove_content_types = MultipleContentTypeField(
|
|
1898
|
+
queryset=RoleModelsQuery().as_queryset(), required=False, label="Remove Content Type(s)"
|
|
1896
1899
|
)
|
|
1897
1900
|
|
|
1898
1901
|
class Meta:
|
|
@@ -2012,7 +2015,8 @@ class StatusBulkEditForm(NautobotBulkEditForm):
|
|
|
2012
2015
|
|
|
2013
2016
|
pk = forms.ModelMultipleChoiceField(queryset=Status.objects.all(), widget=forms.MultipleHiddenInput)
|
|
2014
2017
|
color = forms.CharField(max_length=6, required=False, widget=ColorSelect())
|
|
2015
|
-
|
|
2018
|
+
add_content_types = MultipleContentTypeField(feature="statuses", required=False, label="Add Content Type(s)")
|
|
2019
|
+
remove_content_types = MultipleContentTypeField(feature="statuses", required=False, label="Remove Content Type(s)")
|
|
2016
2020
|
|
|
2017
2021
|
class Meta:
|
|
2018
2022
|
nullable_fields = []
|