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
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from django.apps import apps
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.core.management.base import BaseCommand
|
|
6
|
+
from django.urls import get_resolver
|
|
7
|
+
from django.utils.http import urlencode
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
from nautobot.core.utils.lookup import get_model_for_view_name
|
|
11
|
+
|
|
12
|
+
# List of view names that are excluded for various error responses
|
|
13
|
+
EXCLUDED_VIEW_NAMES = [
|
|
14
|
+
"graphql-api", # "Method \\"GET\\" not allowed."
|
|
15
|
+
"graphql", # "Must provide query string."
|
|
16
|
+
"dcim-api:device-napalm", # "No platform is configured for this device."
|
|
17
|
+
"dcim-api:connected-device-list", # "Request must include \\"peer_device\\" and \\"peer_interface\\" filters."
|
|
18
|
+
"login",
|
|
19
|
+
"logout",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
# List of reversed url name suffixes that are used to identify GET endpoints UI and API
|
|
23
|
+
GET_ENDPOINT_SUFFIXES = ("_list", "_notes", "_changelog", "-detail", "-list", "-notes")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Command(BaseCommand):
|
|
27
|
+
"""
|
|
28
|
+
Example usage: `nautobot-server generate_performance_test_endpoints > endpoints.yml`
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
help = "List all relevant performance test url patterns in Nautobot Core"
|
|
32
|
+
|
|
33
|
+
def add_arguments(self, parser):
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"--output-file",
|
|
36
|
+
help="A file path string that specifies the output file to write the endpoints to.",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def handle(self, *args, **options):
|
|
40
|
+
# Get the URL resolver
|
|
41
|
+
url_patterns = get_resolver().url_patterns
|
|
42
|
+
|
|
43
|
+
# Group the urls by app names
|
|
44
|
+
self.app_name_to_urls = {}
|
|
45
|
+
self.app_name_to_urls["endpoints"] = {}
|
|
46
|
+
# Fetch and store the urls by app names in the dictionary
|
|
47
|
+
self.fetch_urls(url_patterns)
|
|
48
|
+
for view_name, url_patterns in self.app_name_to_urls["endpoints"].items():
|
|
49
|
+
# De-duplicate the URL patterns and sort them.
|
|
50
|
+
self.app_name_to_urls["endpoints"][view_name] = sorted(list(set(url_patterns)))
|
|
51
|
+
|
|
52
|
+
if filepath := options.get("output_file"):
|
|
53
|
+
# Output the endpoints to a yaml file
|
|
54
|
+
with open(filepath, "w") as outfile:
|
|
55
|
+
yaml.dump(self.app_name_to_urls, outfile, sort_keys=True)
|
|
56
|
+
else:
|
|
57
|
+
# Output the endpoints to the console
|
|
58
|
+
self.stdout.write(yaml.dump(self.app_name_to_urls, sort_keys=True))
|
|
59
|
+
|
|
60
|
+
def is_eligible_get_endpoint(self, view_name):
|
|
61
|
+
"""
|
|
62
|
+
Check if the view is a GET endpoint and if it is eligible for performance testing.
|
|
63
|
+
"""
|
|
64
|
+
if view_name not in EXCLUDED_VIEW_NAMES and (view_name.endswith(GET_ENDPOINT_SUFFIXES) or "_" not in view_name):
|
|
65
|
+
return True
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
def append_urls_to_dict(self, url_pattern, model_class, view_name, is_api_endpoint=False):
|
|
69
|
+
"""
|
|
70
|
+
URL patterns are stored in the dictionary in the following format:
|
|
71
|
+
- Any model detail view URL pattern that contains `<uuid:pk>` or `(?P<pk>[/.]+)` will have two endpoints:
|
|
72
|
+
- One with the `model_class.objects.first().pk`
|
|
73
|
+
- One with the `model_class.objects.last().pk`
|
|
74
|
+
- Any model list view URL pattern will have two endpoints:
|
|
75
|
+
- One with default pagination
|
|
76
|
+
- One with custom pagination (5 pages with <total_object_count//5> instances per page)
|
|
77
|
+
- Any generic endpoint like `core:home` will have one endpoint which is the URL pattern itself.
|
|
78
|
+
"""
|
|
79
|
+
if not model_class:
|
|
80
|
+
# A generic endpoint like `core:home`
|
|
81
|
+
if view_name not in self.app_name_to_urls["endpoints"]:
|
|
82
|
+
self.app_name_to_urls["endpoints"][view_name] = []
|
|
83
|
+
self.app_name_to_urls["endpoints"][view_name].append(url_pattern)
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
# Handle detail view url patterns
|
|
87
|
+
total_count = len(model_class.objects.all())
|
|
88
|
+
if "_list" not in view_name and "-list" not in view_name:
|
|
89
|
+
# If the model class is found, then we know we are dealing with a model related endpoint
|
|
90
|
+
if total_count == 0:
|
|
91
|
+
# TODO handle the case where there is no instances of the model is found
|
|
92
|
+
self.stderr.write(f"Not enough instances of {model_class} found, need at least 1")
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
# Identify the placeholder for the uuid
|
|
96
|
+
replace_string = ""
|
|
97
|
+
if "<uuid:pk>" in url_pattern:
|
|
98
|
+
replace_string = "<uuid:pk>"
|
|
99
|
+
elif "(?P<pk>[/.]+)" in url_pattern:
|
|
100
|
+
replace_string = "(?P<pk>[/.]+)"
|
|
101
|
+
|
|
102
|
+
if replace_string:
|
|
103
|
+
# Replace the uuid with the actual uuid
|
|
104
|
+
if total_count == 1:
|
|
105
|
+
# Case where there is only one instance of the model
|
|
106
|
+
first_url_pattern = url_pattern.replace(replace_string, str(model_class.objects.first().pk))
|
|
107
|
+
if view_name not in self.app_name_to_urls["endpoints"]:
|
|
108
|
+
self.app_name_to_urls["endpoints"][view_name] = []
|
|
109
|
+
self.app_name_to_urls["endpoints"][view_name].append(first_url_pattern)
|
|
110
|
+
else:
|
|
111
|
+
# Case where there is more than one instance of the model
|
|
112
|
+
first_url_pattern = url_pattern.replace(replace_string, str(model_class.objects.first().pk))
|
|
113
|
+
second_url_pattern = url_pattern.replace(replace_string, str(model_class.objects.last().pk))
|
|
114
|
+
if view_name not in self.app_name_to_urls["endpoints"]:
|
|
115
|
+
self.app_name_to_urls["endpoints"][view_name] = []
|
|
116
|
+
self.app_name_to_urls["endpoints"][view_name].append(first_url_pattern)
|
|
117
|
+
self.app_name_to_urls["endpoints"][view_name].append(second_url_pattern)
|
|
118
|
+
# Handle list view url patterns
|
|
119
|
+
else:
|
|
120
|
+
if view_name not in self.app_name_to_urls["endpoints"]:
|
|
121
|
+
self.app_name_to_urls["endpoints"][view_name] = []
|
|
122
|
+
# One endpoint with default pagination
|
|
123
|
+
self.app_name_to_urls["endpoints"][view_name].append(url_pattern)
|
|
124
|
+
page_query_parameter = 5
|
|
125
|
+
per_page_query_parameter = total_count // page_query_parameter
|
|
126
|
+
if not is_api_endpoint:
|
|
127
|
+
query_params = urlencode(
|
|
128
|
+
{
|
|
129
|
+
"per_page": per_page_query_parameter,
|
|
130
|
+
"page": page_query_parameter,
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
else:
|
|
134
|
+
query_params = urlencode(
|
|
135
|
+
{
|
|
136
|
+
"limit": per_page_query_parameter,
|
|
137
|
+
"offset": per_page_query_parameter * (page_query_parameter - 1),
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
# One endpoint with non-default pagination
|
|
141
|
+
self.app_name_to_urls["endpoints"][view_name].append(url_pattern + f"?{query_params}")
|
|
142
|
+
|
|
143
|
+
def construct_view_name_and_url_pattern(self, pattern) -> tuple[Optional[str], Optional[str], bool]:
|
|
144
|
+
"""
|
|
145
|
+
Args:
|
|
146
|
+
pattern (django.urls.resolvers.URLPattern): A URL pattern object.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
url_pattern (str): The URL pattern of the view.
|
|
150
|
+
view_name (str): The URL name of the view.
|
|
151
|
+
is_api_endpoint (bool): True if the endpoint is an API endpoint, False otherwise.
|
|
152
|
+
"""
|
|
153
|
+
lookup_str_list = pattern.lookup_str.split(".")
|
|
154
|
+
|
|
155
|
+
# Determine if the endpoint belongs to a plugin
|
|
156
|
+
is_app = lookup_str_list[0] != "nautobot"
|
|
157
|
+
is_api_endpoint = "api" in lookup_str_list
|
|
158
|
+
# One of the nautobot apps: nautobot.circuits, nautobot.dcim, and etc. if not is_app
|
|
159
|
+
# One of the plugins: example_app, and etc. if is_app
|
|
160
|
+
app_name = lookup_str_list[0] if is_app else lookup_str_list[1]
|
|
161
|
+
|
|
162
|
+
model = pattern.default_args.get("model", None)
|
|
163
|
+
if model:
|
|
164
|
+
app_name = model._meta.app_label
|
|
165
|
+
|
|
166
|
+
# Retrieve the base URL for the app to be used in the URL pattern
|
|
167
|
+
app_config = apps.get_app_config(app_name)
|
|
168
|
+
base_url = app_config.base_url if hasattr(app_config, "base_url") else app_name
|
|
169
|
+
|
|
170
|
+
if app_name == "users" and pattern.name in ["login", "logout"]:
|
|
171
|
+
# No need to test the login and logout endpoints for performance testing
|
|
172
|
+
url_pattern = f"/{pattern.pattern}" # /login, /logout
|
|
173
|
+
view_name = f"{pattern.name}" # login, logout
|
|
174
|
+
elif app_name == "core":
|
|
175
|
+
# Handle the special case where a view exist in the core app
|
|
176
|
+
# but its url pattern and view name does not include the prefix "/core" or "core:"
|
|
177
|
+
# ['nautobot', 'core', "views", "HomeView"]
|
|
178
|
+
# ['nautobot', 'core', "api", "views", "APIRootView"]
|
|
179
|
+
if pattern.name in ["api-root", "api-status", "graphql-api"]:
|
|
180
|
+
is_api_endpoint = True
|
|
181
|
+
url_pattern = f"/api/{pattern.pattern}" # /api/status
|
|
182
|
+
view_name = f"{pattern.name}" # api-status
|
|
183
|
+
elif pattern.name in ["home", "about", "search", "worker-status", "graphql", "metrics"]:
|
|
184
|
+
url_pattern = f"/{pattern.pattern}" # /home, /about, /search
|
|
185
|
+
view_name = f"{pattern.name}" # home, about, search
|
|
186
|
+
else:
|
|
187
|
+
url_pattern = None
|
|
188
|
+
view_name = None
|
|
189
|
+
elif app_name == "extras" and "plugins" in lookup_str_list:
|
|
190
|
+
# Handle the special case first for Installed apps related view is nested under the extras app.
|
|
191
|
+
# ['nautobot', 'extras', 'plugins', 'views', 'InstalledAppsView']
|
|
192
|
+
|
|
193
|
+
# We need special case handling to determine if the endpoint is an api endpoint as well for this view
|
|
194
|
+
view_class_name = lookup_str_list[-1]
|
|
195
|
+
if "API" in view_class_name:
|
|
196
|
+
is_api_endpoint = True
|
|
197
|
+
apps_or_plugins = "plugins" if "plugins" in pattern.name else "apps"
|
|
198
|
+
if is_api_endpoint:
|
|
199
|
+
url_pattern = f"/api/{apps_or_plugins}/{pattern.pattern}" # /api/apps/installed-apps
|
|
200
|
+
view_name = f"{apps_or_plugins}-api:{pattern.name}" # apps-api:apps-list
|
|
201
|
+
else:
|
|
202
|
+
url_pattern = f"/{apps_or_plugins}/{pattern.pattern}" # /apps/installed-apps
|
|
203
|
+
view_name = f"{apps_or_plugins}:{pattern.name}" # apps:apps_list
|
|
204
|
+
elif is_api_endpoint:
|
|
205
|
+
if not is_app:
|
|
206
|
+
# One of the nautobot apps: nautobot.circuits, nautobot.dcim, and etc.
|
|
207
|
+
url_pattern = f"/api/{base_url}/{pattern.pattern}" # /api/dcim/devices/
|
|
208
|
+
app_name = f"{app_name}-api" # dcim-api
|
|
209
|
+
view_name = f"{app_name}:{pattern.name}" # dcim-api:device-list
|
|
210
|
+
else:
|
|
211
|
+
api_app_name = f"{app_name}-api" # example_app-api
|
|
212
|
+
view_name = (
|
|
213
|
+
f"plugins-api:{api_app_name}:{pattern.name}" # plugins-api:example_app-api:examplemodel-list
|
|
214
|
+
)
|
|
215
|
+
url_pattern = f"/api/plugins/{base_url}/{pattern.pattern}" # /api/plugins/example-app/models/
|
|
216
|
+
else:
|
|
217
|
+
if not is_app:
|
|
218
|
+
url_pattern = f"/{base_url}/{pattern.pattern}" # /dcim/devices/
|
|
219
|
+
view_name = f"{app_name}:{pattern.name}" # dcim:device_list
|
|
220
|
+
else:
|
|
221
|
+
view_name = f"plugins:{app_name}:{pattern.name}" # plugins:example_app:examplemodel_list
|
|
222
|
+
url_pattern = f"/plugins/{base_url}/{pattern.pattern}" # /plugins/example-app/models/
|
|
223
|
+
|
|
224
|
+
return url_pattern, view_name, is_api_endpoint
|
|
225
|
+
|
|
226
|
+
def fetch_urls(self, url_patterns):
|
|
227
|
+
"""
|
|
228
|
+
Store the URL patterns in the dictionary to output an .YAML file
|
|
229
|
+
The dictionary will have the following structure:
|
|
230
|
+
{
|
|
231
|
+
"endpoints": {
|
|
232
|
+
<app_name>:<view_name>: [
|
|
233
|
+
<url_pattern_1>,
|
|
234
|
+
<url_pattern_2>,
|
|
235
|
+
],
|
|
236
|
+
dcim:device: [
|
|
237
|
+
"/dcim/devices/cfbd447f-d563-4fac-bb75-bdda70ab4e80/",
|
|
238
|
+
"/dcim/devices/38471bfe-0aca-4e09-b545-b0f90280fb66/",
|
|
239
|
+
],
|
|
240
|
+
dcim-api:device-detail: [
|
|
241
|
+
"/api/dcim/devices/cfbd447f-d563-4fac-bb75-bdda70ab4e80/",
|
|
242
|
+
"/api/dcim/devices/38471bfe-0aca-4e09-b545-b0f90280fb66/",
|
|
243
|
+
],
|
|
244
|
+
...
|
|
245
|
+
},
|
|
246
|
+
...
|
|
247
|
+
"""
|
|
248
|
+
for pattern in url_patterns:
|
|
249
|
+
if hasattr(pattern, "url_patterns"):
|
|
250
|
+
# If it's a nested URL pattern, recursively list its URLs
|
|
251
|
+
self.fetch_urls(pattern.url_patterns)
|
|
252
|
+
else:
|
|
253
|
+
# Only fetch urls from relevant apps
|
|
254
|
+
if pattern.lookup_str.startswith(("nautobot.", *settings.PLUGINS)):
|
|
255
|
+
url_pattern, view_name, is_api_endpoint = self.construct_view_name_and_url_pattern(pattern)
|
|
256
|
+
# We do not need to test the ?format=<json,csv,api> endpoints and non-GET endpoints
|
|
257
|
+
if (
|
|
258
|
+
url_pattern is not None
|
|
259
|
+
and "(?P<format>[a-z0-9]+)" not in url_pattern
|
|
260
|
+
and "<drf_format_suffix:format>" not in url_pattern
|
|
261
|
+
and self.is_eligible_get_endpoint(view_name)
|
|
262
|
+
):
|
|
263
|
+
# Replace "^" and "$" from the url pattern
|
|
264
|
+
url_pattern = url_pattern.replace("^", "").replace("$", "")
|
|
265
|
+
# Retrieve the model class for the view name
|
|
266
|
+
try:
|
|
267
|
+
model_class = get_model_for_view_name(view_name)
|
|
268
|
+
except ValueError:
|
|
269
|
+
# In case it is a generic view like /home, /about, /search
|
|
270
|
+
model_class = None
|
|
271
|
+
self.append_urls_to_dict(url_pattern, model_class, view_name, is_api_endpoint)
|
nautobot/core/models/utils.py
CHANGED
|
@@ -112,7 +112,12 @@ def serialize_object(obj, extra=None, exclude=None):
|
|
|
112
112
|
|
|
113
113
|
# Include any tags. Check for tags cached on the instance; fall back to using the manager.
|
|
114
114
|
if is_taggable(obj):
|
|
115
|
-
|
|
115
|
+
# Note that when upgrading from Nautobot 1.x to 2.0, this method may be called during data migrations,
|
|
116
|
+
# specifically ipam_0022 and dcim_0034, to create ObjectChange records.
|
|
117
|
+
# This can be problematic (see issue #6952) as the Tag records in the DB still have `created` as a `DateField`,
|
|
118
|
+
# but the 2.x code expects this to be a `DateTimeField` (as it will be after the upgrade completes in full).
|
|
119
|
+
# We "cleverly" bypass that issue by using `.only("name")` since that's the only actual Tag field we need here.
|
|
120
|
+
tags = getattr(obj, "_tags", []) or obj.tags.only("name")
|
|
116
121
|
data["tags"] = [tag.name for tag in tags]
|
|
117
122
|
|
|
118
123
|
# Append any extra data
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
{% extends 'generic/object_bulk_destroy.html' %}
|
|
2
|
-
{% comment %}
|
|
2
|
+
{% comment %}3.0 TODO: remove this template, which only exists for backward compatibility with 2.4 and earlier{% endcomment %}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
{% extends 'generic/object_bulk_update.html' %}
|
|
2
|
-
{% comment %}
|
|
2
|
+
{% comment %}3.0 TODO: remove this template, which only exists for backward compatibility with 2.4 and earlier{% endcomment %}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
{% extends 'generic/object_bulk_create.html' %}
|
|
2
|
-
{% comment %}
|
|
2
|
+
{% comment %}3.0 TODO: remove this template, which only exists for backward compatibility with 2.4 and earlier{% endcomment %}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
{% extends 'generic/object_destroy.html' %}
|
|
2
|
-
{% comment %}
|
|
2
|
+
{% comment %}3.0 TODO: remove this template, which only exists for backward compatibility with 2.4 and earlier{% endcomment %}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
{% extends 'generic/object_retrieve.html' %}
|
|
2
|
-
{% comment %}
|
|
2
|
+
{% comment %}3.0 TODO: remove this template, which only exists for backward compatibility with 2.4 and earlier{% endcomment %}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
{% extends 'generic/object_create.html' %}
|
|
2
|
-
{% comment %}
|
|
2
|
+
{% comment %}3.0 TODO: remove this template, which only exists for backward compatibility with 2.4 and earlier{% endcomment %}
|
|
@@ -24,7 +24,10 @@
|
|
|
24
24
|
onerror="window.location='{% url 'media_failure' %}?filename=js/theme.js'"></script>
|
|
25
25
|
<script src="{% versioned_static 'js/table_sorting_indicator.js' %}"
|
|
26
26
|
onerror="window.location='{% url 'media_failure' %}?filename=js/table_sorting_indicator.js'"></script>
|
|
27
|
+
<script src="{% versioned_static 'js/dropdown.js' %}"
|
|
28
|
+
onerror="window.location='{% url 'media_failure' %}?filename=js/dropdown.js'"></script>
|
|
27
29
|
<script type="text/javascript">
|
|
30
|
+
var nautobot_static_url = "{% static '' %}";
|
|
28
31
|
var nautobot_api_path = "{% url 'api-root' %}";
|
|
29
32
|
var nautobot_csrf_token = "{{ csrf_token }}";
|
|
30
33
|
var loading = $(".loading");
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{% if widget.is_initial %}{{ widget.initial_text }}: <a href="{{ widget.value.url }}">{{ widget.value }}</a>{% if not widget.required %}
|
|
2
|
+
<input type="checkbox" name="{{ widget.checkbox_name }}" id="{{ widget.checkbox_id }}"{% if widget.attrs.disabled %} disabled{% endif %}{% if widget.attrs.checked %} checked{% endif %}>
|
|
3
|
+
<label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}</label>{% endif %}<br>
|
|
4
|
+
{{ widget.input_text }}:{% endif %}
|
|
5
|
+
<input type="{{ widget.type }}" name="{{ widget.name }}" class="filestyle" data-placeholder="No file selected" {% include "django/forms/widgets/attrs.html" %}>
|
|
@@ -870,9 +870,7 @@ def saved_view_modal(
|
|
|
870
870
|
):
|
|
871
871
|
from nautobot.extras.forms import SavedViewModalForm
|
|
872
872
|
from nautobot.extras.models import SavedView
|
|
873
|
-
|
|
874
|
-
param_dict = {}
|
|
875
|
-
filters_applied = parse_qs(params)
|
|
873
|
+
from nautobot.extras.utils import fixup_filterset_query_params
|
|
876
874
|
|
|
877
875
|
sort_order = []
|
|
878
876
|
per_page = None
|
|
@@ -889,6 +887,8 @@ def saved_view_modal(
|
|
|
889
887
|
"table_changes_pending",
|
|
890
888
|
"clear_view",
|
|
891
889
|
]
|
|
890
|
+
param_dict = {}
|
|
891
|
+
filters_applied = fixup_filterset_query_params(parse_qs(params), view, non_filter_params)
|
|
892
892
|
|
|
893
893
|
view_class = lookup.get_view_for_model(model, "List")
|
|
894
894
|
table_name = None
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
1
3
|
from django import template
|
|
2
4
|
from django.utils.html import format_html_join
|
|
3
5
|
|
|
4
6
|
from nautobot.core.utils.lookup import get_view_for_model
|
|
5
7
|
from nautobot.core.views.utils import get_obj_from_context
|
|
6
8
|
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
7
11
|
register = template.Library()
|
|
8
12
|
|
|
9
13
|
|
|
@@ -26,15 +30,27 @@ def render_components(context, components):
|
|
|
26
30
|
@register.simple_tag(takes_context=True)
|
|
27
31
|
def render_detail_view_extra_buttons(context):
|
|
28
32
|
"""
|
|
29
|
-
Render the "extra_buttons"
|
|
33
|
+
Render the "extra_buttons" from the context's object_detail_content, or as fallback, from the base detail view.
|
|
30
34
|
|
|
31
35
|
This makes it possible for "extra" tabs (such as Changelog and Notes, and any added by App TemplateExtensions)
|
|
32
36
|
to automatically still render any `extra_buttons` defined by the base detail view, without the tab-specific views
|
|
33
37
|
needing to explicitly inherit from the base view.
|
|
34
38
|
"""
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
object_detail_content = context.get("object_detail_content")
|
|
40
|
+
if object_detail_content is None:
|
|
41
|
+
obj = get_obj_from_context(context)
|
|
42
|
+
if obj is None:
|
|
43
|
+
logger.error("No 'obj' or 'object' found in the render context!")
|
|
44
|
+
return ""
|
|
45
|
+
base_detail_view = get_view_for_model(obj)
|
|
46
|
+
if base_detail_view is None:
|
|
47
|
+
logger.warning(
|
|
48
|
+
"Unable to identify the base detail view - check that it has a valid name, i.e. %sUIViewSet or %sView",
|
|
49
|
+
type(obj).__name__,
|
|
50
|
+
type(obj).__name__,
|
|
51
|
+
)
|
|
52
|
+
return ""
|
|
53
|
+
object_detail_content = getattr(base_detail_view, "object_detail_content", None)
|
|
38
54
|
if object_detail_content is not None and object_detail_content.extra_buttons:
|
|
39
55
|
return render_components(context, object_detail_content.extra_buttons)
|
|
40
56
|
return ""
|
nautobot/core/testing/forms.py
CHANGED
|
@@ -24,7 +24,7 @@ class FormTestCases:
|
|
|
24
24
|
self.skipTest(f"{self.form_class.__name__}.{field_name} has no query_params")
|
|
25
25
|
field_model = field_class.queryset.model
|
|
26
26
|
filterset_class = get_filterset_for_model(field_model)
|
|
27
|
-
filterset_fields = set(filterset_class.
|
|
27
|
+
filterset_fields = set(filterset_class.get_filters().keys())
|
|
28
28
|
invalid_query_params = query_params_fields - filterset_fields
|
|
29
29
|
self.assertFalse(
|
|
30
30
|
invalid_query_params,
|
|
@@ -86,6 +86,10 @@ class ObjectsListMixin:
|
|
|
86
86
|
"""
|
|
87
87
|
self.click_button('#select_all_box button[name="_edit"]')
|
|
88
88
|
|
|
89
|
+
def click_table_link(self, row=1, column=2):
|
|
90
|
+
"""By default, tries to click column next to checkbox to go to the details page."""
|
|
91
|
+
self.browser.find_by_xpath(f'//*[@id="object_list_form"]//tbody/tr[{row}]/td[{column}]/a').click()
|
|
92
|
+
|
|
89
93
|
@property
|
|
90
94
|
def objects_list_visible_items(self):
|
|
91
95
|
"""
|
|
@@ -108,6 +112,22 @@ class ObjectsListMixin:
|
|
|
108
112
|
self.click_button('#default-filter button[type="submit"]')
|
|
109
113
|
|
|
110
114
|
|
|
115
|
+
class ObjectDetailsMixin:
|
|
116
|
+
def assertPanelValue(self, panel_label, field_label, expected_value, exact_match=False):
|
|
117
|
+
"""
|
|
118
|
+
Find the proper panel and asserts if given value match rendered field value.
|
|
119
|
+
By default, it's not using the exact match, because on the UI we're often adding
|
|
120
|
+
additional tags, relationships or units.
|
|
121
|
+
"""
|
|
122
|
+
panel_xpath = f'//*[@id="main"]//div[@class="panel-heading"][contains(normalize-space(), "{panel_label}")]/following-sibling::table'
|
|
123
|
+
value = self.browser.find_by_xpath(f'{panel_xpath}//td[text()="{field_label}"]/following-sibling::td[1]').text
|
|
124
|
+
|
|
125
|
+
if exact_match:
|
|
126
|
+
self.assertEqual(value, str(expected_value))
|
|
127
|
+
else:
|
|
128
|
+
self.assertIn(str(expected_value), value)
|
|
129
|
+
|
|
130
|
+
|
|
111
131
|
class BulkOperationsMixin:
|
|
112
132
|
def confirm_bulk_delete_operation(self):
|
|
113
133
|
"""
|
|
@@ -196,6 +216,7 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
|
|
|
196
216
|
|
|
197
217
|
host = "0.0.0.0" # noqa: S104 # hardcoded-bind-all-interfaces -- false positive
|
|
198
218
|
selenium_host = SELENIUM_HOST # Docker: `nautobot`; else `host.docker.internal`
|
|
219
|
+
logged_in = False
|
|
199
220
|
|
|
200
221
|
@classmethod
|
|
201
222
|
def setUpClass(cls):
|
|
@@ -219,6 +240,10 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
|
|
|
219
240
|
def live_server_url(cls): # pylint: disable=no-self-argument
|
|
220
241
|
return f"http://{cls.selenium_host}:{cls.server_thread.port}"
|
|
221
242
|
|
|
243
|
+
def tearDown(self):
|
|
244
|
+
if self.logged_in:
|
|
245
|
+
self.logout()
|
|
246
|
+
|
|
222
247
|
@classmethod
|
|
223
248
|
def tearDownClass(cls):
|
|
224
249
|
"""Close down the browser after tests are ran."""
|
|
@@ -291,19 +316,27 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
|
|
|
291
316
|
self.browser.is_element_not_present_by_css(".loading-results", wait_time=5)
|
|
292
317
|
return search_box
|
|
293
318
|
|
|
319
|
+
def _select_select2_result(self):
|
|
320
|
+
found_results = self.browser.find_by_css(".select2-results li.select2-results__option")
|
|
321
|
+
# click the first found item if it's not `None`: special value to nullify field
|
|
322
|
+
if found_results.first.text != "None":
|
|
323
|
+
found_results.first.click()
|
|
324
|
+
else:
|
|
325
|
+
found_results[1].click()
|
|
326
|
+
|
|
294
327
|
def fill_select2_field(self, field_name, value):
|
|
295
328
|
"""
|
|
296
329
|
Helper function to fill a Select2 single selection field on add/edit forms.
|
|
297
330
|
"""
|
|
298
|
-
|
|
299
|
-
|
|
331
|
+
self._fill_select2_field(field_name, value)
|
|
332
|
+
self._select_select2_result()
|
|
300
333
|
|
|
301
334
|
def fill_filters_select2_field(self, field_name, value):
|
|
302
335
|
"""
|
|
303
336
|
Helper function to fill a Select2 single selection field on filters modals.
|
|
304
337
|
"""
|
|
305
338
|
self._fill_select2_field(field_name, value, search_box_class="select2-search select2-search--inline")
|
|
306
|
-
self.
|
|
339
|
+
self._select_select2_result()
|
|
307
340
|
|
|
308
341
|
def fill_select2_multiselect_field(self, field_name, value):
|
|
309
342
|
"""
|
|
@@ -327,6 +360,7 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
|
|
|
327
360
|
self.user.is_superuser = True
|
|
328
361
|
self.user.save()
|
|
329
362
|
self.login(self.user.username, self.password)
|
|
363
|
+
self.logged_in = True
|
|
330
364
|
|
|
331
365
|
|
|
332
366
|
class BulkOperationsTestCases:
|
|
@@ -367,10 +401,6 @@ class BulkOperationsTestCases:
|
|
|
367
401
|
self.login_as_superuser()
|
|
368
402
|
self.go_to_model_list_page()
|
|
369
403
|
|
|
370
|
-
def tearDown(self):
|
|
371
|
-
self.logout()
|
|
372
|
-
super().tearDown()
|
|
373
|
-
|
|
374
404
|
def go_to_model_list_page(self):
|
|
375
405
|
self.click_navbar_entry(*self.model_menu_path)
|
|
376
406
|
self.assertEqual(self.browser.url, self.live_server_url + reverse(f"{self.model_base_viewname}_list"))
|
nautobot/core/tests/test_api.py
CHANGED
|
@@ -609,7 +609,7 @@ class WritableNestedSerializerTest(testing.APITestCase):
|
|
|
609
609
|
dcim_models.LocationType.objects.get(name="Building"),
|
|
610
610
|
]
|
|
611
611
|
for location_type in self.locations_types:
|
|
612
|
-
location_type.content_types.
|
|
612
|
+
location_type.content_types.add(vlan_group_ct, vlan_ct)
|
|
613
613
|
|
|
614
614
|
self.statuses = extras_models.Status.objects.get_for_model(dcim_models.Location)
|
|
615
615
|
self.location1 = dcim_models.Location.objects.create(
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from io import StringIO
|
|
2
|
+
|
|
3
|
+
from django.core.management import call_command
|
|
4
|
+
import yaml
|
|
5
|
+
|
|
6
|
+
from nautobot.core.testing import TestCase
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ManagementCommandTestCase(TestCase):
|
|
10
|
+
"""Test case for core management commands."""
|
|
11
|
+
|
|
12
|
+
def setUp(self):
|
|
13
|
+
"""Initialize user and client."""
|
|
14
|
+
super().setUpNautobot()
|
|
15
|
+
self.user.is_superuser = True
|
|
16
|
+
self.user.is_staff = True
|
|
17
|
+
self.user.save()
|
|
18
|
+
self.client.force_login(self.user)
|
|
19
|
+
|
|
20
|
+
def test_generate_performance_test_endpoints(self):
|
|
21
|
+
"""Test the generate_performance_test_endpoints management command."""
|
|
22
|
+
out = StringIO()
|
|
23
|
+
call_command("generate_performance_test_endpoints", stdout=out)
|
|
24
|
+
endpoints_dict = yaml.safe_load(out.getvalue())["endpoints"]
|
|
25
|
+
# status_code_to_endpoints = collections.defaultdict(list)
|
|
26
|
+
for view_name, value in endpoints_dict.items():
|
|
27
|
+
for endpoint in value:
|
|
28
|
+
response = self.client.get(endpoint, follow=True)
|
|
29
|
+
self.assertHttpStatus(
|
|
30
|
+
response, 200, f"{view_name}: {endpoint} returns status Code {response.status_code} instead of 200"
|
|
31
|
+
)
|
|
@@ -8,7 +8,7 @@ from django.apps import apps
|
|
|
8
8
|
from django.contrib.auth import get_user_model
|
|
9
9
|
from django.contrib.auth.models import Group
|
|
10
10
|
from django.contrib.contenttypes.models import ContentType
|
|
11
|
-
from django.db.models import Q
|
|
11
|
+
from django.db.models import Count, Q
|
|
12
12
|
from django.test import override_settings, TestCase
|
|
13
13
|
from django.test.client import RequestFactory
|
|
14
14
|
from django.urls import reverse
|
|
@@ -919,8 +919,8 @@ class GraphQLQueryTest(GraphQLTestCaseBase):
|
|
|
919
919
|
priority=789,
|
|
920
920
|
),
|
|
921
921
|
)
|
|
922
|
-
|
|
923
|
-
|
|
922
|
+
cls.namespace = Namespace.objects.annotate(prefix_count=Count("prefixes")).filter(prefix_count__gt=2).first()
|
|
923
|
+
prefixes = Prefix.objects.filter(namespace=cls.namespace)
|
|
924
924
|
vrfs = (
|
|
925
925
|
VRF.objects.create(name="VRF 1", rd="65000:100", namespace=cls.namespace),
|
|
926
926
|
VRF.objects.create(name="VRF 2", rd="65000:200", namespace=cls.namespace),
|
nautobot/core/tests/test_jobs.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import codecs
|
|
1
2
|
from datetime import timedelta
|
|
2
3
|
import json
|
|
3
4
|
from pathlib import Path
|
|
@@ -72,7 +73,9 @@ class ExportObjectListTest(TransactionTestCase):
|
|
|
72
73
|
self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
|
|
73
74
|
self.assertTrue(job_result.files.exists())
|
|
74
75
|
self.assertEqual(Path(job_result.files.first().file.name).name, "nautobot_statuses.csv")
|
|
75
|
-
|
|
76
|
+
csv_bytes = job_result.files.first().file.read()
|
|
77
|
+
self.assertTrue(csv_bytes.startswith(codecs.BOM_UTF8), csv_bytes)
|
|
78
|
+
csv_data = csv_bytes.decode("utf-8")
|
|
76
79
|
self.assertIn(str(instance1.pk), csv_data)
|
|
77
80
|
self.assertNotIn(str(instance2.pk), csv_data)
|
|
78
81
|
|
|
@@ -273,10 +273,25 @@ class GetFooForModelTest(TestCase):
|
|
|
273
273
|
"""
|
|
274
274
|
Test that `get_model_for_view_name` returns the appropriate Model, if the colon separated view name provided.
|
|
275
275
|
"""
|
|
276
|
-
with self.subTest("Test core view."):
|
|
276
|
+
with self.subTest("Test core UI view."):
|
|
277
277
|
self.assertEqual(lookup.get_model_for_view_name("dcim:device_list"), dcim_models.Device)
|
|
278
|
-
|
|
278
|
+
self.assertEqual(lookup.get_model_for_view_name("dcim:device"), dcim_models.Device)
|
|
279
|
+
with self.subTest("Test app UI view."):
|
|
279
280
|
self.assertEqual(lookup.get_model_for_view_name("plugins:example_app:examplemodel_list"), ExampleModel)
|
|
281
|
+
self.assertEqual(lookup.get_model_for_view_name("plugins:example_app:examplemodel"), ExampleModel)
|
|
282
|
+
with self.subTest("Test core API view."):
|
|
283
|
+
self.assertEqual(lookup.get_model_for_view_name("dcim-api:device-list"), dcim_models.Device)
|
|
284
|
+
self.assertEqual(lookup.get_model_for_view_name("dcim-api:device-detail"), dcim_models.Device)
|
|
285
|
+
with self.subTest("Test app API view."):
|
|
286
|
+
self.assertEqual(
|
|
287
|
+
lookup.get_model_for_view_name("plugins-api:example_app-api:examplemodel-detail"), ExampleModel
|
|
288
|
+
)
|
|
289
|
+
self.assertEqual(
|
|
290
|
+
lookup.get_model_for_view_name("plugins-api:example_app-api:examplemodel-list"), ExampleModel
|
|
291
|
+
)
|
|
292
|
+
with self.subTest("Test unconventional model views."):
|
|
293
|
+
self.assertEqual(lookup.get_model_for_view_name("extras-api:contenttype-detail"), ContentType)
|
|
294
|
+
self.assertEqual(lookup.get_model_for_view_name("users-api:group-detail"), Group)
|
|
280
295
|
with self.subTest("Test unexpected view."):
|
|
281
296
|
with self.assertRaises(ValueError) as err:
|
|
282
297
|
lookup.get_model_for_view_name("unknown:plugins:example_app:examplemodel_list")
|
|
@@ -1372,7 +1372,7 @@ class StatsPanel(Panel):
|
|
|
1372
1372
|
value = [related_object_list_url, related_object_count, related_object_title]
|
|
1373
1373
|
stats[related_object_model_class] = value
|
|
1374
1374
|
related_object_model_filterset = get_filterset_for_model(related_object_model_class)
|
|
1375
|
-
if self.filter_name not in related_object_model_filterset.
|
|
1375
|
+
if self.filter_name not in related_object_model_filterset.get_filters():
|
|
1376
1376
|
raise FieldDoesNotExist(
|
|
1377
1377
|
f"{self.filter_name} is not a valid filter field for {related_object_model_class_meta.verbose_name}"
|
|
1378
1378
|
)
|