nautobot 2.2.4__py3-none-any.whl → 2.2.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of nautobot might be problematic. Click here for more details.

Files changed (365) hide show
  1. nautobot/apps/api.py +2 -0
  2. nautobot/apps/models.py +2 -0
  3. nautobot/circuits/forms.py +15 -0
  4. nautobot/circuits/navigation.py +9 -1
  5. nautobot/circuits/views.py +2 -0
  6. nautobot/core/api/fields.py +13 -0
  7. nautobot/core/api/serializers.py +7 -1
  8. nautobot/core/filters.py +11 -0
  9. nautobot/core/management/commands/generate_test_data.py +128 -158
  10. nautobot/core/models/fields.py +15 -0
  11. nautobot/core/settings.yaml +3 -3
  12. nautobot/core/testing/filters.py +24 -1
  13. nautobot/core/testing/views.py +13 -1
  14. nautobot/core/tests/test_utils.py +48 -1
  15. nautobot/core/utils/git.py +121 -49
  16. nautobot/core/utils/module_loading.py +10 -2
  17. nautobot/core/views/utils.py +18 -1
  18. nautobot/dcim/factory.py +1 -1
  19. nautobot/dcim/filters/__init__.py +1 -1
  20. nautobot/dcim/forms.py +23 -4
  21. nautobot/dcim/tables/devicetypes.py +15 -4
  22. nautobot/dcim/tests/test_models.py +2 -0
  23. nautobot/dcim/tests/test_views.py +84 -0
  24. nautobot/dcim/views.py +3 -0
  25. nautobot/extras/api/views.py +2 -2
  26. nautobot/extras/context_managers.py +3 -0
  27. nautobot/extras/datasources/git.py +133 -135
  28. nautobot/extras/datasources/utils.py +3 -0
  29. nautobot/extras/filters/__init__.py +16 -1
  30. nautobot/extras/forms/forms.py +49 -3
  31. nautobot/extras/forms/mixins.py +0 -6
  32. nautobot/extras/jobs.py +9 -1
  33. nautobot/extras/migrations/0107_laxurlfield.py +28 -0
  34. nautobot/extras/migrations/0108_jobbutton_enabled.py +17 -0
  35. nautobot/extras/models/datasources.py +6 -4
  36. nautobot/extras/models/jobs.py +30 -0
  37. nautobot/extras/models/models.py +2 -4
  38. nautobot/extras/signals.py +6 -1
  39. nautobot/extras/tables.py +3 -0
  40. nautobot/extras/templates/extras/jobbutton_retrieve.html +6 -2
  41. nautobot/extras/templatetags/job_buttons.py +2 -2
  42. nautobot/extras/tests/git_data/01-valid-files/__init__.py +0 -0
  43. nautobot/extras/tests/git_data/01-valid-files/config_context_schemas/schema-1.yaml +18 -0
  44. nautobot/extras/tests/git_data/01-valid-files/config_contexts/context.yaml +12 -0
  45. nautobot/extras/tests/git_data/01-valid-files/config_contexts/devices/test-device.json +3 -0
  46. nautobot/extras/tests/git_data/01-valid-files/config_contexts/locations/Test Location.json +7 -0
  47. nautobot/extras/tests/git_data/01-valid-files/export_templates/dcim/device/template.j2 +3 -0
  48. nautobot/extras/tests/git_data/01-valid-files/export_templates/dcim/device/template2.html +4 -0
  49. nautobot/extras/tests/git_data/01-valid-files/export_templates/ipam/vlan/template.j2 +3 -0
  50. nautobot/extras/tests/git_data/01-valid-files/jobs/__init__.py +5 -0
  51. nautobot/extras/tests/git_data/01-valid-files/jobs/my_job.py +16 -0
  52. nautobot/extras/tests/git_data/02-invalid-files/__init__.py +0 -0
  53. nautobot/extras/tests/git_data/02-invalid-files/config_context_schemas/badschema1.json +2 -0
  54. nautobot/extras/tests/git_data/02-invalid-files/config_context_schemas/badschema2.json +1 -0
  55. nautobot/extras/tests/git_data/02-invalid-files/config_contexts/badcontext1.json +2 -0
  56. nautobot/extras/tests/git_data/02-invalid-files/config_contexts/badcontext2.json +1 -0
  57. nautobot/extras/tests/git_data/02-invalid-files/config_contexts/badcontext3.json +3 -0
  58. nautobot/extras/tests/git_data/02-invalid-files/config_contexts/devices/nosuchdevice.json +1 -0
  59. nautobot/extras/tests/git_data/02-invalid-files/dcim/template.j2 +0 -0
  60. nautobot/extras/tests/git_data/02-invalid-files/devices/template.j2 +0 -0
  61. nautobot/extras/tests/git_data/02-invalid-files/export_templates/dcim/nosuchmodel/template.j2 +3 -0
  62. nautobot/extras/tests/git_data/02-invalid-files/export_templates/nosuchapp/device/template.j2 +3 -0
  63. nautobot/extras/tests/git_data/02-invalid-files/jobs/__init__.py +2 -0
  64. nautobot/extras/tests/git_data/02-invalid-files/jobs/importerror.py +1 -0
  65. nautobot/extras/tests/git_data/02-invalid-files/jobs/syntaxerror.py +1 -0
  66. nautobot/extras/tests/git_helper.py +76 -0
  67. nautobot/extras/tests/test_api.py +52 -13
  68. nautobot/extras/tests/test_context_managers.py +33 -1
  69. nautobot/extras/tests/test_datasources.py +94 -276
  70. nautobot/extras/tests/test_filters.py +69 -0
  71. nautobot/extras/tests/test_forms.py +0 -3
  72. nautobot/extras/tests/test_models.py +8 -3
  73. nautobot/extras/tests/test_views.py +69 -11
  74. nautobot/extras/views.py +12 -10
  75. nautobot/ipam/filters.py +9 -1
  76. nautobot/ipam/forms.py +26 -0
  77. nautobot/ipam/tables.py +1 -1
  78. nautobot/ipam/tests/test_filters.py +15 -0
  79. nautobot/ipam/tests/test_views.py +9 -2
  80. nautobot/ipam/views.py +11 -0
  81. nautobot/project-static/docs/404.html +84 -9
  82. nautobot/project-static/docs/apps/index.html +97 -11
  83. nautobot/project-static/docs/apps/nautobot-apps.html +97 -11
  84. nautobot/project-static/docs/assets/app-icons/icon-CapacityMetrics.svg +1 -0
  85. nautobot/project-static/docs/assets/app-icons/icon-CircuitMaintenance.png +0 -0
  86. nautobot/project-static/docs/assets/javascripts/{bundle.3220b9d7.min.js → bundle.ad660dcc.min.js} +6 -6
  87. nautobot/project-static/docs/assets/javascripts/{bundle.3220b9d7.min.js.map → bundle.ad660dcc.min.js.map} +3 -3
  88. nautobot/project-static/docs/assets/javascripts/glightbox.min.js +1 -0
  89. nautobot/project-static/docs/assets/stylesheets/glightbox.min.css +1 -0
  90. nautobot/project-static/docs/assets/stylesheets/{main.66ac8b77.min.css → main.6543a935.min.css} +1 -1
  91. nautobot/project-static/docs/assets/stylesheets/{main.66ac8b77.min.css.map → main.6543a935.min.css.map} +1 -1
  92. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +97 -11
  93. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +97 -11
  94. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +97 -11
  95. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +97 -11
  96. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +97 -11
  97. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +97 -11
  98. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +97 -11
  99. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +97 -11
  100. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +97 -11
  101. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +97 -11
  102. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +97 -11
  103. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +97 -11
  104. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +97 -11
  105. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +97 -11
  106. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +157 -13
  107. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +97 -11
  108. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +97 -11
  109. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +97 -11
  110. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +107 -12
  111. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +97 -11
  112. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +97 -11
  113. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +144 -12
  114. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +97 -11
  115. nautobot/project-static/docs/development/apps/api/configuration-view.html +97 -11
  116. nautobot/project-static/docs/development/apps/api/database-backend-config.html +97 -11
  117. nautobot/project-static/docs/development/apps/api/models/django-admin.html +97 -11
  118. nautobot/project-static/docs/development/apps/api/models/global-search.html +97 -11
  119. nautobot/project-static/docs/development/apps/api/models/graphql.html +97 -11
  120. nautobot/project-static/docs/development/apps/api/models/index.html +97 -11
  121. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +97 -11
  122. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +97 -11
  123. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +98 -12
  124. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +97 -11
  125. nautobot/project-static/docs/development/apps/api/platform-features/index.html +97 -11
  126. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +97 -11
  127. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +97 -11
  128. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +97 -11
  129. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +97 -11
  130. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +97 -11
  131. nautobot/project-static/docs/development/apps/api/prometheus.html +97 -11
  132. nautobot/project-static/docs/development/apps/api/setup.html +97 -11
  133. nautobot/project-static/docs/development/apps/api/testing.html +97 -11
  134. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +97 -11
  135. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +97 -11
  136. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +97 -11
  137. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +97 -11
  138. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +97 -11
  139. nautobot/project-static/docs/development/apps/api/views/base-template.html +97 -11
  140. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +97 -11
  141. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +97 -11
  142. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +97 -11
  143. nautobot/project-static/docs/development/apps/api/views/index.html +97 -11
  144. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +97 -11
  145. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +97 -11
  146. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +97 -11
  147. nautobot/project-static/docs/development/apps/api/views/notes.html +97 -11
  148. nautobot/project-static/docs/development/apps/api/views/rest-api.html +97 -11
  149. nautobot/project-static/docs/development/apps/api/views/urls.html +97 -11
  150. nautobot/project-static/docs/development/apps/index.html +97 -11
  151. nautobot/project-static/docs/development/apps/migration/code-updates.html +98 -12
  152. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +97 -11
  153. nautobot/project-static/docs/development/apps/migration/from-v1.html +99 -13
  154. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +97 -11
  155. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +97 -11
  156. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +97 -11
  157. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +97 -11
  158. nautobot/project-static/docs/development/apps/porting-from-netbox.html +97 -11
  159. nautobot/project-static/docs/development/core/application-registry.html +98 -12
  160. nautobot/project-static/docs/development/core/best-practices.html +97 -11
  161. nautobot/project-static/docs/development/core/bootstrap-ui.html +97 -11
  162. nautobot/project-static/docs/development/core/caching.html +97 -11
  163. nautobot/project-static/docs/development/core/controllers.html +97 -11
  164. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +97 -11
  165. nautobot/project-static/docs/development/core/generic-views.html +97 -11
  166. nautobot/project-static/docs/development/core/getting-started.html +99 -13
  167. nautobot/project-static/docs/development/core/homepage.html +97 -11
  168. nautobot/project-static/docs/development/core/index.html +97 -11
  169. nautobot/project-static/docs/development/core/model-checklist.html +97 -11
  170. nautobot/project-static/docs/development/core/model-features.html +97 -11
  171. nautobot/project-static/docs/development/core/natural-keys.html +97 -11
  172. nautobot/project-static/docs/development/core/navigation-menu.html +97 -11
  173. nautobot/project-static/docs/development/core/release-checklist.html +97 -11
  174. nautobot/project-static/docs/development/core/role-internals.html +97 -11
  175. nautobot/project-static/docs/development/core/settings.html +97 -11
  176. nautobot/project-static/docs/development/core/style-guide.html +97 -11
  177. nautobot/project-static/docs/development/core/templates.html +97 -11
  178. nautobot/project-static/docs/development/core/testing.html +98 -12
  179. nautobot/project-static/docs/development/core/user-preferences.html +97 -11
  180. nautobot/project-static/docs/development/index.html +97 -11
  181. nautobot/project-static/docs/development/jobs/index.html +97 -11
  182. nautobot/project-static/docs/development/jobs/migration/from-v1.html +97 -11
  183. nautobot/project-static/docs/index.html +13 -8362
  184. nautobot/project-static/docs/objects.inv +0 -0
  185. nautobot/project-static/docs/overview/application_stack.html +8229 -0
  186. nautobot/project-static/docs/overview/design_philosophy.html +8158 -0
  187. nautobot/project-static/docs/overview/index.html +8230 -0
  188. nautobot/project-static/docs/release-notes/index.html +97 -11
  189. nautobot/project-static/docs/release-notes/version-1.0.html +98 -12
  190. nautobot/project-static/docs/release-notes/version-1.1.html +97 -11
  191. nautobot/project-static/docs/release-notes/version-1.2.html +99 -13
  192. nautobot/project-static/docs/release-notes/version-1.3.html +98 -12
  193. nautobot/project-static/docs/release-notes/version-1.4.html +99 -13
  194. nautobot/project-static/docs/release-notes/version-1.5.html +99 -13
  195. nautobot/project-static/docs/release-notes/version-1.6.html +626 -160
  196. nautobot/project-static/docs/release-notes/version-2.0.html +100 -14
  197. nautobot/project-static/docs/release-notes/version-2.1.html +97 -11
  198. nautobot/project-static/docs/release-notes/version-2.2.html +546 -107
  199. nautobot/project-static/docs/requirements.txt +3 -2
  200. nautobot/project-static/docs/search/search_index.json +1 -1
  201. nautobot/project-static/docs/sitemap.xml +268 -258
  202. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  203. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +97 -11
  204. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +97 -11
  205. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +99 -13
  206. nautobot/project-static/docs/user-guide/administration/configuration/index.html +98 -12
  207. nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +100 -14
  208. nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +97 -11
  209. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +97 -11
  210. nautobot/project-static/docs/user-guide/administration/guides/caching.html +97 -11
  211. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +97 -11
  212. nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +97 -11
  213. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +97 -11
  214. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +97 -11
  215. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +98 -12
  216. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +97 -11
  217. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +97 -11
  218. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +97 -11
  219. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +97 -11
  220. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +97 -11
  221. nautobot/project-static/docs/user-guide/administration/installation/index.html +97 -11
  222. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +97 -11
  223. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +101 -15
  224. nautobot/project-static/docs/user-guide/administration/installation/services.html +98 -12
  225. nautobot/project-static/docs/user-guide/administration/installation-extras/docker.html +97 -11
  226. nautobot/project-static/docs/user-guide/administration/installation-extras/health-checks.html +97 -11
  227. nautobot/project-static/docs/user-guide/administration/installation-extras/selinux-troubleshooting.html +97 -11
  228. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +97 -11
  229. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +97 -11
  230. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +97 -11
  231. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +97 -11
  232. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +97 -11
  233. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +97 -11
  234. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +97 -11
  235. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +98 -12
  236. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +97 -11
  237. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +97 -11
  238. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +97 -30
  239. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/tables/v2-code-location-changes.yaml +1 -1
  240. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +100 -14
  241. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +97 -11
  242. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +97 -11
  243. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +97 -11
  244. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +97 -11
  245. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +97 -11
  246. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +97 -11
  247. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +97 -11
  248. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +97 -11
  249. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +97 -11
  250. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +97 -11
  251. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +97 -11
  252. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +97 -11
  253. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +97 -11
  254. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +99 -13
  255. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +97 -11
  256. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +97 -11
  257. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +97 -11
  258. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +97 -11
  259. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +98 -12
  260. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +97 -11
  261. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +97 -11
  262. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +97 -11
  263. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +97 -11
  264. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +97 -11
  265. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +97 -11
  266. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +97 -11
  267. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +97 -11
  268. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +97 -11
  269. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +97 -11
  270. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +97 -11
  271. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +97 -11
  272. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +97 -11
  273. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +97 -11
  274. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +97 -11
  275. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +97 -11
  276. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +97 -11
  277. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +97 -11
  278. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +97 -11
  279. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +97 -11
  280. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +97 -11
  281. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +97 -11
  282. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +97 -11
  283. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +97 -11
  284. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +97 -11
  285. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +97 -11
  286. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +97 -11
  287. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +97 -11
  288. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +97 -11
  289. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +97 -11
  290. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +97 -11
  291. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +97 -11
  292. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +97 -11
  293. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +97 -11
  294. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +97 -11
  295. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +97 -11
  296. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +97 -11
  297. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +97 -11
  298. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +97 -11
  299. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +97 -11
  300. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +97 -11
  301. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +97 -11
  302. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +97 -11
  303. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +97 -11
  304. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +97 -11
  305. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +97 -11
  306. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +97 -11
  307. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +100 -14
  308. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +97 -11
  309. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +97 -11
  310. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +98 -12
  311. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +97 -11
  312. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +97 -11
  313. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +97 -11
  314. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +97 -11
  315. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +98 -12
  316. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +97 -11
  317. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +97 -11
  318. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +97 -11
  319. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +97 -11
  320. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +97 -11
  321. nautobot/project-static/docs/user-guide/index.html +100 -14
  322. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +97 -11
  323. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +97 -11
  324. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +97 -11
  325. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +97 -11
  326. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +97 -11
  327. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +97 -11
  328. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +97 -11
  329. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +97 -11
  330. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +97 -11
  331. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +97 -11
  332. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +97 -11
  333. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +98 -12
  334. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +98 -12
  335. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +102 -15
  336. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +97 -11
  337. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +99 -13
  338. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +99 -13
  339. nautobot/project-static/docs/user-guide/platform-functionality/note.html +97 -11
  340. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +97 -11
  341. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +97 -11
  342. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +97 -11
  343. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +97 -11
  344. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +97 -11
  345. nautobot/project-static/docs/user-guide/platform-functionality/role.html +98 -44
  346. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +97 -11
  347. nautobot/project-static/docs/user-guide/platform-functionality/status.html +97 -11
  348. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +97 -11
  349. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +97 -11
  350. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +97 -11
  351. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +97 -11
  352. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +97 -11
  353. nautobot/project-static/js/forms.js +2 -1
  354. nautobot/tenancy/forms.py +9 -0
  355. nautobot/tenancy/views.py +1 -0
  356. nautobot/virtualization/forms.py +18 -6
  357. nautobot/virtualization/templates/virtualization/clustertype.html +2 -2
  358. nautobot/virtualization/views.py +2 -0
  359. {nautobot-2.2.4.dist-info → nautobot-2.2.6.dist-info}/METADATA +1 -1
  360. {nautobot-2.2.4.dist-info → nautobot-2.2.6.dist-info}/RECORD +364 -331
  361. nautobot/extras/tests/test_git.py +0 -23
  362. {nautobot-2.2.4.dist-info → nautobot-2.2.6.dist-info}/LICENSE.txt +0 -0
  363. {nautobot-2.2.4.dist-info → nautobot-2.2.6.dist-info}/NOTICE +0 -0
  364. {nautobot-2.2.4.dist-info → nautobot-2.2.6.dist-info}/WHEEL +0 -0
  365. {nautobot-2.2.4.dist-info → nautobot-2.2.6.dist-info}/entry_points.txt +0 -0
@@ -213,6 +213,8 @@ class ViewTestCases:
213
213
  escape(str(instance.cf.get(custom_field.key) or "")), response_body, msg=response_body
214
214
  )
215
215
 
216
+ return response # for consumption by child test cases if desired
217
+
216
218
  @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
217
219
  def test_get_object_with_constrained_permission(self):
218
220
  instance1, instance2 = self._get_queryset().all()[:2]
@@ -230,11 +232,14 @@ class ViewTestCases:
230
232
  obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
231
233
 
232
234
  # Try GET to permitted object
233
- self.assertHttpStatus(self.client.get(instance1.get_absolute_url()), 200)
235
+ response = self.client.get(instance1.get_absolute_url())
236
+ self.assertHttpStatus(response, 200)
234
237
 
235
238
  # Try GET to non-permitted object
236
239
  self.assertHttpStatus(self.client.get(instance2.get_absolute_url()), 404)
237
240
 
241
+ return response # for consumption by child test cases if desired
242
+
238
243
  @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
239
244
  def test_has_advanced_tab(self):
240
245
  instance = self._get_queryset().first()
@@ -740,6 +745,13 @@ class ViewTestCases:
740
745
  def get_list_view(self):
741
746
  return lookup.get_view_for_model(self.model, view_type="List")
742
747
 
748
+ def test_list_view_has_filter_form(self):
749
+ view = self.get_list_view()
750
+ if hasattr(view, "filterset_form"): # ObjectListView
751
+ self.assertIsNotNone(view.filterset_form, "List view lacks a FilterForm")
752
+ if hasattr(view, "filterset_form_class"): # ObjectListViewMixin
753
+ self.assertIsNotNone(view.filterset_form_class, "List viewset lacks a FilterForm")
754
+
743
755
  def test_table_with_indentation_is_removed_on_filter_or_sort(self):
744
756
  self.user.is_superuser = True
745
757
  self.user.save()
@@ -4,6 +4,7 @@ from django import forms as django_forms
4
4
  from django.apps import apps
5
5
  from django.contrib.auth.models import Group
6
6
  from django.contrib.contenttypes.models import ContentType
7
+ from django.core.exceptions import ValidationError
7
8
  from django.db.models import Q
8
9
  from django.http import QueryDict
9
10
  from django.test import TestCase
@@ -11,7 +12,7 @@ from django.test import TestCase
11
12
  from nautobot.circuits import models as circuits_models
12
13
  from nautobot.core import exceptions, forms, settings_funcs
13
14
  from nautobot.core.api import utils as api_utils
14
- from nautobot.core.models import fields as core_fields, utils as models_utils
15
+ from nautobot.core.models import fields as core_fields, utils as models_utils, validators
15
16
  from nautobot.core.utils import data as data_utils, filtering, lookup, requests
16
17
  from nautobot.core.utils.migrations import update_object_change_ct_for_replaced_models
17
18
  from nautobot.dcim import filters as dcim_filters, forms as dcim_forms, models as dcim_models, tables
@@ -372,6 +373,52 @@ class SlugifyFunctionsTest(TestCase):
372
373
  self.assertEqual(core_fields.slugify_dashes_to_underscores(content), expected)
373
374
 
374
375
 
376
+ class LaxURLFieldTest(TestCase):
377
+ """Test LaxURLField and related functionality."""
378
+
379
+ VALID_URLS = [
380
+ "http://example.com",
381
+ "https://local-dns/foo/bar.git", # not supported out-of-the-box by Django, hence our custom classes
382
+ "https://1.1.1.1:8080/",
383
+ "https://[2001:db8::]/",
384
+ ]
385
+ INVALID_URLS = [
386
+ "unknown://example.com/",
387
+ "foo:/",
388
+ "http://file://",
389
+ ]
390
+
391
+ def test_enhanced_url_validator(self):
392
+ for valid in self.VALID_URLS:
393
+ with self.subTest(valid=valid):
394
+ validators.EnhancedURLValidator()(valid)
395
+
396
+ for invalid in self.INVALID_URLS:
397
+ with self.subTest(invalid=invalid):
398
+ with self.assertRaises(django_forms.ValidationError):
399
+ validators.EnhancedURLValidator()(invalid)
400
+
401
+ def test_forms_lax_url_field(self):
402
+ for valid in self.VALID_URLS:
403
+ with self.subTest(valid=valid):
404
+ forms.LaxURLField().clean(valid)
405
+
406
+ for invalid in self.INVALID_URLS:
407
+ with self.subTest(invalid=invalid):
408
+ with self.assertRaises(django_forms.ValidationError):
409
+ forms.LaxURLField().clean(invalid)
410
+
411
+ def test_models_lax_url_field(self):
412
+ for valid in self.VALID_URLS:
413
+ with self.subTest(valid=valid):
414
+ core_fields.LaxURLField().run_validators(valid)
415
+
416
+ for invalid in self.INVALID_URLS:
417
+ with self.subTest(invalid=invalid):
418
+ with self.assertRaises(ValidationError):
419
+ core_fields.LaxURLField().run_validators(invalid)
420
+
421
+
375
422
  class LookupRelatedFunctionTest(TestCase):
376
423
  def test_is_single_choice_field(self):
377
424
  """
@@ -3,6 +3,7 @@
3
3
  from collections import namedtuple
4
4
  import logging
5
5
  import os
6
+ import string
6
7
 
7
8
  from git import Repo
8
9
 
@@ -34,8 +35,8 @@ GIT_ENVIRONMENT = {
34
35
 
35
36
  def swap_status_initials(data):
36
37
  """Swap Git status initials with its equivalent."""
37
- initial, text = data.split("\t")
38
- return GitDiffLog(status=GIT_STATUS_MAP.get(initial), text=text)
38
+ initial, text = data.split("\t", 1)
39
+ return GitDiffLog(status=GIT_STATUS_MAP.get(initial[0]), text=text)
39
40
 
40
41
 
41
42
  def convert_git_diff_log_to_list(logs):
@@ -93,69 +94,140 @@ class GitRepo:
93
94
  """
94
95
  Check out the given branch, and optionally the specified commit within that branch.
95
96
 
97
+ Args:
98
+ branch (str): A branch name, a tag name, or a (possibly abbreviated) commit identifier.
99
+ commit_hexsha (str): A specific (possibly abbreviated) commit identifier.
100
+
101
+ If `commit_hexsha` is specified and `branch` is either a tag or a commit identifier, they must match.
102
+ If `commit_hexsha` is specified and `branch` is a branch name, it must contain the specified commit.
103
+
96
104
  Returns:
97
- (str, bool): commit_hexsha the repo contains now, whether any change occurred
105
+ (str, bool): commit_hexsha the repo contains now, and whether any change occurred
98
106
  """
99
107
  # Short-circuit logic - do we already have this commit checked out?
100
- if commit_hexsha and commit_hexsha == self.head:
108
+ if commit_hexsha and self.head.startswith(commit_hexsha):
101
109
  logger.debug(f"Commit {commit_hexsha} is already checked out.")
102
- return (commit_hexsha, False)
110
+ return (self.head, False)
111
+ # User might specify the commit as a "branch" name...
112
+ if not commit_hexsha and set(branch).issubset(string.hexdigits) and self.head.startswith(branch):
113
+ logger.debug("Commit %s is already checked out.", branch)
114
+ return (self.head, False)
103
115
 
104
116
  self.fetch()
105
- if commit_hexsha:
106
- # Sanity check - GitPython doesn't provide a handy API for this so we just call a raw Git command:
107
- # $ git branch origin/<branch> --remotes --contains <commit>
108
- # prints the branch name if it DOES contain the commit, and nothing if it DOES NOT contain the commit.
109
- # Since we did a `fetch` and not a `pull` above, we need to check for the commit in the remote origin
110
- # branch, not the local (not-yet-updated) branch.
111
- if branch not in self.repo.git.branch(f"origin/{branch}", "--remotes", "--contains", commit_hexsha):
112
- raise RuntimeError(f"Requested to check out commit `{commit_hexsha}`, but it's not in branch {branch}!")
113
- logger.info(f"Checking out commit `{commit_hexsha}` on branch `{branch}`...")
114
- self.repo.git.checkout(commit_hexsha)
115
- return (commit_hexsha, True)
116
-
117
- if branch in self.repo.heads:
118
- branch_head = self.repo.heads[branch]
119
- else:
120
- try:
121
- branch_head = self.repo.create_head(branch, self.repo.remotes.origin.refs[branch])
122
- branch_head.set_tracking_branch(self.repo.remotes.origin.refs[branch])
123
- except IndexError as git_error:
124
- logger.error(
125
- "Branch %s does not exist at %s. %s", branch, next(iter(self.repo.remotes.origin.urls)), git_error
126
- )
127
- raise BranchDoesNotExist(
128
- f"Please create branch '{branch}' in upstream and try again."
129
- f" If this is a new repo, please add a commit before syncing. {git_error}"
117
+ # Is `branch` actually a branch, a tag, or a commit? Heuristics:
118
+ is_branch = branch in self.repo.remotes.origin.refs
119
+ is_tag = branch in self.repo.tags
120
+ maybe_commit = set(branch).issubset(string.hexdigits)
121
+ logger.debug(
122
+ "Branch %s --> is_branch: %s, is_tag: %s, maybe_commit: %s",
123
+ branch,
124
+ is_branch,
125
+ is_tag,
126
+ maybe_commit,
127
+ )
128
+
129
+ if is_branch:
130
+ if commit_hexsha:
131
+ # Sanity check - GitPython doesn't provide a handy API for this so we just call a raw Git command:
132
+ # $ git branch origin/<branch> --remotes --contains <commit>
133
+ # prints the branch name if it DOES contain the commit, and nothing if it DOES NOT contain the commit.
134
+ # Since we did a `fetch` and not a `pull` above, we need to check for the commit in the remote origin
135
+ # branch, not the local (not-yet-updated) branch.
136
+ if branch not in self.repo.git.branch(f"origin/{branch}", "--remotes", "--contains", commit_hexsha):
137
+ raise RuntimeError(
138
+ f"Requested to check out commit {commit_hexsha}, but it's not part of branch {branch}!"
139
+ )
140
+ logger.info("Checking out commit %s on branch %s...", commit_hexsha, branch)
141
+ self.repo.git.checkout(commit_hexsha)
142
+ return (self.head, True)
143
+
144
+ if branch in self.repo.heads:
145
+ branch_head = self.repo.heads[branch]
146
+ else:
147
+ try:
148
+ branch_head = self.repo.create_head(branch, self.repo.remotes.origin.refs[branch])
149
+ branch_head.set_tracking_branch(self.repo.remotes.origin.refs[branch])
150
+ except IndexError as git_error:
151
+ logger.error(
152
+ "Branch %s does not exist at %s. %s",
153
+ branch,
154
+ next(iter(self.repo.remotes.origin.urls)),
155
+ git_error,
156
+ )
157
+ raise BranchDoesNotExist(
158
+ f"Please create branch '{branch}' in upstream and try again."
159
+ f" If this is a new repo, please add a commit before syncing. {git_error}"
160
+ )
161
+
162
+ logger.info("Checking out latest commit on branch %s...", branch)
163
+ branch_head.checkout()
164
+ # No specific commit hash was given, so make sure we get the latest from origin
165
+ # We would use repo.remotes.origin.pull() here, but that will fail in the case where someone has
166
+ # force-pushed to the upstream repo since the last time we did a pull. To be safe, we reset instead.
167
+ self.repo.head.reset(f"origin/{branch}", index=True, working_tree=True)
168
+ logger.info("Latest commit on branch `%s` is `%s`", branch, self.head)
169
+ return (self.head, True)
170
+
171
+ if is_tag:
172
+ tag = self.repo.tags[branch]
173
+ if commit_hexsha:
174
+ # Sanity check
175
+ if not tag.commit.hexsha.startswith(commit_hexsha):
176
+ raise RuntimeError(
177
+ f"Requested to check out tag {branch} and commit {commit_hexsha} together, "
178
+ f"but tag {branch} is actually commit {tag.commit.hexsha}!"
179
+ )
180
+ logger.info("Checking out tag %s...", branch)
181
+ self.repo.git.checkout(branch)
182
+ return (self.head, True)
183
+
184
+ if maybe_commit:
185
+ # Sanity check
186
+ if commit_hexsha and not (commit_hexsha.startswith(branch) or branch.startswith(commit_hexsha)):
187
+ raise RuntimeError(
188
+ f"Requested to check out both {branch} and {commit_hexsha} together, "
189
+ f"but {branch} is neither a branch, a tag, nor the same commit hash!"
130
190
  )
131
191
 
132
- logger.info(f"Checking out latest commit on branch `{branch}`...")
133
- branch_head.checkout()
134
- # No specific commit hash was given, so make sure we get the latest from origin
135
- # We would use repo.remotes.origin.pull() here, but that will fail in the case where someone has
136
- # force-pushed to the upstream repo since the last time we did a pull. To be safe, we reset instead.
137
- self.repo.head.reset(f"origin/{branch}", index=True, working_tree=True)
138
- commit_hexsha = self.repo.head.reference.commit.hexsha
139
- logger.info(f"Latest commit on branch `{branch}` is `{commit_hexsha}`")
140
- return (commit_hexsha, True)
192
+ logger.info("Checking out commit %s...", branch)
193
+ self.repo.git.checkout(branch)
194
+ return (self.head, True)
195
+
196
+ # Fallthru
197
+ raise BranchDoesNotExist(
198
+ f"{branch} does not appear to be an existing branch, tag, or possible commit hash. "
199
+ "Please check your upstream repository and the data you are using."
200
+ )
141
201
 
142
202
  def diff_remote(self, branch):
143
203
  logger.debug("Fetching from remote.")
144
204
  self.fetch()
145
-
146
- try:
147
- self.repo.remotes.origin.refs[branch]
148
- except IndexError as git_error:
149
- logger.error(
150
- "Branch %s does not exist at %s. %s", branch, next(iter(self.repo.remotes.origin.urls)), git_error
151
- )
205
+ # Is `branch` actually a branch, a tag, or a commit? Heuristics:
206
+ is_branch = branch in self.repo.remotes.origin.refs
207
+ is_tag = branch in self.repo.tags
208
+ maybe_commit = set(branch).issubset(string.hexdigits)
209
+ logger.debug(
210
+ "Branch %s --> is_branch: %s, is_tag: %s, maybe_commit: %s",
211
+ branch,
212
+ is_branch,
213
+ is_tag,
214
+ maybe_commit,
215
+ )
216
+
217
+ if not is_branch and not is_tag and not maybe_commit:
218
+ logger.error("Branch %s does not exist at %s", branch, next(iter(self.repo.remotes.origin.urls)))
152
219
  raise BranchDoesNotExist(
153
220
  f"Please create branch '{branch}' in upstream and try again."
154
- f" If this is a new repo, please add a commit before syncing. {git_error}"
221
+ f" If this is a new repo, please add a commit before syncing."
155
222
  )
156
223
 
157
- logger.debug("Getting diff between local branch and remote branch")
158
- diff = self.repo.git.diff("--name-status", f"origin/{branch}")
224
+ if is_branch:
225
+ logger.debug("Getting diff between local branch and remote branch")
226
+ diff = self.repo.git.diff("--name-status", f"origin/{branch}")
227
+ else:
228
+ logger.debug("Getting diff between local state and specified tag or commit")
229
+ diff = self.repo.git.diff("--name-status", branch)
230
+
159
231
  if diff: # if diff is not empty
160
232
  return convert_git_diff_log_to_list(diff)
161
233
  logger.debug("No Difference")
@@ -24,6 +24,15 @@ def _temporarily_add_to_sys_path(path):
24
24
  sys.path = old_sys_path
25
25
 
26
26
 
27
+ def clear_module_from_sys_modules(module_name):
28
+ """
29
+ Remove the module and all its submodules from sys.modules.
30
+ """
31
+ for name in list(sys.modules.keys()):
32
+ if name == module_name or name.startswith(f"{module_name}."):
33
+ del sys.modules[name]
34
+
35
+
27
36
  def import_modules_privately(path, module_path=None, ignore_import_errors=True):
28
37
  """
29
38
  Import modules from the filesystem without adding the path permanently to `sys.path`.
@@ -69,7 +78,7 @@ def import_modules_privately(path, module_path=None, ignore_import_errors=True):
69
78
  continue
70
79
 
71
80
  if discovered_module_name in sys.modules:
72
- del sys.modules[discovered_module_name]
81
+ clear_module_from_sys_modules(discovered_module_name)
73
82
 
74
83
  try:
75
84
  if not is_package:
@@ -81,7 +90,6 @@ def import_modules_privately(path, module_path=None, ignore_import_errors=True):
81
90
  spec.loader.exec_module(module)
82
91
  else:
83
92
  module = importlib.import_module(discovered_module_name)
84
-
85
93
  importlib.reload(module)
86
94
  except Exception as exc:
87
95
  logger.error("Unable to load module %s from %s: %s", discovered_module_name, path, exc)
@@ -215,11 +215,28 @@ def handle_protectederror(obj_list, request, e):
215
215
  protected_count,
216
216
  )
217
217
 
218
+ # Format objects based on whether they have a detail view/absolute url
219
+ objects_with_absolute_url = []
220
+ objects_without_absolute_url = []
218
221
  # Append dependent objects to error message
222
+ for dependent in protected_objects[:50]:
223
+ try:
224
+ dependent.get_absolute_url()
225
+ objects_with_absolute_url.append(dependent)
226
+ except AttributeError:
227
+ objects_without_absolute_url.append(dependent)
228
+
219
229
  err_message += format_html_join(
220
230
  ", ",
221
231
  '<a href="{}">{}</a>',
222
- ((dependent.get_absolute_url(), dependent) for dependent in protected_objects[:50]),
232
+ ((dependent.get_absolute_url(), dependent) for dependent in objects_with_absolute_url),
233
+ )
234
+ if objects_with_absolute_url and objects_without_absolute_url:
235
+ err_message += format_html(", ")
236
+ err_message += format_html_join(
237
+ ", ",
238
+ "<span>{}</span>",
239
+ ((dependent,) for dependent in objects_without_absolute_url),
223
240
  )
224
241
 
225
242
  messages.error(request, err_message)
nautobot/dcim/factory.py CHANGED
@@ -708,6 +708,6 @@ class ControllerManagedDeviceGroupFactory(PrimaryModelFactory):
708
708
  name = UniqueFaker("word")
709
709
  parent = factory.Maybe("has_parent", random_instance(ControllerManagedDeviceGroup), None)
710
710
  controller = factory.LazyAttribute(
711
- lambda o: o.parent.controller if o.parent else Controller.objects.order_by("?").first()
711
+ lambda o: o.parent.controller if o.parent else factory.random.randgen.choice(Controller.objects.all())
712
712
  )
713
713
  weight = factory.Faker("pyint", min_value=1, max_value=1000)
@@ -102,13 +102,13 @@ __all__ = (
102
102
  "ControllerManagedDeviceGroupFilterSet",
103
103
  "DeviceBayFilterSet",
104
104
  "DeviceBayTemplateFilterSet",
105
+ "DeviceFamilyFilterSet",
105
106
  "DeviceFilterSet",
106
107
  "DeviceRedundancyGroupFilterSet",
107
108
  "DeviceTypeFilterSet",
108
109
  "DeviceTypeToSoftwareImageFileFilterSet",
109
110
  "FrontPortFilterSet",
110
111
  "FrontPortTemplateFilterSet",
111
- "DeviceFamilyFilterSet",
112
112
  "InterfaceConnectionFilterSet",
113
113
  "InterfaceFilterSet",
114
114
  "InterfaceRedundancyGroupFilterSet",
nautobot/dcim/forms.py CHANGED
@@ -728,6 +728,15 @@ class ManufacturerForm(NautobotModelForm):
728
728
  ]
729
729
 
730
730
 
731
+ class ManufacturerFilterForm(NautobotFilterForm):
732
+ model = Manufacturer
733
+ q = forms.CharField(required=False, label="Search")
734
+ device_types = DynamicModelMultipleChoiceField(
735
+ queryset=DeviceType.objects.all(), to_field_name="model", required=False
736
+ )
737
+ platforms = DynamicModelMultipleChoiceField(queryset=Platform.objects.all(), to_field_name="name", required=False)
738
+
739
+
731
740
  #
732
741
  # Device Family
733
742
  #
@@ -745,6 +754,9 @@ class DeviceFamilyForm(NautobotModelForm):
745
754
  class DeviceFamilyFilterForm(NautobotFilterForm):
746
755
  model = DeviceFamily
747
756
  q = forms.CharField(required=False, label="Search")
757
+ device_types = DynamicModelMultipleChoiceField(
758
+ queryset=DeviceType.objects.all(), to_field_name="model", required=False
759
+ )
748
760
  tags = TagFilterField(model)
749
761
 
750
762
 
@@ -1582,6 +1594,13 @@ class PlatformForm(NautobotModelForm):
1582
1594
  }
1583
1595
 
1584
1596
 
1597
+ class PlatformFilterForm(NautobotFilterForm):
1598
+ model = Platform
1599
+ q = forms.CharField(required=False, label="Search")
1600
+ name = forms.CharField(required=False)
1601
+ network_driver = forms.CharField(required=False)
1602
+
1603
+
1585
1604
  #
1586
1605
  # Devices
1587
1606
  #
@@ -2567,14 +2586,14 @@ class InterfaceBulkEditForm(
2567
2586
  queryset=VLAN.objects.all(),
2568
2587
  required=False,
2569
2588
  query_params={
2570
- "location": "null",
2589
+ "locations": "null",
2571
2590
  },
2572
2591
  )
2573
2592
  tagged_vlans = DynamicModelMultipleChoiceField(
2574
2593
  queryset=VLAN.objects.all(),
2575
2594
  required=False,
2576
2595
  query_params={
2577
- "location": "null",
2596
+ "locations": "null",
2578
2597
  },
2579
2598
  )
2580
2599
  vrf = DynamicModelChoiceField(
@@ -2618,8 +2637,8 @@ class InterfaceBulkEditForm(
2618
2637
  # Limit VLAN choices by Location
2619
2638
  if locations.count() == 1:
2620
2639
  location = locations.first()
2621
- self.fields["untagged_vlan"].widget.add_query_param("location", location.pk)
2622
- self.fields["tagged_vlans"].widget.add_query_param("location", location.pk)
2640
+ self.fields["untagged_vlan"].widget.add_query_param("locations", location.pk)
2641
+ self.fields["tagged_vlans"].widget.add_query_param("locations", location.pk)
2623
2642
 
2624
2643
  # Restrict parent/bridge/LAG interface assignment by device (or VC master)
2625
2644
  if device_count == 1:
@@ -45,9 +45,15 @@ __all__ = (
45
45
  class ManufacturerTable(BaseTable):
46
46
  pk = ToggleColumn()
47
47
  name = tables.LinkColumn()
48
- device_type_count = tables.Column(verbose_name="Device Types")
49
- inventory_item_count = tables.Column(verbose_name="Inventory Items")
50
- platform_count = tables.Column(verbose_name="Platforms")
48
+ device_type_count = LinkedCountColumn(
49
+ viewname="dcim:devicetype_list", url_params={"manufacturer": "name"}, verbose_name="Device Types"
50
+ )
51
+ inventory_item_count = LinkedCountColumn(
52
+ viewname="dcim:inventoryitem_list", url_params={"manufacturer": "name"}, verbose_name="Inventory Items"
53
+ )
54
+ platform_count = LinkedCountColumn(
55
+ viewname="dcim:platform_list", url_params={"manufacturer": "name"}, verbose_name="Platforms"
56
+ )
51
57
  actions = ButtonsColumn(Manufacturer)
52
58
 
53
59
  class Meta(BaseTable.Meta):
@@ -71,7 +77,9 @@ class ManufacturerTable(BaseTable):
71
77
  class DeviceFamilyTable(BaseTable):
72
78
  pk = ToggleColumn()
73
79
  name = tables.Column(linkify=True)
74
- device_type_count = tables.Column(verbose_name="Device Types")
80
+ device_type_count = LinkedCountColumn(
81
+ viewname="dcim:devicetype_list", url_params={"device_family": "name"}, verbose_name="Device Types"
82
+ )
75
83
  actions = ButtonsColumn(DeviceFamily)
76
84
  tags = TagColumn(url_name="dcim:devicefamily_list")
77
85
 
@@ -95,6 +103,8 @@ class DeviceFamilyTable(BaseTable):
95
103
  class DeviceTypeTable(BaseTable):
96
104
  pk = ToggleColumn()
97
105
  model = tables.Column(linkify=True, verbose_name="Device Type")
106
+ manufacturer = tables.Column(linkify=True)
107
+ device_family = tables.Column(linkify=True)
98
108
  is_full_depth = BooleanColumn(verbose_name="Full Depth")
99
109
  device_count = LinkedCountColumn(
100
110
  viewname="dcim:device_list",
@@ -109,6 +119,7 @@ class DeviceTypeTable(BaseTable):
109
119
  "pk",
110
120
  "model",
111
121
  "manufacturer",
122
+ "device_family",
112
123
  "part_number",
113
124
  "u_height",
114
125
  "is_full_depth",
@@ -1854,12 +1854,14 @@ class SoftwareVersionTestCase(ModelTestCases.BaseModelTestCase):
1854
1854
 
1855
1855
  # Only return the device types with a direct m2m relationship to the version's software image files
1856
1856
  device_type = DeviceType.objects.filter(software_image_files__isnull=False).first()
1857
+ self.assertIsNotNone(device_type)
1857
1858
  self.assertQuerysetEqualAndNotEmpty(
1858
1859
  qs.get_for_object(device_type), qs.filter(software_image_files__device_types=device_type)
1859
1860
  )
1860
1861
 
1861
1862
  # Only return the software version set on the device's software_version foreign key
1862
1863
  device = Device.objects.filter(software_version__isnull=False).first()
1864
+ self.assertIsNotNone(device)
1863
1865
  self.assertQuerysetEqualAndNotEmpty(qs.get_for_object(device), [device.software_version])
1864
1866
 
1865
1867
  # Only return the software version set on the inventory item's software_version foreign key
@@ -3398,6 +3398,48 @@ class SoftwareImageFileTestCase(ViewTestCases.PrimaryObjectViewTestCase):
3398
3398
  "download_url": "https://example.com/software_image_file_test_case.bin",
3399
3399
  }
3400
3400
 
3401
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
3402
+ def test_correct_handling_for_model_protected_error(self):
3403
+ platform = Platform.objects.first()
3404
+ software_version_status = Status.objects.get_for_model(SoftwareVersion).first()
3405
+ software_image_file_status = Status.objects.get_for_model(SoftwareImageFile).first()
3406
+ software_version = SoftwareVersion.objects.create(
3407
+ platform=platform, version="Test version 1.0.0", status=software_version_status
3408
+ )
3409
+ software_image_file = SoftwareImageFile.objects.create(
3410
+ software_version=software_version,
3411
+ image_file_name="software_image_file_qs_test_1.bin",
3412
+ status=software_image_file_status,
3413
+ )
3414
+ device_type = DeviceType.objects.first()
3415
+ device_role = Role.objects.get_for_model(Device).first()
3416
+ device_status = Status.objects.get_for_model(Device).first()
3417
+ location = Location.objects.filter(location_type__name="Campus").first()
3418
+ Device.objects.create(
3419
+ device_type=device_type,
3420
+ role=device_role,
3421
+ name="Device 1",
3422
+ location=location,
3423
+ status=device_status,
3424
+ software_version=software_version,
3425
+ )
3426
+ device_type_to_software_image_file = DeviceTypeToSoftwareImageFile.objects.create(
3427
+ device_type=device_type, software_image_file=software_image_file
3428
+ )
3429
+
3430
+ self.add_permissions("dcim.delete_softwareimagefile")
3431
+ pk_list = [software_image_file.pk]
3432
+ data = {
3433
+ "pk": pk_list,
3434
+ "confirm": True,
3435
+ "_confirm": True, # Form button
3436
+ }
3437
+ response = self.client.post(self._get_url("bulk_delete"), data, follow=True)
3438
+ self.assertHttpStatus(response, 200)
3439
+ response_body = response.content.decode(response.charset)
3440
+ # Assert protected error message included in the response body
3441
+ self.assertInHTML(f"<span>{device_type_to_software_image_file}</span>", response_body)
3442
+
3401
3443
 
3402
3444
  class SoftwareVersionTestCase(ViewTestCases.PrimaryObjectViewTestCase):
3403
3445
  model = SoftwareVersion
@@ -3436,6 +3478,48 @@ class SoftwareVersionTestCase(ViewTestCases.PrimaryObjectViewTestCase):
3436
3478
  "pre_release": True,
3437
3479
  }
3438
3480
 
3481
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
3482
+ def test_correct_handling_for_model_protected_error(self):
3483
+ platform = Platform.objects.first()
3484
+ software_version_status = Status.objects.get_for_model(SoftwareVersion).first()
3485
+ software_image_file_status = Status.objects.get_for_model(SoftwareImageFile).first()
3486
+ software_version = SoftwareVersion.objects.create(
3487
+ platform=platform, version="Test version 1.0.0", status=software_version_status
3488
+ )
3489
+ software_image_file = SoftwareImageFile.objects.create(
3490
+ software_version=software_version,
3491
+ image_file_name="software_image_file_qs_test_1.bin",
3492
+ status=software_image_file_status,
3493
+ )
3494
+ device_type = DeviceType.objects.first()
3495
+ device_role = Role.objects.get_for_model(Device).first()
3496
+ device_status = Status.objects.get_for_model(Device).first()
3497
+ location = Location.objects.filter(location_type__name="Campus").first()
3498
+ Device.objects.create(
3499
+ device_type=device_type,
3500
+ role=device_role,
3501
+ name="Device 1",
3502
+ location=location,
3503
+ status=device_status,
3504
+ software_version=software_version,
3505
+ )
3506
+ device_type_to_software_image_file = DeviceTypeToSoftwareImageFile.objects.create(
3507
+ device_type=device_type, software_image_file=software_image_file
3508
+ )
3509
+
3510
+ self.add_permissions("dcim.delete_softwareversion")
3511
+ pk_list = [software_version.pk]
3512
+ data = {
3513
+ "pk": pk_list,
3514
+ "confirm": True,
3515
+ "_confirm": True, # Form button
3516
+ }
3517
+ response = self.client.post(self._get_url("bulk_delete"), data, follow=True)
3518
+ self.assertHttpStatus(response, 200)
3519
+ response_body = response.content.decode(response.charset)
3520
+ # Assert protected error message included in the response body
3521
+ self.assertInHTML(f"<span>{device_type_to_software_image_file}</span>", response_body)
3522
+
3439
3523
 
3440
3524
  class ControllerTestCase(ViewTestCases.PrimaryObjectViewTestCase):
3441
3525
  model = Controller
nautobot/dcim/views.py CHANGED
@@ -710,6 +710,7 @@ class ManufacturerListView(generic.ObjectListView):
710
710
  platform_count=count_related(Platform, "manufacturer"),
711
711
  )
712
712
  filterset = filters.ManufacturerFilterSet
713
+ filterset_form = forms.ManufacturerFilterForm
713
714
  table = tables.ManufacturerTable
714
715
 
715
716
 
@@ -1212,6 +1213,7 @@ class PlatformListView(generic.ObjectListView):
1212
1213
  virtual_machine_count=count_related(VirtualMachine, "platform"),
1213
1214
  )
1214
1215
  filterset = filters.PlatformFilterSet
1216
+ filterset_form = forms.PlatformFilterForm
1215
1217
  table = tables.PlatformTable
1216
1218
 
1217
1219
 
@@ -3103,6 +3105,7 @@ class InterfaceRedundancyGroupAssociationUIViewSet(ObjectEditViewMixin, ObjectDe
3103
3105
 
3104
3106
  class DeviceFamilyUIViewSet(NautobotUIViewSet):
3105
3107
  filterset_class = filters.DeviceFamilyFilterSet
3108
+ filterset_form_class = forms.DeviceFamilyFilterForm
3106
3109
  form_class = forms.DeviceFamilyForm
3107
3110
  bulk_update_form_class = forms.DeviceFamilyBulkEditForm
3108
3111
  queryset = DeviceFamily.objects.annotate(device_type_count=count_related(DeviceType, "device_family"))