nautobot 2.4.2__py3-none-any.whl → 2.4.4__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/apps/filters.py +2 -0
- nautobot/circuits/filters.py +1 -1
- nautobot/circuits/templates/circuits/inc/circuit_termination.html +1 -1
- nautobot/circuits/tests/integration/test_circuit.py +135 -0
- nautobot/circuits/tests/test_models.py +5 -3
- nautobot/circuits/views.py +4 -1
- nautobot/cloud/api/views.py +3 -3
- nautobot/cloud/filters.py +3 -6
- nautobot/cloud/tests/test_filters.py +21 -0
- nautobot/core/admin.py +2 -0
- nautobot/core/constants.py +0 -1
- nautobot/core/forms/__init__.py +2 -0
- nautobot/core/forms/forms.py +2 -1
- nautobot/core/forms/widgets.py +8 -0
- nautobot/core/jobs/__init__.py +2 -1
- nautobot/core/management/commands/generate_performance_test_endpoints.py +271 -0
- nautobot/core/models/utils.py +6 -1
- nautobot/core/templates/generic/object_bulk_delete.html +1 -1
- nautobot/core/templates/generic/object_bulk_edit.html +1 -1
- nautobot/core/templates/generic/object_bulk_import.html +1 -1
- nautobot/core/templates/generic/object_create.html +5 -0
- nautobot/core/templates/generic/object_delete.html +1 -1
- nautobot/core/templates/generic/object_detail.html +1 -1
- nautobot/core/templates/generic/object_edit.html +1 -1
- nautobot/core/templates/inc/javascript.html +3 -0
- nautobot/core/templates/widgets/clearable_file.html +5 -0
- nautobot/core/templatetags/helpers.py +3 -3
- nautobot/core/templatetags/ui_framework.py +20 -4
- nautobot/core/testing/forms.py +1 -1
- nautobot/core/testing/integration.py +37 -7
- nautobot/core/tests/test_api.py +1 -1
- nautobot/core/tests/test_commands.py +31 -0
- nautobot/core/tests/test_graphql.py +3 -3
- nautobot/core/tests/test_jobs.py +4 -1
- nautobot/core/tests/test_utils.py +17 -2
- nautobot/core/ui/object_detail.py +1 -1
- nautobot/core/utils/lookup.py +12 -1
- nautobot/core/views/generic.py +9 -1
- nautobot/core/views/mixins.py +9 -1
- nautobot/dcim/api/serializers.py +36 -0
- nautobot/dcim/api/views.py +12 -11
- nautobot/dcim/elevations.py +17 -4
- nautobot/dcim/factory.py +9 -1
- nautobot/dcim/filters/__init__.py +27 -1
- nautobot/dcim/forms.py +16 -7
- nautobot/dcim/models/devices.py +12 -7
- nautobot/dcim/signals.py +26 -0
- nautobot/dcim/templates/dcim/cable_trace.html +4 -4
- nautobot/dcim/templates/dcim/consoleport.html +14 -4
- nautobot/dcim/templates/dcim/consoleserverport.html +14 -4
- nautobot/dcim/templates/dcim/device/lldp_neighbors.html +3 -3
- nautobot/dcim/templates/dcim/frontport.html +7 -2
- nautobot/dcim/templates/dcim/interface.html +9 -4
- nautobot/dcim/templates/dcim/powerfeed.html +8 -3
- nautobot/dcim/templates/dcim/poweroutlet.html +14 -4
- nautobot/dcim/templates/dcim/powerport.html +14 -4
- nautobot/dcim/templates/dcim/rearport.html +7 -2
- nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +0 -62
- nautobot/dcim/templates/dcim/virtualdevicecontext_update.html +6 -0
- nautobot/dcim/tests/integration/test_fileinputpicker.py +87 -0
- nautobot/dcim/tests/test_api.py +176 -0
- nautobot/dcim/tests/test_filters.py +56 -3
- nautobot/dcim/tests/test_models.py +41 -1
- nautobot/dcim/views.py +24 -14
- nautobot/extras/api/mixins.py +1 -1
- nautobot/extras/api/views.py +4 -4
- nautobot/extras/filters/__init__.py +4 -0
- nautobot/extras/forms/forms.py +4 -0
- nautobot/extras/jobs.py +8 -1
- nautobot/extras/models/datasources.py +7 -3
- nautobot/extras/plugins/__init__.py +26 -1
- nautobot/extras/templates/extras/inc/jobresult.html +12 -13
- nautobot/extras/templates/extras/job.html +1 -0
- nautobot/extras/templates/extras/objectchange.html +28 -12
- nautobot/extras/tests/test_api.py +16 -15
- nautobot/extras/tests/test_dynamicgroups.py +14 -0
- nautobot/extras/tests/test_filters.py +2 -0
- nautobot/extras/tests/test_plugins.py +32 -1
- nautobot/extras/tests/test_views.py +209 -11
- nautobot/extras/utils.py +30 -0
- nautobot/extras/views.py +32 -14
- nautobot/ipam/api/serializers.py +7 -8
- nautobot/ipam/api/views.py +5 -5
- nautobot/ipam/factory.py +27 -8
- nautobot/ipam/filters.py +67 -29
- nautobot/ipam/formfields.py +51 -0
- nautobot/ipam/forms.py +15 -7
- nautobot/ipam/migrations/0051_added_optional_vrf_relationship_to_vdc.py +41 -0
- nautobot/ipam/models.py +63 -5
- nautobot/ipam/tables.py +21 -7
- nautobot/ipam/tests/test_api.py +107 -66
- nautobot/ipam/tests/test_filters.py +145 -5
- nautobot/ipam/tests/test_views.py +15 -2
- nautobot/project-static/bootstrap-filestyle-1.2.3/bootstrap-filestyle.min.js +11 -0
- nautobot/project-static/css/base.css +11 -0
- nautobot/project-static/css/dark.css +2 -1
- nautobot/project-static/docs/apps/index.html +1 -1
- nautobot/project-static/docs/apps/nautobot-apps.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +62 -0
- nautobot/project-static/docs/development/apps/api/configuration-view.html +0 -3
- nautobot/project-static/docs/development/apps/api/models/graphql.html +9 -13
- nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +94 -1
- nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +2 -5
- nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +0 -3
- nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +0 -3
- nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +0 -3
- nautobot/project-static/docs/development/apps/api/prometheus.html +0 -3
- nautobot/project-static/docs/development/apps/api/setup.html +1 -1
- nautobot/project-static/docs/development/apps/api/testing.html +0 -6
- nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +0 -3
- nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +0 -3
- nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +0 -3
- nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +0 -3
- nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +1 -7
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +0 -7
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +0 -4
- nautobot/project-static/docs/development/apps/api/views/notes.html +0 -3
- nautobot/project-static/docs/development/apps/index.html +2 -35
- nautobot/project-static/docs/development/apps/migration/code-updates.html +7 -6
- nautobot/project-static/docs/development/apps/migration/dependency-updates.html +2 -2
- nautobot/project-static/docs/development/apps/migration/from-v1.html +3 -3
- nautobot/project-static/docs/development/core/application-registry.html +0 -6
- nautobot/project-static/docs/development/core/best-practices.html +1 -28
- nautobot/project-static/docs/development/core/bootstrap-ui.html +1 -1
- nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +65 -11
- nautobot/project-static/docs/development/core/getting-started.html +14 -18
- nautobot/project-static/docs/development/core/homepage.html +0 -3
- nautobot/project-static/docs/development/core/index.html +1 -1
- nautobot/project-static/docs/development/core/minikube-dev-environment-for-k8s-jobs.html +3 -3
- nautobot/project-static/docs/development/core/model-checklist.html +1 -1
- nautobot/project-static/docs/development/core/navigation-menu.html +1 -1
- nautobot/project-static/docs/development/core/release-checklist.html +1 -1
- nautobot/project-static/docs/development/core/settings.html +1 -1
- nautobot/project-static/docs/development/core/style-guide.html +4 -9
- nautobot/project-static/docs/development/core/templates.html +0 -3
- nautobot/project-static/docs/development/core/testing.html +0 -9
- nautobot/project-static/docs/development/jobs/index.html +11 -30
- nautobot/project-static/docs/development/jobs/migration/from-v1.html +3 -2
- nautobot/project-static/docs/index.html +3 -2
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/overview/application_stack.html +2 -20
- nautobot/project-static/docs/release-notes/version-1.0.html +2 -2
- nautobot/project-static/docs/release-notes/version-1.1.html +2 -2
- nautobot/project-static/docs/release-notes/version-1.2.html +3 -3
- nautobot/project-static/docs/release-notes/version-1.3.html +1 -1
- nautobot/project-static/docs/release-notes/version-1.4.html +17 -17
- nautobot/project-static/docs/release-notes/version-1.5.html +8 -8
- nautobot/project-static/docs/release-notes/version-1.6.html +4 -4
- nautobot/project-static/docs/release-notes/version-2.0.html +10 -10
- nautobot/project-static/docs/release-notes/version-2.1.html +7 -7
- nautobot/project-static/docs/release-notes/version-2.2.html +1 -1
- nautobot/project-static/docs/release-notes/version-2.3.html +4 -4
- nautobot/project-static/docs/release-notes/version-2.4.html +379 -0
- 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/authentication/ldap.html +3 -3
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +4 -4
- nautobot/project-static/docs/user-guide/administration/configuration/redis.html +1 -1
- nautobot/project-static/docs/user-guide/administration/configuration/settings.html +3 -13
- nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +5 -5
- nautobot/project-static/docs/user-guide/administration/guides/docker.html +3 -18
- nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +1 -1
- nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +4 -4
- nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +15 -15
- nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +2 -2
- nautobot/project-static/docs/user-guide/administration/installation/app-install.html +1 -1
- nautobot/project-static/docs/user-guide/administration/installation/index.html +0 -16
- nautobot/project-static/docs/user-guide/administration/installation/install_system.html +1 -1
- nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +7 -10
- nautobot/project-static/docs/user-guide/administration/installation/services.html +1 -12
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +3 -3
- nautobot/project-static/docs/user-guide/administration/security/index.html +1 -1
- nautobot/project-static/docs/user-guide/administration/security/notices.html +1 -0
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +5 -35
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/tables/v2-code-location-changes.yaml +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +12 -9
- nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +0 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +0 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +1 -17
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +0 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +0 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +1 -7
- nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +0 -6
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +0 -3
- nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +0 -8
- nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +15 -15
- nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/graphql.html +0 -6
- nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +3 -15
- nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +1 -27
- nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +0 -8
- nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +3 -6
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +0 -8
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +0 -7
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +6 -6
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +0 -14
- nautobot/project-static/docs/user-guide/platform-functionality/note.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +1 -10
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +0 -14
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +0 -19
- nautobot/project-static/docs/user-guide/platform-functionality/secret.html +3 -9
- nautobot/project-static/docs/user-guide/platform-functionality/status.html +0 -8
- nautobot/project-static/docs/user-guide/platform-functionality/tag.html +0 -4
- nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +1 -13
- nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +0 -5
- nautobot/project-static/js/dropdown.js +28 -0
- nautobot/project-static/js/editor.js +292 -0
- nautobot/project-static/monaco-editor-0.52.2/README.md +81 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/base/worker/workerMain.js +31 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/basic-languages/xml/xml.js +10 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/basic-languages/yaml/yaml.js +10 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/editor/editor.main.css +8 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/editor/editor.main.js +798 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/language/json/jsonMode.js +19 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/language/json/jsonWorker.js +42 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/loader.js +11 -0
- nautobot/tenancy/filters/__init__.py +3 -5
- nautobot/tenancy/forms.py +9 -0
- nautobot/tenancy/templates/tenancy/tenant_create.html +21 -0
- nautobot/tenancy/templates/tenancy/tenant_edit.html +2 -21
- nautobot/tenancy/templates/tenancy/tenantgroup.html +2 -44
- nautobot/tenancy/templates/tenancy/tenantgroup_retrieve.html +1 -0
- nautobot/tenancy/tests/test_filters.py +10 -0
- nautobot/tenancy/tests/test_views.py +5 -1
- nautobot/tenancy/urls.py +7 -79
- nautobot/tenancy/views.py +51 -80
- nautobot/virtualization/views.py +0 -1
- nautobot/wireless/api/serializers.py +6 -1
- nautobot/wireless/api/views.py +3 -3
- nautobot/wireless/tables.py +9 -4
- nautobot/wireless/tests/test_api.py +5 -9
- {nautobot-2.4.2.dist-info → nautobot-2.4.4.dist-info}/METADATA +9 -9
- {nautobot-2.4.2.dist-info → nautobot-2.4.4.dist-info}/RECORD +267 -246
- {nautobot-2.4.2.dist-info → nautobot-2.4.4.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.2.dist-info → nautobot-2.4.4.dist-info}/NOTICE +0 -0
- {nautobot-2.4.2.dist-info → nautobot-2.4.4.dist-info}/WHEEL +0 -0
- {nautobot-2.4.2.dist-info → nautobot-2.4.4.dist-info}/entry_points.txt +0 -0
|
@@ -820,17 +820,33 @@ class DynamicGroupTestCase(
|
|
|
820
820
|
return super()._get_queryset().filter(group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER) # TODO
|
|
821
821
|
|
|
822
822
|
def test_get_object_with_permission(self):
|
|
823
|
-
|
|
823
|
+
location_ct = ContentType.objects.get_for_model(Location)
|
|
824
|
+
instance = self._get_queryset().exclude(content_type=location_ct).first()
|
|
824
825
|
# Add view permissions for the group's members:
|
|
825
|
-
self.add_permissions(
|
|
826
|
+
self.add_permissions(
|
|
827
|
+
get_permission_for_model(instance.content_type.model_class(), "view"), "extras.view_dynamicgroup"
|
|
828
|
+
)
|
|
826
829
|
|
|
827
|
-
response =
|
|
830
|
+
response = self.client.get(instance.get_absolute_url())
|
|
831
|
+
self.assertHttpStatus(response, 200)
|
|
828
832
|
|
|
829
833
|
response_body = extract_page_body(response.content.decode(response.charset))
|
|
830
834
|
# Check that the "members" table in the detail view includes all appropriate member objects
|
|
831
835
|
for member in instance.members:
|
|
832
836
|
self.assertIn(str(member.pk), response_body)
|
|
833
837
|
|
|
838
|
+
# Test accessing DynamicGroup detail view with a different content type, more specifically, TreeModel
|
|
839
|
+
# https://github.com/nautobot/nautobot/issues/6806
|
|
840
|
+
tree_model_dg = DynamicGroup.objects.create(name="DG 4", content_type=location_ct)
|
|
841
|
+
# Add view permissions for the group's members:
|
|
842
|
+
self.add_permissions(get_permission_for_model(tree_model_dg.content_type.model_class(), "view"))
|
|
843
|
+
response = self.client.get(tree_model_dg.get_absolute_url())
|
|
844
|
+
self.assertHttpStatus(response, 200)
|
|
845
|
+
response_body = extract_page_body(response.content.decode(response.charset))
|
|
846
|
+
# Check that the "members" table in the detail view includes all appropriate member objects
|
|
847
|
+
for member in tree_model_dg.members:
|
|
848
|
+
self.assertIn(str(member.pk), response_body)
|
|
849
|
+
|
|
834
850
|
def test_get_object_with_constrained_permission(self):
|
|
835
851
|
instance = self._get_queryset().first()
|
|
836
852
|
# Add view permission for one of the group's members but not the others:
|
|
@@ -1164,6 +1180,13 @@ class GitRepositoryTestCase(
|
|
|
1164
1180
|
self.form_data = form_data
|
|
1165
1181
|
super().test_edit_object_with_constrained_permission()
|
|
1166
1182
|
|
|
1183
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1184
|
+
def test_view_when_no_sync_job_result_exists(self):
|
|
1185
|
+
instance = self._get_queryset().first()
|
|
1186
|
+
response = self.client.get(reverse("extras:gitrepository_result", kwargs={"pk": instance.pk}))
|
|
1187
|
+
self.assertEqual(response.status_code, 200)
|
|
1188
|
+
self.assertEqual(response.context["result"], {})
|
|
1189
|
+
|
|
1167
1190
|
def test_post_sync_repo_anonymous(self):
|
|
1168
1191
|
self.client.logout()
|
|
1169
1192
|
url = reverse("extras:gitrepository_sync", kwargs={"pk": self._get_queryset().first().pk})
|
|
@@ -1306,19 +1329,18 @@ class SavedViewTest(ModelViewTestCase):
|
|
|
1306
1329
|
|
|
1307
1330
|
model = SavedView
|
|
1308
1331
|
|
|
1309
|
-
def get_view_url_for_saved_view(self, saved_view, action="detail"):
|
|
1332
|
+
def get_view_url_for_saved_view(self, saved_view=None, action="detail"):
|
|
1310
1333
|
"""
|
|
1311
1334
|
Since saved view detail url redirects, we need to manually construct its detail url
|
|
1312
1335
|
to test the content of its response.
|
|
1313
1336
|
"""
|
|
1314
|
-
|
|
1315
|
-
pk = saved_view.pk
|
|
1337
|
+
url = ""
|
|
1316
1338
|
|
|
1317
|
-
if action == "detail":
|
|
1318
|
-
url = reverse(view) + f"?saved_view={pk}"
|
|
1319
|
-
elif action == "edit":
|
|
1339
|
+
if action == "detail" and saved_view:
|
|
1340
|
+
url = reverse(saved_view.view) + f"?saved_view={saved_view.pk}"
|
|
1341
|
+
elif action == "edit" and saved_view:
|
|
1320
1342
|
url = saved_view.get_absolute_url() + "update-config/"
|
|
1321
|
-
|
|
1343
|
+
elif action == "create":
|
|
1322
1344
|
url = reverse("extras:savedview_add")
|
|
1323
1345
|
|
|
1324
1346
|
return url
|
|
@@ -1411,7 +1433,14 @@ class SavedViewTest(ModelViewTestCase):
|
|
|
1411
1433
|
|
|
1412
1434
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1413
1435
|
def test_update_saved_view_as_owner(self):
|
|
1414
|
-
|
|
1436
|
+
view_name = "dcim:location_list"
|
|
1437
|
+
instance = SavedView.objects.create(
|
|
1438
|
+
name="Location Saved View",
|
|
1439
|
+
owner=self.user,
|
|
1440
|
+
view=view_name,
|
|
1441
|
+
is_global_default=True,
|
|
1442
|
+
)
|
|
1443
|
+
|
|
1415
1444
|
update_query_strings = ["per_page=12", "&status=active", "&name=new_name_filter", "&sort=name"]
|
|
1416
1445
|
update_url = self.get_view_url_for_saved_view(instance, "edit") + "?" + "".join(update_query_strings)
|
|
1417
1446
|
# Try update the saved view with the same user as the owner of the saved view
|
|
@@ -1543,6 +1572,62 @@ class SavedViewTest(ModelViewTestCase):
|
|
|
1543
1572
|
# Assert that Location List View got redirected to Saved View set as user default
|
|
1544
1573
|
self.assertBodyContains(response, "<strong>User Location Default View</strong>", html=True)
|
|
1545
1574
|
|
|
1575
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1576
|
+
def test_filtered_view_precedes_global_default(self):
|
|
1577
|
+
view_name = "dcim:location_list"
|
|
1578
|
+
# Global saved view that will show Floor type locations only.
|
|
1579
|
+
SavedView.objects.create(
|
|
1580
|
+
name="Global Location Default View",
|
|
1581
|
+
owner=self.user,
|
|
1582
|
+
view=view_name,
|
|
1583
|
+
is_global_default=True,
|
|
1584
|
+
config={
|
|
1585
|
+
"filter_params": {
|
|
1586
|
+
"location_type": ["Floor"],
|
|
1587
|
+
}
|
|
1588
|
+
},
|
|
1589
|
+
)
|
|
1590
|
+
response = self.client.get(reverse(view_name) + "?location_type=Campus", follow=True)
|
|
1591
|
+
# Assert that the user is not redirected to the global default view
|
|
1592
|
+
# But instead redirected to the filtered view
|
|
1593
|
+
self.assertNotIn(
|
|
1594
|
+
"<strong>Global Location Default View</strong>",
|
|
1595
|
+
extract_page_body(response.content.decode(response.charset)),
|
|
1596
|
+
)
|
|
1597
|
+
|
|
1598
|
+
# Floor type locations (Floor-<number>) should not be visible in the response
|
|
1599
|
+
self.assertNotIn(
|
|
1600
|
+
"Floor-",
|
|
1601
|
+
extract_page_body(response.content.decode(response.charset)),
|
|
1602
|
+
)
|
|
1603
|
+
|
|
1604
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1605
|
+
def test_filtered_view_precedes_user_default(self):
|
|
1606
|
+
view_name = "dcim:location_list"
|
|
1607
|
+
# User saved view that will show Floor type locations only.
|
|
1608
|
+
sv = SavedView.objects.create(
|
|
1609
|
+
name="User Location Default View",
|
|
1610
|
+
owner=self.user,
|
|
1611
|
+
view=view_name,
|
|
1612
|
+
config={
|
|
1613
|
+
"filter_params": {
|
|
1614
|
+
"location_type": ["Floor"],
|
|
1615
|
+
}
|
|
1616
|
+
},
|
|
1617
|
+
)
|
|
1618
|
+
UserSavedViewAssociation.objects.create(user=self.user, saved_view=sv, view_name=sv.view)
|
|
1619
|
+
response = self.client.get(reverse(view_name) + "?location_type=Campus", follow=True)
|
|
1620
|
+
# Assert that the user is not redirected to the user default view
|
|
1621
|
+
# But instead redirected to the filtered view
|
|
1622
|
+
self.assertNotIn(
|
|
1623
|
+
"<strong>User Location Default View</strong>", extract_page_body(response.content.decode(response.charset))
|
|
1624
|
+
)
|
|
1625
|
+
# Floor type locations (Floor-<number>) should not be visible in the response
|
|
1626
|
+
self.assertNotIn(
|
|
1627
|
+
"Floor-",
|
|
1628
|
+
extract_page_body(response.content.decode(response.charset)),
|
|
1629
|
+
)
|
|
1630
|
+
|
|
1546
1631
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
1547
1632
|
def test_is_shared(self):
|
|
1548
1633
|
view_name = "dcim:location_list"
|
|
@@ -1568,6 +1653,119 @@ class SavedViewTest(ModelViewTestCase):
|
|
|
1568
1653
|
self.assertIn(str(sv_shared.pk), response_body, msg=response_body)
|
|
1569
1654
|
self.assertNotIn(str(sv_not_shared.pk), response_body, msg=response_body)
|
|
1570
1655
|
|
|
1656
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1657
|
+
def test_create_saved_views_contain_boolean_filter_params(self):
|
|
1658
|
+
"""
|
|
1659
|
+
Test the entire Save View workflow from creating a Saved View to rendering the View with boolean filter parameters.
|
|
1660
|
+
"""
|
|
1661
|
+
with self.subTest("Create job Saved View with boolean filter parameters"):
|
|
1662
|
+
view_name = "extras:job_list"
|
|
1663
|
+
app_label = view_name.split(":")[0]
|
|
1664
|
+
model_name = view_name.split(":")[1].split("_")[0]
|
|
1665
|
+
self.add_permissions(f"{app_label}.view_{model_name}")
|
|
1666
|
+
create_query_strings = [
|
|
1667
|
+
"&hidden=True",
|
|
1668
|
+
]
|
|
1669
|
+
create_url = self.get_view_url_for_saved_view(action="create")
|
|
1670
|
+
sv_name = "Hidden Jobs"
|
|
1671
|
+
request = {
|
|
1672
|
+
"path": create_url,
|
|
1673
|
+
"data": post_data({"name": sv_name, "view": f"{view_name}", "params": "".join(create_query_strings)}),
|
|
1674
|
+
}
|
|
1675
|
+
self.assertHttpStatus(self.client.post(**request), 302)
|
|
1676
|
+
instance = SavedView.objects.get(name=sv_name)
|
|
1677
|
+
hidden_job = Job.objects.get(name="Example hidden job")
|
|
1678
|
+
hidden_job.description = "I should not show in the UI!"
|
|
1679
|
+
hidden_job.save()
|
|
1680
|
+
self.assertEqual(instance.config["filter_params"]["hidden"], "True")
|
|
1681
|
+
response = self.client.get(reverse(view_name) + "?saved_view=" + str(instance.pk), follow=True)
|
|
1682
|
+
# Assert that Job List View rendered with the boolean filter parameter without error
|
|
1683
|
+
self.assertHttpStatus(response, 200)
|
|
1684
|
+
response_body = extract_page_body(response.content.decode(response.charset))
|
|
1685
|
+
self.assertIn(str(instance.pk), response_body, msg=response_body)
|
|
1686
|
+
self.assertBodyContains(response, f"<strong>{sv_name}</strong>", html=True)
|
|
1687
|
+
# This is the description
|
|
1688
|
+
self.assertBodyContains(response, "I should not show in the UI!", html=True)
|
|
1689
|
+
|
|
1690
|
+
with self.subTest("Create device Saved View with boolean filter parameters"):
|
|
1691
|
+
view_name = "dcim:device_list"
|
|
1692
|
+
app_label = view_name.split(":")[0]
|
|
1693
|
+
model_name = view_name.split(":")[1].split("_")[0]
|
|
1694
|
+
self.add_permissions(f"{app_label}.view_{model_name}")
|
|
1695
|
+
create_query_strings = [
|
|
1696
|
+
"&per_page=12",
|
|
1697
|
+
"&has_primary_ip=True",
|
|
1698
|
+
"&sort=name",
|
|
1699
|
+
]
|
|
1700
|
+
create_url = self.get_view_url_for_saved_view(action="create")
|
|
1701
|
+
sv_name = "Devices with primary ips"
|
|
1702
|
+
request = {
|
|
1703
|
+
"path": create_url,
|
|
1704
|
+
"data": post_data({"name": sv_name, "view": f"{view_name}", "params": "".join(create_query_strings)}),
|
|
1705
|
+
}
|
|
1706
|
+
self.assertHttpStatus(self.client.post(**request), 302)
|
|
1707
|
+
instance = SavedView.objects.get(name=sv_name)
|
|
1708
|
+
self.assertEqual(instance.config["pagination_count"], 12)
|
|
1709
|
+
self.assertEqual(instance.config["filter_params"]["has_primary_ip"], "True")
|
|
1710
|
+
self.assertEqual(instance.config["sort_order"], ["name"])
|
|
1711
|
+
response = self.client.get(reverse(view_name) + "?saved_view=" + str(instance.pk), follow=True)
|
|
1712
|
+
# Assert that Job List View rendered with the boolean filter parameter without error
|
|
1713
|
+
self.assertHttpStatus(response, 200)
|
|
1714
|
+
response_body = extract_page_body(response.content.decode(response.charset))
|
|
1715
|
+
self.assertIn(str(instance.pk), response_body, msg=response_body)
|
|
1716
|
+
self.assertBodyContains(response, f"<strong>{sv_name}</strong>", html=True)
|
|
1717
|
+
|
|
1718
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1719
|
+
def test_update_saved_view_contain_boolean_filter_params(self):
|
|
1720
|
+
with self.subTest("Update job Saved View with boolean filter parameters"):
|
|
1721
|
+
view_name = "extras:job_list"
|
|
1722
|
+
sv_name = "Non-hidden jobs"
|
|
1723
|
+
instance = SavedView.objects.create(
|
|
1724
|
+
name=sv_name,
|
|
1725
|
+
owner=self.user,
|
|
1726
|
+
view=view_name,
|
|
1727
|
+
)
|
|
1728
|
+
update_query_strings = ["hidden=False"]
|
|
1729
|
+
update_url = self.get_view_url_for_saved_view(instance, "edit") + "?" + "".join(update_query_strings)
|
|
1730
|
+
# Try update the saved view with the same user as the owner of the saved view
|
|
1731
|
+
instance.owner.is_active = True
|
|
1732
|
+
instance.owner.save()
|
|
1733
|
+
self.client.force_login(instance.owner)
|
|
1734
|
+
response = self.client.get(update_url)
|
|
1735
|
+
self.assertHttpStatus(response, 302)
|
|
1736
|
+
instance.refresh_from_db()
|
|
1737
|
+
self.assertEqual(instance.config["filter_params"]["hidden"], "False")
|
|
1738
|
+
response = self.client.get(reverse(view_name) + "?saved_view=" + str(instance.pk), follow=True)
|
|
1739
|
+
# Assert that Job List View rendered with the boolean filter parameter without error
|
|
1740
|
+
self.assertHttpStatus(response, 200)
|
|
1741
|
+
response_body = extract_page_body(response.content.decode(response.charset))
|
|
1742
|
+
self.assertNotIn("Example hidden job", response_body, msg=response_body)
|
|
1743
|
+
self.assertBodyContains(response, f"<strong>{sv_name}</strong>", html=True)
|
|
1744
|
+
|
|
1745
|
+
with self.subTest("Update device Saved View with boolean filter parameters"):
|
|
1746
|
+
view_name = "dcim:device_list"
|
|
1747
|
+
sv_name = "Devices with no primary ips"
|
|
1748
|
+
instance = SavedView.objects.create(
|
|
1749
|
+
name=sv_name,
|
|
1750
|
+
owner=self.user,
|
|
1751
|
+
view=view_name,
|
|
1752
|
+
)
|
|
1753
|
+
update_query_strings = ["has_primary_ip=False"]
|
|
1754
|
+
update_url = self.get_view_url_for_saved_view(instance, "edit") + "?" + "".join(update_query_strings)
|
|
1755
|
+
# Try update the saved view with the same user as the owner of the saved view
|
|
1756
|
+
instance.owner.is_active = True
|
|
1757
|
+
instance.owner.save()
|
|
1758
|
+
self.client.force_login(instance.owner)
|
|
1759
|
+
response = self.client.get(update_url)
|
|
1760
|
+
self.assertHttpStatus(response, 302)
|
|
1761
|
+
instance.refresh_from_db()
|
|
1762
|
+
self.assertEqual(instance.config["filter_params"]["has_primary_ip"], "False")
|
|
1763
|
+
response = self.client.get(reverse(view_name) + "?saved_view=" + str(instance.pk), follow=True)
|
|
1764
|
+
# Assert that Job List View rendered with the boolean filter parameter without error
|
|
1765
|
+
self.assertHttpStatus(response, 200)
|
|
1766
|
+
response_body = extract_page_body(response.content.decode(response.charset))
|
|
1767
|
+
self.assertBodyContains(response, f"<strong>{sv_name}</strong>", html=True)
|
|
1768
|
+
|
|
1571
1769
|
|
|
1572
1770
|
# Not a full-fledged PrimaryObjectViewTestCase as there's no BulkEditView for Secrets
|
|
1573
1771
|
class SecretTestCase(
|
nautobot/extras/utils.py
CHANGED
|
@@ -22,9 +22,12 @@ import redis.exceptions
|
|
|
22
22
|
|
|
23
23
|
from nautobot.core.choices import ColorChoices
|
|
24
24
|
from nautobot.core.constants import CHARFIELD_MAX_LENGTH
|
|
25
|
+
from nautobot.core.exceptions import FilterSetFieldNotFound
|
|
25
26
|
from nautobot.core.models.managers import TagsManager
|
|
26
27
|
from nautobot.core.models.utils import find_models_with_matching_fields
|
|
27
28
|
from nautobot.core.utils.data import is_uuid
|
|
29
|
+
from nautobot.core.utils.lookup import get_filterset_for_model, get_model_for_view_name
|
|
30
|
+
from nautobot.core.utils.requests import is_single_choice_field
|
|
28
31
|
from nautobot.extras.choices import DynamicGroupTypeChoices, JobQueueTypeChoices, ObjectChangeActionChoices
|
|
29
32
|
from nautobot.extras.constants import (
|
|
30
33
|
CHANGELOG_MAX_CHANGE_CONTEXT_DETAIL,
|
|
@@ -880,3 +883,30 @@ def bulk_delete_with_bulk_change_logging(qs, batch_size=1000):
|
|
|
880
883
|
finally:
|
|
881
884
|
change_context.defer_object_changes = False
|
|
882
885
|
change_context.reset_deferred_object_changes()
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
def fixup_filterset_query_params(param_dict, view_name, non_filter_params):
|
|
889
|
+
"""
|
|
890
|
+
Called before saving query filter parameters to a SavedView's config. This function will format
|
|
891
|
+
single value query parameters to be saved as a single values instead of lists of singles values.
|
|
892
|
+
|
|
893
|
+
Args:
|
|
894
|
+
param_dict (dict): key-value pairs of query parameters.
|
|
895
|
+
view_name (str): The name of the view that the saved view is associated with. "dcim:location_list" for example.
|
|
896
|
+
non_filter_params (list): List of non-query parameters that should not be formatted.
|
|
897
|
+
"""
|
|
898
|
+
model = get_model_for_view_name(view_name)
|
|
899
|
+
try:
|
|
900
|
+
filterset_class = get_filterset_for_model(model)
|
|
901
|
+
except TypeError:
|
|
902
|
+
return param_dict
|
|
903
|
+
|
|
904
|
+
filterset = filterset_class()
|
|
905
|
+
|
|
906
|
+
for filter_field, value in param_dict.items():
|
|
907
|
+
try:
|
|
908
|
+
if filter_field not in non_filter_params and is_single_choice_field(filterset, filter_field):
|
|
909
|
+
param_dict[filter_field] = value[0]
|
|
910
|
+
except FilterSetFieldNotFound:
|
|
911
|
+
pass
|
|
912
|
+
return param_dict
|
nautobot/extras/views.py
CHANGED
|
@@ -26,6 +26,7 @@ from rest_framework.permissions import IsAuthenticated
|
|
|
26
26
|
|
|
27
27
|
from nautobot.core.constants import PAGINATE_COUNT_DEFAULT
|
|
28
28
|
from nautobot.core.events import publish_event
|
|
29
|
+
from nautobot.core.exceptions import FilterSetFieldNotFound
|
|
29
30
|
from nautobot.core.forms import restrict_form_fields
|
|
30
31
|
from nautobot.core.models.querysets import count_related
|
|
31
32
|
from nautobot.core.models.utils import pretty_print_query, serialize_object_v2
|
|
@@ -36,12 +37,13 @@ from nautobot.core.ui.object_detail import ObjectDetailContent, ObjectFieldsPane
|
|
|
36
37
|
from nautobot.core.utils.config import get_settings_or_config
|
|
37
38
|
from nautobot.core.utils.lookup import (
|
|
38
39
|
get_filterset_for_model,
|
|
40
|
+
get_model_for_view_name,
|
|
39
41
|
get_route_for_model,
|
|
40
42
|
get_table_class_string_from_view_name,
|
|
41
43
|
get_table_for_model,
|
|
42
44
|
)
|
|
43
45
|
from nautobot.core.utils.permissions import get_permission_for_model
|
|
44
|
-
from nautobot.core.utils.requests import normalize_querydict
|
|
46
|
+
from nautobot.core.utils.requests import is_single_choice_field, normalize_querydict
|
|
45
47
|
from nautobot.core.views import generic, viewsets
|
|
46
48
|
from nautobot.core.views.mixins import (
|
|
47
49
|
GetReturnURLMixin,
|
|
@@ -69,7 +71,7 @@ from nautobot.dcim.tables import (
|
|
|
69
71
|
VirtualDeviceContextTable,
|
|
70
72
|
)
|
|
71
73
|
from nautobot.extras.context_managers import deferred_change_logging_for_bulk_operation
|
|
72
|
-
from nautobot.extras.utils import get_base_template, get_job_queue, get_worker_count
|
|
74
|
+
from nautobot.extras.utils import fixup_filterset_query_params, get_base_template, get_job_queue, get_worker_count
|
|
73
75
|
from nautobot.ipam.models import IPAddress, Prefix, VLAN
|
|
74
76
|
from nautobot.ipam.tables import IPAddressTable, PrefixTable, VLANTable
|
|
75
77
|
from nautobot.virtualization.models import VirtualMachine, VMInterface
|
|
@@ -720,8 +722,13 @@ class DynamicGroupView(generic.ObjectView):
|
|
|
720
722
|
|
|
721
723
|
if table_class is not None:
|
|
722
724
|
# Members table (for display on Members nav tab)
|
|
725
|
+
if hasattr(members, "without_tree_fields"):
|
|
726
|
+
members = members.without_tree_fields()
|
|
723
727
|
members_table = table_class(
|
|
724
|
-
members.restrict(request.user, "view"),
|
|
728
|
+
members.restrict(request.user, "view"),
|
|
729
|
+
orderable=False,
|
|
730
|
+
exclude=["dynamic_group_count"],
|
|
731
|
+
hide_hierarchy_ui=True,
|
|
725
732
|
)
|
|
726
733
|
paginate = {
|
|
727
734
|
"paginator_class": EnhancedPaginator,
|
|
@@ -1158,6 +1165,9 @@ class GitRepositoryResultView(generic.ObjectView):
|
|
|
1158
1165
|
def get_extra_context(self, request, instance):
|
|
1159
1166
|
job_result = instance.get_latest_sync()
|
|
1160
1167
|
|
|
1168
|
+
if job_result is None:
|
|
1169
|
+
job_result = {}
|
|
1170
|
+
|
|
1161
1171
|
return {
|
|
1162
1172
|
"result": job_result,
|
|
1163
1173
|
"base_template": "extras/gitrepository.html",
|
|
@@ -1265,9 +1275,10 @@ class JobListView(generic.ObjectListView):
|
|
|
1265
1275
|
def alter_queryset(self, request):
|
|
1266
1276
|
queryset = super().alter_queryset(request)
|
|
1267
1277
|
# Default to hiding "hidden" and non-installed jobs
|
|
1268
|
-
|
|
1278
|
+
filter_params = self.get_filter_params(request)
|
|
1279
|
+
if "hidden" not in filter_params:
|
|
1269
1280
|
queryset = queryset.filter(hidden=False)
|
|
1270
|
-
if "installed" not in
|
|
1281
|
+
if "installed" not in filter_params:
|
|
1271
1282
|
queryset = queryset.filter(installed=True)
|
|
1272
1283
|
return queryset
|
|
1273
1284
|
|
|
@@ -1806,15 +1817,23 @@ class SavedViewUIViewSet(
|
|
|
1806
1817
|
if sort_order:
|
|
1807
1818
|
sv.config["sort_order"] = sort_order
|
|
1808
1819
|
|
|
1820
|
+
model = get_model_for_view_name(sv.view)
|
|
1821
|
+
filterset_class = get_filterset_for_model(model)
|
|
1822
|
+
filterset = filterset_class()
|
|
1809
1823
|
filter_params = {}
|
|
1810
1824
|
for key in request.GET:
|
|
1811
1825
|
if key in self.non_filter_params:
|
|
1812
1826
|
continue
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1827
|
+
try:
|
|
1828
|
+
if is_single_choice_field(filterset, key):
|
|
1829
|
+
filter_params[key] = request.GET.getlist(key)[0]
|
|
1830
|
+
except FilterSetFieldNotFound:
|
|
1831
|
+
continue
|
|
1832
|
+
try:
|
|
1833
|
+
if not is_single_choice_field(filterset, key):
|
|
1834
|
+
filter_params[key] = request.GET.getlist(key)
|
|
1835
|
+
except FilterSetFieldNotFound:
|
|
1836
|
+
continue
|
|
1818
1837
|
|
|
1819
1838
|
if filter_params:
|
|
1820
1839
|
sv.config["filter_params"] = filter_params
|
|
@@ -1839,14 +1858,14 @@ class SavedViewUIViewSet(
|
|
|
1839
1858
|
and the name of the new SavedView from request.POST to create a new SavedView.
|
|
1840
1859
|
"""
|
|
1841
1860
|
name = request.POST.get("name")
|
|
1861
|
+
view_name = request.POST.get("view")
|
|
1842
1862
|
is_shared = request.POST.get("is_shared", False)
|
|
1843
1863
|
if is_shared:
|
|
1844
1864
|
is_shared = True
|
|
1845
1865
|
params = request.POST.get("params", "")
|
|
1866
|
+
param_dict = fixup_filterset_query_params(parse_qs(params), view_name, self.non_filter_params)
|
|
1846
1867
|
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
single_value_params = ["saved_view", "table_changes_pending", "all_filters_removed", "q", "per_page"]
|
|
1868
|
+
single_value_params = ["saved_view", "table_changes_pending", "all_filters_removed", "per_page"]
|
|
1850
1869
|
for key in param_dict.keys():
|
|
1851
1870
|
if key in single_value_params:
|
|
1852
1871
|
param_dict[key] = param_dict[key][0]
|
|
@@ -1855,7 +1874,6 @@ class SavedViewUIViewSet(
|
|
|
1855
1874
|
derived_instance = None
|
|
1856
1875
|
if derived_view_pk:
|
|
1857
1876
|
derived_instance = self.get_queryset().get(pk=derived_view_pk)
|
|
1858
|
-
view_name = request.POST.get("view")
|
|
1859
1877
|
try:
|
|
1860
1878
|
reverse(view_name)
|
|
1861
1879
|
except NoReverseMatch:
|
nautobot/ipam/api/serializers.py
CHANGED
|
@@ -63,14 +63,13 @@ class VRFDeviceAssignmentSerializer(ValidatedModelSerializer):
|
|
|
63
63
|
validators = []
|
|
64
64
|
|
|
65
65
|
def validate(self, attrs):
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
validator(attrs, self)
|
|
66
|
+
foreign_key_fields = ["device", "virtual_machine", "virtual_device_context"]
|
|
67
|
+
for foreign_key in foreign_key_fields:
|
|
68
|
+
if attrs.get(foreign_key):
|
|
69
|
+
validator = UniqueTogetherValidator(
|
|
70
|
+
queryset=VRFDeviceAssignment.objects.all(), fields=(foreign_key, "vrf")
|
|
71
|
+
)
|
|
72
|
+
validator(attrs, self)
|
|
74
73
|
return super().validate(attrs)
|
|
75
74
|
|
|
76
75
|
|
nautobot/ipam/api/views.py
CHANGED
|
@@ -13,7 +13,7 @@ from nautobot.core.constants import MAX_PAGE_SIZE_DEFAULT, PAGINATE_COUNT_DEFAUL
|
|
|
13
13
|
from nautobot.core.models.querysets import count_related
|
|
14
14
|
from nautobot.core.utils.config import get_settings_or_config
|
|
15
15
|
from nautobot.dcim.models import Location
|
|
16
|
-
from nautobot.extras.api.views import NautobotModelViewSet
|
|
16
|
+
from nautobot.extras.api.views import ModelViewSet, NautobotModelViewSet
|
|
17
17
|
from nautobot.ipam import filters
|
|
18
18
|
from nautobot.ipam.api import serializers
|
|
19
19
|
from nautobot.ipam.models import (
|
|
@@ -55,13 +55,13 @@ class VRFViewSet(NautobotModelViewSet):
|
|
|
55
55
|
filterset_class = filters.VRFFilterSet
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
class VRFDeviceAssignmentViewSet(
|
|
58
|
+
class VRFDeviceAssignmentViewSet(ModelViewSet):
|
|
59
59
|
queryset = VRFDeviceAssignment.objects.all()
|
|
60
60
|
serializer_class = serializers.VRFDeviceAssignmentSerializer
|
|
61
61
|
filterset_class = filters.VRFDeviceAssignmentFilterSet
|
|
62
62
|
|
|
63
63
|
|
|
64
|
-
class VRFPrefixAssignmentViewSet(
|
|
64
|
+
class VRFPrefixAssignmentViewSet(ModelViewSet):
|
|
65
65
|
queryset = VRFPrefixAssignment.objects.all()
|
|
66
66
|
serializer_class = serializers.VRFPrefixAssignmentSerializer
|
|
67
67
|
filterset_class = filters.VRFPrefixAssignmentFilterSet
|
|
@@ -323,7 +323,7 @@ class PrefixViewSet(NautobotModelViewSet):
|
|
|
323
323
|
return Response(serializer.data)
|
|
324
324
|
|
|
325
325
|
|
|
326
|
-
class PrefixLocationAssignmentViewSet(
|
|
326
|
+
class PrefixLocationAssignmentViewSet(ModelViewSet):
|
|
327
327
|
queryset = PrefixLocationAssignment.objects.all()
|
|
328
328
|
serializer_class = serializers.PrefixLocationAssignmentSerializer
|
|
329
329
|
filterset_class = filters.PrefixLocationAssignmentFilterSet
|
|
@@ -581,7 +581,7 @@ class VLANViewSet(NautobotModelViewSet):
|
|
|
581
581
|
raise self.LocationIncompatibleLegacyBehavior from e
|
|
582
582
|
|
|
583
583
|
|
|
584
|
-
class VLANLocationAssignmentViewSet(
|
|
584
|
+
class VLANLocationAssignmentViewSet(ModelViewSet):
|
|
585
585
|
queryset = VLANLocationAssignment.objects.all()
|
|
586
586
|
serializer_class = serializers.VLANLocationAssignmentSerializer
|
|
587
587
|
filterset_class = filters.VLANLocationAssignmentFilterSet
|
nautobot/ipam/factory.py
CHANGED
|
@@ -15,7 +15,7 @@ from nautobot.core.factory import (
|
|
|
15
15
|
random_instance,
|
|
16
16
|
UniqueFaker,
|
|
17
17
|
)
|
|
18
|
-
from nautobot.dcim.models import Location
|
|
18
|
+
from nautobot.dcim.models import Location, VirtualDeviceContext
|
|
19
19
|
from nautobot.extras.models import Role, Status
|
|
20
20
|
from nautobot.ipam.choices import PrefixTypeChoices
|
|
21
21
|
from nautobot.ipam.models import IPAddress, Namespace, Prefix, RIR, RouteTarget, VLAN, VLANGroup, VRF
|
|
@@ -127,6 +127,24 @@ class VRFFactory(PrimaryModelFactory):
|
|
|
127
127
|
else:
|
|
128
128
|
self.export_targets.set(get_random_instances(RouteTarget))
|
|
129
129
|
|
|
130
|
+
@factory.post_generation
|
|
131
|
+
def prefixes(self, create, extracted, **kwargs):
|
|
132
|
+
if create:
|
|
133
|
+
if extracted:
|
|
134
|
+
self.prefixes.set(extracted)
|
|
135
|
+
else:
|
|
136
|
+
self.prefixes.set(
|
|
137
|
+
get_random_instances(lambda: Prefix.objects.filter(namespace=self.namespace), minimum=0)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
@factory.post_generation
|
|
141
|
+
def virtual_device_contexts(self, create, extracted, **kwargs):
|
|
142
|
+
if create:
|
|
143
|
+
if extracted:
|
|
144
|
+
self.virtual_device_contexts.set(extracted)
|
|
145
|
+
else:
|
|
146
|
+
self.virtual_device_contexts.set(get_random_instances(VirtualDeviceContext))
|
|
147
|
+
|
|
130
148
|
|
|
131
149
|
class VLANGroupFactory(OrganizationalModelFactory):
|
|
132
150
|
class Meta:
|
|
@@ -295,7 +313,6 @@ class PrefixFactory(PrimaryModelFactory):
|
|
|
295
313
|
has_role = NautobotBoolIterator()
|
|
296
314
|
has_tenant = NautobotBoolIterator()
|
|
297
315
|
has_vlan = NautobotBoolIterator()
|
|
298
|
-
# has_vrf = NautobotBoolIterator()
|
|
299
316
|
is_ipv6 = NautobotBoolIterator()
|
|
300
317
|
|
|
301
318
|
prefix = factory.Maybe(
|
|
@@ -321,12 +338,6 @@ class PrefixFactory(PrimaryModelFactory):
|
|
|
321
338
|
None,
|
|
322
339
|
)
|
|
323
340
|
namespace = random_instance(Namespace, allow_null=False)
|
|
324
|
-
# TODO: Update for M2M tests
|
|
325
|
-
# vrf = factory.Maybe(
|
|
326
|
-
# "has_vrf",
|
|
327
|
-
# factory.SubFactory(VRFGetOrCreateFactory, tenant=factory.SelfAttribute("..tenant")),
|
|
328
|
-
# None,
|
|
329
|
-
# )
|
|
330
341
|
rir = factory.Maybe("has_rir", random_instance(RIR, allow_null=False), None)
|
|
331
342
|
date_allocated = factory.Maybe("has_date_allocated", factory.Faker("date_time", tzinfo=datetime.timezone.utc), None)
|
|
332
343
|
|
|
@@ -343,6 +354,14 @@ class PrefixFactory(PrimaryModelFactory):
|
|
|
343
354
|
)
|
|
344
355
|
)
|
|
345
356
|
|
|
357
|
+
@factory.post_generation
|
|
358
|
+
def vrfs(self, create, extracted, **kwargs):
|
|
359
|
+
if create:
|
|
360
|
+
if extracted:
|
|
361
|
+
self.vrfs.set(extracted)
|
|
362
|
+
else:
|
|
363
|
+
self.vrfs.set(get_random_instances(lambda: VRF.objects.filter(namespace=self.namespace), minimum=0))
|
|
364
|
+
|
|
346
365
|
@factory.post_generation
|
|
347
366
|
def children(self, create, extracted, **kwargs):
|
|
348
367
|
"""Creates child prefixes and ip addresses within the prefix IP space.
|