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.
Files changed (267) hide show
  1. nautobot/apps/filters.py +2 -0
  2. nautobot/circuits/filters.py +1 -1
  3. nautobot/circuits/templates/circuits/inc/circuit_termination.html +1 -1
  4. nautobot/circuits/tests/integration/test_circuit.py +135 -0
  5. nautobot/circuits/tests/test_models.py +5 -3
  6. nautobot/circuits/views.py +4 -1
  7. nautobot/cloud/api/views.py +3 -3
  8. nautobot/cloud/filters.py +3 -6
  9. nautobot/cloud/tests/test_filters.py +21 -0
  10. nautobot/core/admin.py +2 -0
  11. nautobot/core/constants.py +0 -1
  12. nautobot/core/forms/__init__.py +2 -0
  13. nautobot/core/forms/forms.py +2 -1
  14. nautobot/core/forms/widgets.py +8 -0
  15. nautobot/core/jobs/__init__.py +2 -1
  16. nautobot/core/management/commands/generate_performance_test_endpoints.py +271 -0
  17. nautobot/core/models/utils.py +6 -1
  18. nautobot/core/templates/generic/object_bulk_delete.html +1 -1
  19. nautobot/core/templates/generic/object_bulk_edit.html +1 -1
  20. nautobot/core/templates/generic/object_bulk_import.html +1 -1
  21. nautobot/core/templates/generic/object_create.html +5 -0
  22. nautobot/core/templates/generic/object_delete.html +1 -1
  23. nautobot/core/templates/generic/object_detail.html +1 -1
  24. nautobot/core/templates/generic/object_edit.html +1 -1
  25. nautobot/core/templates/inc/javascript.html +3 -0
  26. nautobot/core/templates/widgets/clearable_file.html +5 -0
  27. nautobot/core/templatetags/helpers.py +3 -3
  28. nautobot/core/templatetags/ui_framework.py +20 -4
  29. nautobot/core/testing/forms.py +1 -1
  30. nautobot/core/testing/integration.py +37 -7
  31. nautobot/core/tests/test_api.py +1 -1
  32. nautobot/core/tests/test_commands.py +31 -0
  33. nautobot/core/tests/test_graphql.py +3 -3
  34. nautobot/core/tests/test_jobs.py +4 -1
  35. nautobot/core/tests/test_utils.py +17 -2
  36. nautobot/core/ui/object_detail.py +1 -1
  37. nautobot/core/utils/lookup.py +12 -1
  38. nautobot/core/views/generic.py +9 -1
  39. nautobot/core/views/mixins.py +9 -1
  40. nautobot/dcim/api/serializers.py +36 -0
  41. nautobot/dcim/api/views.py +12 -11
  42. nautobot/dcim/elevations.py +17 -4
  43. nautobot/dcim/factory.py +9 -1
  44. nautobot/dcim/filters/__init__.py +27 -1
  45. nautobot/dcim/forms.py +16 -7
  46. nautobot/dcim/models/devices.py +12 -7
  47. nautobot/dcim/signals.py +26 -0
  48. nautobot/dcim/templates/dcim/cable_trace.html +4 -4
  49. nautobot/dcim/templates/dcim/consoleport.html +14 -4
  50. nautobot/dcim/templates/dcim/consoleserverport.html +14 -4
  51. nautobot/dcim/templates/dcim/device/lldp_neighbors.html +3 -3
  52. nautobot/dcim/templates/dcim/frontport.html +7 -2
  53. nautobot/dcim/templates/dcim/interface.html +9 -4
  54. nautobot/dcim/templates/dcim/powerfeed.html +8 -3
  55. nautobot/dcim/templates/dcim/poweroutlet.html +14 -4
  56. nautobot/dcim/templates/dcim/powerport.html +14 -4
  57. nautobot/dcim/templates/dcim/rearport.html +7 -2
  58. nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +0 -62
  59. nautobot/dcim/templates/dcim/virtualdevicecontext_update.html +6 -0
  60. nautobot/dcim/tests/integration/test_fileinputpicker.py +87 -0
  61. nautobot/dcim/tests/test_api.py +176 -0
  62. nautobot/dcim/tests/test_filters.py +56 -3
  63. nautobot/dcim/tests/test_models.py +41 -1
  64. nautobot/dcim/views.py +24 -14
  65. nautobot/extras/api/mixins.py +1 -1
  66. nautobot/extras/api/views.py +4 -4
  67. nautobot/extras/filters/__init__.py +4 -0
  68. nautobot/extras/forms/forms.py +4 -0
  69. nautobot/extras/jobs.py +8 -1
  70. nautobot/extras/models/datasources.py +7 -3
  71. nautobot/extras/plugins/__init__.py +26 -1
  72. nautobot/extras/templates/extras/inc/jobresult.html +12 -13
  73. nautobot/extras/templates/extras/job.html +1 -0
  74. nautobot/extras/templates/extras/objectchange.html +28 -12
  75. nautobot/extras/tests/test_api.py +16 -15
  76. nautobot/extras/tests/test_dynamicgroups.py +14 -0
  77. nautobot/extras/tests/test_filters.py +2 -0
  78. nautobot/extras/tests/test_plugins.py +32 -1
  79. nautobot/extras/tests/test_views.py +209 -11
  80. nautobot/extras/utils.py +30 -0
  81. nautobot/extras/views.py +32 -14
  82. nautobot/ipam/api/serializers.py +7 -8
  83. nautobot/ipam/api/views.py +5 -5
  84. nautobot/ipam/factory.py +27 -8
  85. nautobot/ipam/filters.py +67 -29
  86. nautobot/ipam/formfields.py +51 -0
  87. nautobot/ipam/forms.py +15 -7
  88. nautobot/ipam/migrations/0051_added_optional_vrf_relationship_to_vdc.py +41 -0
  89. nautobot/ipam/models.py +63 -5
  90. nautobot/ipam/tables.py +21 -7
  91. nautobot/ipam/tests/test_api.py +107 -66
  92. nautobot/ipam/tests/test_filters.py +145 -5
  93. nautobot/ipam/tests/test_views.py +15 -2
  94. nautobot/project-static/bootstrap-filestyle-1.2.3/bootstrap-filestyle.min.js +11 -0
  95. nautobot/project-static/css/base.css +11 -0
  96. nautobot/project-static/css/dark.css +2 -1
  97. nautobot/project-static/docs/apps/index.html +1 -1
  98. nautobot/project-static/docs/apps/nautobot-apps.html +1 -1
  99. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +62 -0
  100. nautobot/project-static/docs/development/apps/api/configuration-view.html +0 -3
  101. nautobot/project-static/docs/development/apps/api/models/graphql.html +9 -13
  102. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +94 -1
  103. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +2 -5
  104. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +0 -3
  105. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +0 -3
  106. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +0 -3
  107. nautobot/project-static/docs/development/apps/api/prometheus.html +0 -3
  108. nautobot/project-static/docs/development/apps/api/setup.html +1 -1
  109. nautobot/project-static/docs/development/apps/api/testing.html +0 -6
  110. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +0 -3
  111. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +0 -3
  112. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +0 -3
  113. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +0 -3
  114. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +1 -7
  115. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +0 -7
  116. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +0 -4
  117. nautobot/project-static/docs/development/apps/api/views/notes.html +0 -3
  118. nautobot/project-static/docs/development/apps/index.html +2 -35
  119. nautobot/project-static/docs/development/apps/migration/code-updates.html +7 -6
  120. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +2 -2
  121. nautobot/project-static/docs/development/apps/migration/from-v1.html +3 -3
  122. nautobot/project-static/docs/development/core/application-registry.html +0 -6
  123. nautobot/project-static/docs/development/core/best-practices.html +1 -28
  124. nautobot/project-static/docs/development/core/bootstrap-ui.html +1 -1
  125. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +65 -11
  126. nautobot/project-static/docs/development/core/getting-started.html +14 -18
  127. nautobot/project-static/docs/development/core/homepage.html +0 -3
  128. nautobot/project-static/docs/development/core/index.html +1 -1
  129. nautobot/project-static/docs/development/core/minikube-dev-environment-for-k8s-jobs.html +3 -3
  130. nautobot/project-static/docs/development/core/model-checklist.html +1 -1
  131. nautobot/project-static/docs/development/core/navigation-menu.html +1 -1
  132. nautobot/project-static/docs/development/core/release-checklist.html +1 -1
  133. nautobot/project-static/docs/development/core/settings.html +1 -1
  134. nautobot/project-static/docs/development/core/style-guide.html +4 -9
  135. nautobot/project-static/docs/development/core/templates.html +0 -3
  136. nautobot/project-static/docs/development/core/testing.html +0 -9
  137. nautobot/project-static/docs/development/jobs/index.html +11 -30
  138. nautobot/project-static/docs/development/jobs/migration/from-v1.html +3 -2
  139. nautobot/project-static/docs/index.html +3 -2
  140. nautobot/project-static/docs/objects.inv +0 -0
  141. nautobot/project-static/docs/overview/application_stack.html +2 -20
  142. nautobot/project-static/docs/release-notes/version-1.0.html +2 -2
  143. nautobot/project-static/docs/release-notes/version-1.1.html +2 -2
  144. nautobot/project-static/docs/release-notes/version-1.2.html +3 -3
  145. nautobot/project-static/docs/release-notes/version-1.3.html +1 -1
  146. nautobot/project-static/docs/release-notes/version-1.4.html +17 -17
  147. nautobot/project-static/docs/release-notes/version-1.5.html +8 -8
  148. nautobot/project-static/docs/release-notes/version-1.6.html +4 -4
  149. nautobot/project-static/docs/release-notes/version-2.0.html +10 -10
  150. nautobot/project-static/docs/release-notes/version-2.1.html +7 -7
  151. nautobot/project-static/docs/release-notes/version-2.2.html +1 -1
  152. nautobot/project-static/docs/release-notes/version-2.3.html +4 -4
  153. nautobot/project-static/docs/release-notes/version-2.4.html +379 -0
  154. nautobot/project-static/docs/requirements.txt +1 -1
  155. nautobot/project-static/docs/search/search_index.json +1 -1
  156. nautobot/project-static/docs/sitemap.xml +290 -290
  157. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  158. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +3 -3
  159. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +4 -4
  160. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +1 -1
  161. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +3 -13
  162. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +5 -5
  163. nautobot/project-static/docs/user-guide/administration/guides/docker.html +3 -18
  164. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +1 -1
  165. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +4 -4
  166. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +15 -15
  167. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +2 -2
  168. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +1 -1
  169. nautobot/project-static/docs/user-guide/administration/installation/index.html +0 -16
  170. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +1 -1
  171. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +7 -10
  172. nautobot/project-static/docs/user-guide/administration/installation/services.html +1 -12
  173. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +3 -3
  174. nautobot/project-static/docs/user-guide/administration/security/index.html +1 -1
  175. nautobot/project-static/docs/user-guide/administration/security/notices.html +1 -0
  176. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +5 -35
  177. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +1 -1
  178. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/tables/v2-code-location-changes.yaml +1 -1
  179. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +12 -9
  180. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +0 -4
  181. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +0 -3
  182. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +0 -4
  183. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +0 -4
  184. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +0 -4
  185. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +0 -4
  186. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +0 -4
  187. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +0 -4
  188. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +0 -3
  189. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +0 -4
  190. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +0 -4
  191. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +0 -4
  192. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +1 -17
  193. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +0 -3
  194. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +0 -4
  195. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +0 -4
  196. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +0 -3
  197. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +1 -7
  198. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +0 -4
  199. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +0 -4
  200. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +0 -4
  201. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +0 -4
  202. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +0 -4
  203. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +0 -4
  204. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +0 -4
  205. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +0 -6
  206. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +0 -3
  207. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +0 -4
  208. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +0 -4
  209. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +0 -8
  210. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +15 -15
  211. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +1 -1
  212. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +0 -6
  213. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +0 -3
  214. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +3 -15
  215. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +1 -27
  216. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +0 -8
  217. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +3 -6
  218. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +0 -8
  219. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +0 -7
  220. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +0 -3
  221. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +0 -3
  222. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +2 -2
  223. nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +6 -6
  224. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +0 -14
  225. nautobot/project-static/docs/user-guide/platform-functionality/note.html +0 -3
  226. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +1 -10
  227. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +0 -3
  228. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +0 -14
  229. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +0 -19
  230. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +3 -9
  231. nautobot/project-static/docs/user-guide/platform-functionality/status.html +0 -8
  232. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +0 -4
  233. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +1 -13
  234. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +0 -5
  235. nautobot/project-static/js/dropdown.js +28 -0
  236. nautobot/project-static/js/editor.js +292 -0
  237. nautobot/project-static/monaco-editor-0.52.2/README.md +81 -0
  238. nautobot/project-static/monaco-editor-0.52.2/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
  239. nautobot/project-static/monaco-editor-0.52.2/vs/base/worker/workerMain.js +31 -0
  240. nautobot/project-static/monaco-editor-0.52.2/vs/basic-languages/xml/xml.js +10 -0
  241. nautobot/project-static/monaco-editor-0.52.2/vs/basic-languages/yaml/yaml.js +10 -0
  242. nautobot/project-static/monaco-editor-0.52.2/vs/editor/editor.main.css +8 -0
  243. nautobot/project-static/monaco-editor-0.52.2/vs/editor/editor.main.js +798 -0
  244. nautobot/project-static/monaco-editor-0.52.2/vs/language/json/jsonMode.js +19 -0
  245. nautobot/project-static/monaco-editor-0.52.2/vs/language/json/jsonWorker.js +42 -0
  246. nautobot/project-static/monaco-editor-0.52.2/vs/loader.js +11 -0
  247. nautobot/tenancy/filters/__init__.py +3 -5
  248. nautobot/tenancy/forms.py +9 -0
  249. nautobot/tenancy/templates/tenancy/tenant_create.html +21 -0
  250. nautobot/tenancy/templates/tenancy/tenant_edit.html +2 -21
  251. nautobot/tenancy/templates/tenancy/tenantgroup.html +2 -44
  252. nautobot/tenancy/templates/tenancy/tenantgroup_retrieve.html +1 -0
  253. nautobot/tenancy/tests/test_filters.py +10 -0
  254. nautobot/tenancy/tests/test_views.py +5 -1
  255. nautobot/tenancy/urls.py +7 -79
  256. nautobot/tenancy/views.py +51 -80
  257. nautobot/virtualization/views.py +0 -1
  258. nautobot/wireless/api/serializers.py +6 -1
  259. nautobot/wireless/api/views.py +3 -3
  260. nautobot/wireless/tables.py +9 -4
  261. nautobot/wireless/tests/test_api.py +5 -9
  262. {nautobot-2.4.2.dist-info → nautobot-2.4.4.dist-info}/METADATA +9 -9
  263. {nautobot-2.4.2.dist-info → nautobot-2.4.4.dist-info}/RECORD +267 -246
  264. {nautobot-2.4.2.dist-info → nautobot-2.4.4.dist-info}/LICENSE.txt +0 -0
  265. {nautobot-2.4.2.dist-info → nautobot-2.4.4.dist-info}/NOTICE +0 -0
  266. {nautobot-2.4.2.dist-info → nautobot-2.4.4.dist-info}/WHEEL +0 -0
  267. {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)
@@ -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
- tags = getattr(obj, "_tags", []) or obj.tags.all()
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.0 TODO: remove this template, which only exists for backward compatibility with 1.3 and earlier{% endcomment %}
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.0 TODO: remove this template, which only exists for backward compatibility with 1.3 and earlier{% endcomment %}
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.0 TODO: remove this template, which only exists for backward compatibility with 1.3 and earlier{% endcomment %}
2
+ {% comment %}3.0 TODO: remove this template, which only exists for backward compatibility with 2.4 and earlier{% endcomment %}
@@ -64,3 +64,8 @@
64
64
  </div>
65
65
  </form>
66
66
  {% endblock %}
67
+
68
+ {% block javascript %}
69
+ {{ block.super }}
70
+ {{ form.media }}
71
+ {% endblock %}
@@ -1,2 +1,2 @@
1
1
  {% extends 'generic/object_destroy.html' %}
2
- {% comment %}2.0 TODO: remove this template, which only exists for backward compatibility with 1.3 and earlier{% endcomment %}
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.0 TODO: remove this template, which only exists for backward compatibility with 1.3 and earlier{% endcomment %}
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.0 TODO: remove this template, which only exists for backward compatibility with 1.3 and earlier{% endcomment %}
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" if any from the base detail view associated with the context object.
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
- obj = get_obj_from_context(context)
36
- base_detail_view = get_view_for_model(obj)
37
- object_detail_content = getattr(base_detail_view, "object_detail_content", None)
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 ""
@@ -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.declared_filters.keys())
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
- search_box = self._fill_select2_field(field_name, value)
299
- search_box.first.type(Keys.ENTER)
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.browser.find_by_xpath(f"//li[contains(@class, 'select2-results__option') and text()='{value}']").click()
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"))
@@ -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.set([vlan_group_ct, vlan_ct])
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
- prefixes = Prefix.objects.all()[:2]
923
- cls.namespace = prefixes[0].namespace
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),
@@ -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
- csv_data = job_result.files.first().file.read().decode("utf-8")
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
- with self.subTest("Test app view."):
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.declared_filters:
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
  )