nautobot 2.3.4__py3-none-any.whl → 2.3.5__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 (308) hide show
  1. nautobot/core/tests/runner.py +13 -6
  2. nautobot/core/tests/test_views.py +40 -1
  3. nautobot/core/views/generic.py +15 -15
  4. nautobot/core/views/mixins.py +12 -1
  5. nautobot/core/views/renderers.py +3 -1
  6. nautobot/core/views/utils.py +1 -1
  7. nautobot/dcim/api/serializers.py +1 -0
  8. nautobot/dcim/api/views.py +2 -0
  9. nautobot/dcim/forms.py +1 -1
  10. nautobot/dcim/templates/dcim/devicefamily_retrieve.html +1 -1
  11. nautobot/dcim/tests/test_api.py +58 -4
  12. nautobot/dcim/tests/test_models.py +0 -2
  13. nautobot/dcim/views.py +5 -2
  14. nautobot/extras/api/views.py +9 -0
  15. nautobot/extras/models/jobs.py +9 -1
  16. nautobot/extras/querysets.py +10 -1
  17. nautobot/extras/tables.py +3 -0
  18. nautobot/extras/tests/test_api.py +36 -0
  19. nautobot/extras/tests/test_views.py +76 -1
  20. nautobot/extras/views.py +10 -7
  21. nautobot/ipam/forms.py +6 -1
  22. nautobot/ipam/models.py +5 -11
  23. nautobot/ipam/navigation.py +8 -1
  24. nautobot/ipam/templates/ipam/prefix.html +1 -1
  25. nautobot/ipam/tests/test_filters.py +1 -1
  26. nautobot/ipam/tests/test_views.py +41 -41
  27. nautobot/ipam/views.py +1 -1
  28. nautobot/project-static/docs/404.html +2 -2
  29. nautobot/project-static/docs/apps/index.html +2 -2
  30. nautobot/project-static/docs/apps/nautobot-apps.html +2 -2
  31. nautobot/project-static/docs/assets/javascripts/workers/{search.07f07601.min.js → search.6ce7567c.min.js} +3 -3
  32. nautobot/project-static/docs/assets/javascripts/workers/{search.07f07601.min.js.map → search.6ce7567c.min.js.map} +2 -2
  33. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +2 -2
  34. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +2 -2
  35. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +2 -2
  36. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +2 -2
  37. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +2 -2
  38. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +2 -2
  39. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +2 -2
  40. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +2 -2
  41. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +2 -2
  42. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +2 -2
  43. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +2 -2
  44. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +2 -2
  45. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +2 -2
  46. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +2 -2
  47. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +2 -2
  48. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +2 -2
  49. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +2 -2
  50. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +2 -2
  51. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +2 -2
  52. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +2 -2
  53. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +2 -2
  54. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +2 -2
  55. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +2 -2
  56. nautobot/project-static/docs/development/apps/api/configuration-view.html +2 -2
  57. nautobot/project-static/docs/development/apps/api/database-backend-config.html +2 -2
  58. nautobot/project-static/docs/development/apps/api/models/django-admin.html +2 -2
  59. nautobot/project-static/docs/development/apps/api/models/global-search.html +2 -2
  60. nautobot/project-static/docs/development/apps/api/models/graphql.html +2 -2
  61. nautobot/project-static/docs/development/apps/api/models/index.html +2 -2
  62. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +2 -2
  63. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +2 -2
  64. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +2 -2
  65. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +2 -2
  66. nautobot/project-static/docs/development/apps/api/platform-features/index.html +2 -2
  67. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +2 -2
  68. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +2 -2
  69. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +2 -2
  70. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +2 -2
  71. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +2 -2
  72. nautobot/project-static/docs/development/apps/api/prometheus.html +2 -2
  73. nautobot/project-static/docs/development/apps/api/setup.html +2 -2
  74. nautobot/project-static/docs/development/apps/api/testing.html +2 -2
  75. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +2 -2
  76. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +2 -2
  77. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +2 -2
  78. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +2 -2
  79. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +2 -2
  80. nautobot/project-static/docs/development/apps/api/views/base-template.html +2 -2
  81. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +2 -2
  82. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +2 -2
  83. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +2 -2
  84. nautobot/project-static/docs/development/apps/api/views/index.html +2 -2
  85. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +2 -2
  86. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +2 -2
  87. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +2 -2
  88. nautobot/project-static/docs/development/apps/api/views/notes.html +2 -2
  89. nautobot/project-static/docs/development/apps/api/views/rest-api.html +2 -2
  90. nautobot/project-static/docs/development/apps/api/views/urls.html +2 -2
  91. nautobot/project-static/docs/development/apps/index.html +2 -2
  92. nautobot/project-static/docs/development/apps/migration/code-updates.html +2 -2
  93. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +2 -2
  94. nautobot/project-static/docs/development/apps/migration/from-v1.html +2 -2
  95. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +2 -2
  96. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +2 -2
  97. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +2 -2
  98. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +2 -2
  99. nautobot/project-static/docs/development/apps/porting-from-netbox.html +2 -2
  100. nautobot/project-static/docs/development/core/application-registry.html +2 -2
  101. nautobot/project-static/docs/development/core/best-practices.html +2 -2
  102. nautobot/project-static/docs/development/core/bootstrap-ui.html +2 -2
  103. nautobot/project-static/docs/development/core/caching.html +2 -2
  104. nautobot/project-static/docs/development/core/controllers.html +2 -2
  105. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +2 -2
  106. nautobot/project-static/docs/development/core/generic-views.html +2 -2
  107. nautobot/project-static/docs/development/core/getting-started.html +7 -6
  108. nautobot/project-static/docs/development/core/homepage.html +2 -2
  109. nautobot/project-static/docs/development/core/index.html +2 -2
  110. nautobot/project-static/docs/development/core/model-checklist.html +2 -2
  111. nautobot/project-static/docs/development/core/model-features.html +2 -2
  112. nautobot/project-static/docs/development/core/natural-keys.html +2 -2
  113. nautobot/project-static/docs/development/core/navigation-menu.html +2 -2
  114. nautobot/project-static/docs/development/core/release-checklist.html +2 -2
  115. nautobot/project-static/docs/development/core/role-internals.html +2 -2
  116. nautobot/project-static/docs/development/core/settings.html +2 -2
  117. nautobot/project-static/docs/development/core/style-guide.html +2 -2
  118. nautobot/project-static/docs/development/core/templates.html +2 -2
  119. nautobot/project-static/docs/development/core/testing.html +12 -4
  120. nautobot/project-static/docs/development/core/user-preferences.html +2 -2
  121. nautobot/project-static/docs/development/index.html +2 -2
  122. nautobot/project-static/docs/development/jobs/index.html +2 -2
  123. nautobot/project-static/docs/development/jobs/migration/from-v1.html +2 -2
  124. nautobot/project-static/docs/index.html +2 -2
  125. nautobot/project-static/docs/overview/application_stack.html +2 -2
  126. nautobot/project-static/docs/overview/design_philosophy.html +2 -2
  127. nautobot/project-static/docs/release-notes/index.html +2 -2
  128. nautobot/project-static/docs/release-notes/version-1.0.html +2 -2
  129. nautobot/project-static/docs/release-notes/version-1.1.html +2 -2
  130. nautobot/project-static/docs/release-notes/version-1.2.html +2 -2
  131. nautobot/project-static/docs/release-notes/version-1.3.html +2 -2
  132. nautobot/project-static/docs/release-notes/version-1.4.html +2 -2
  133. nautobot/project-static/docs/release-notes/version-1.5.html +2 -2
  134. nautobot/project-static/docs/release-notes/version-1.6.html +2 -2
  135. nautobot/project-static/docs/release-notes/version-2.0.html +2 -2
  136. nautobot/project-static/docs/release-notes/version-2.1.html +2 -2
  137. nautobot/project-static/docs/release-notes/version-2.2.html +2 -2
  138. nautobot/project-static/docs/release-notes/version-2.3.html +252 -76
  139. nautobot/project-static/docs/search/search_index.json +1 -1
  140. nautobot/project-static/docs/sitemap.xml +269 -269
  141. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  142. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +2 -2
  143. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +2 -2
  144. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +2 -2
  145. nautobot/project-static/docs/user-guide/administration/configuration/index.html +2 -2
  146. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +2 -2
  147. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +2 -2
  148. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +2 -2
  149. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +2 -2
  150. nautobot/project-static/docs/user-guide/administration/guides/docker.html +2 -2
  151. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +2 -2
  152. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +2 -2
  153. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +2 -2
  154. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +2 -2
  155. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +2 -2
  156. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +2 -2
  157. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +2 -2
  158. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +2 -2
  159. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +2 -2
  160. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +2 -2
  161. nautobot/project-static/docs/user-guide/administration/installation/index.html +2 -2
  162. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +2 -2
  163. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +2 -2
  164. nautobot/project-static/docs/user-guide/administration/installation/services.html +2 -2
  165. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +2 -2
  166. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +2 -2
  167. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +2 -2
  168. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +2 -2
  169. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +2 -2
  170. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +2 -2
  171. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +2 -2
  172. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +2 -2
  173. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +2 -2
  174. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +2 -2
  175. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +2 -2
  176. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +2 -2
  177. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +2 -2
  178. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +2 -2
  179. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +2 -2
  180. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +2 -2
  181. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +2 -2
  182. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +2 -2
  183. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +2 -2
  184. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +2 -2
  185. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +2 -2
  186. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +2 -2
  187. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +2 -2
  188. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +2 -2
  189. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +2 -2
  190. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +2 -2
  191. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +2 -2
  192. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +2 -2
  193. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +2 -2
  194. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +2 -2
  195. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +2 -2
  196. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +2 -2
  197. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +2 -2
  198. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +2 -2
  199. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +2 -2
  200. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +2 -2
  201. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +2 -2
  202. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +2 -2
  203. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +2 -2
  204. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +2 -2
  205. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +2 -2
  206. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +2 -2
  207. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +2 -2
  208. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +2 -2
  209. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +2 -2
  210. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +2 -2
  211. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +2 -2
  212. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +2 -2
  213. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +2 -2
  214. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +2 -2
  215. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +2 -2
  216. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +2 -2
  217. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +2 -2
  218. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +2 -2
  219. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +2 -2
  220. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +2 -2
  221. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +2 -2
  222. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +2 -2
  223. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +2 -2
  224. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +2 -2
  225. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +2 -2
  226. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +2 -2
  227. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +2 -2
  228. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +2 -2
  229. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +2 -2
  230. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +2 -2
  231. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +2 -2
  232. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +2 -2
  233. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +2 -2
  234. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +2 -2
  235. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +2 -2
  236. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +2 -2
  237. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +2 -2
  238. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +2 -2
  239. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +2 -2
  240. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +2 -2
  241. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +2 -2
  242. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +2 -2
  243. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +2 -2
  244. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +2 -2
  245. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +2 -2
  246. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +2 -2
  247. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +2 -2
  248. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +2 -2
  249. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +2 -2
  250. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +2 -2
  251. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +2 -2
  252. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +2 -2
  253. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +2 -2
  254. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +2 -2
  255. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +2 -2
  256. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +2 -2
  257. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +2 -2
  258. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +2 -2
  259. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +2 -2
  260. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +2 -2
  261. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +2 -2
  262. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +2 -2
  263. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +2 -2
  264. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +2 -2
  265. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +2 -2
  266. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +2 -2
  267. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +2 -2
  268. nautobot/project-static/docs/user-guide/index.html +2 -2
  269. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +2 -2
  270. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +2 -2
  271. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +2 -2
  272. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +2 -2
  273. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +2 -2
  274. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +2 -2
  275. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +2 -2
  276. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +2 -2
  277. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +2 -2
  278. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +2 -2
  279. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +2 -2
  280. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +2 -2
  281. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +2 -2
  282. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +2 -2
  283. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +2 -2
  284. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +2 -2
  285. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +2 -2
  286. nautobot/project-static/docs/user-guide/platform-functionality/note.html +2 -2
  287. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +2 -2
  288. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +2 -2
  289. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +2 -2
  290. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +2 -2
  291. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +2 -2
  292. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +2 -2
  293. nautobot/project-static/docs/user-guide/platform-functionality/role.html +2 -2
  294. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +2 -2
  295. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +2 -2
  296. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +2 -2
  297. nautobot/project-static/docs/user-guide/platform-functionality/status.html +2 -2
  298. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +2 -2
  299. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +2 -2
  300. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +2 -2
  301. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +2 -2
  302. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +2 -2
  303. {nautobot-2.3.4.dist-info → nautobot-2.3.5.dist-info}/METADATA +2 -2
  304. {nautobot-2.3.4.dist-info → nautobot-2.3.5.dist-info}/RECORD +308 -308
  305. {nautobot-2.3.4.dist-info → nautobot-2.3.5.dist-info}/LICENSE.txt +0 -0
  306. {nautobot-2.3.4.dist-info → nautobot-2.3.5.dist-info}/NOTICE +0 -0
  307. {nautobot-2.3.4.dist-info → nautobot-2.3.5.dist-info}/WHEEL +0 -0
  308. {nautobot-2.3.4.dist-info → nautobot-2.3.5.dist-info}/entry_points.txt +0 -0
@@ -64,9 +64,16 @@ class NautobotTestRunner(DiscoverRunner):
64
64
  action="store_true",
65
65
  help="Save test database to a json fixture file to re-use on subsequent tests.",
66
66
  )
67
+ parser.add_argument(
68
+ "--no-reusedb",
69
+ action="store_false",
70
+ dest="reusedb",
71
+ help="Supplement to --keepdb; if --no-reusedb is set an existing database will NOT be reused.",
72
+ )
67
73
 
68
- def __init__(self, cache_test_fixtures=False, **kwargs):
74
+ def __init__(self, cache_test_fixtures=False, reusedb=True, **kwargs):
69
75
  self.cache_test_fixtures = cache_test_fixtures
76
+ self.reusedb = reusedb
70
77
 
71
78
  # Assert "integration" hasn't been provided w/ --tag
72
79
  incoming_tags = kwargs.get("tags") or []
@@ -118,7 +125,9 @@ class NautobotTestRunner(DiscoverRunner):
118
125
  connection.creation.create_test_db(
119
126
  verbosity=self.verbosity,
120
127
  autoclobber=not self.interactive,
121
- keepdb=self.keepdb,
128
+ keepdb=self.keepdb
129
+ # Extra check added for Nautobot:
130
+ and self.reusedb,
122
131
  serialize=connection.settings_dict["TEST"].get("SERIALIZE", True),
123
132
  )
124
133
 
@@ -149,7 +158,7 @@ class NautobotTestRunner(DiscoverRunner):
149
158
  verbosity=self.verbosity,
150
159
  keepdb=self.keepdb
151
160
  # Extra check added for Nautobot:
152
- and not settings.TEST_USE_FACTORIES,
161
+ and self.reusedb,
153
162
  )
154
163
 
155
164
  # Configure all other connections as mirrors of the first one
@@ -179,9 +188,7 @@ class NautobotTestRunner(DiscoverRunner):
179
188
  connection.creation.destroy_test_db(
180
189
  suffix=str(index + 1),
181
190
  verbosity=self.verbosity,
182
- keepdb=self.keepdb
183
- # Extra check added for Nautobot
184
- and not settings.TEST_USE_FACTORIES,
191
+ keepdb=self.keepdb,
185
192
  )
186
193
 
187
194
  # Extra block added for Nautobot
@@ -1,8 +1,9 @@
1
1
  import re
2
- from unittest import mock
2
+ from unittest import mock, skipIf
3
3
  import urllib.parse
4
4
 
5
5
  from django.apps import apps
6
+ from django.conf import settings
6
7
  from django.contrib.contenttypes.models import ContentType
7
8
  from django.core.files.uploadedfile import SimpleUploadedFile
8
9
  from django.test import override_settings, RequestFactory
@@ -572,3 +573,41 @@ class SilkUIAccessTestCase(TestCase):
572
573
 
573
574
  # Check for success status code (e.g., 200)
574
575
  self.assertEqual(response.status_code, 200)
576
+
577
+
578
+ class ExampleViewWithCustomPermissionsTest(TestCase):
579
+ @skipIf(
580
+ "example_app" not in settings.PLUGINS,
581
+ "example_app not in settings.PLUGINS",
582
+ )
583
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
584
+ def test_permission_classes_attribute_is_enforced(self):
585
+ """
586
+ If example app is installed, check if the ViewWithCustomPermissions
587
+ is enforcing the permissions specified in its `permission_classes` attribute.
588
+ """
589
+ # Test IsAuthenticated permission
590
+ self.add_permissions("example_app.view_examplemodel")
591
+ self.client.logout()
592
+ url = reverse("plugins:example_app:view_with_custom_permissions")
593
+ response = self.client.get(url, follow=True)
594
+ self.assertHttpStatus(response, 200)
595
+ response_body = response.content.decode(response.charset)
596
+ # check if the user is redirected to the login page
597
+ self.assertIn(f'<input type="hidden" name="next" value="{url}" />', response_body)
598
+
599
+ # Test IsAdmin permission
600
+ self.client.force_login(self.user)
601
+ response = self.client.get(url, follow=True)
602
+ self.assertHttpStatus(response, 403)
603
+ response_body = response.content.decode(response.charset)
604
+ # check if the users have to have the permission to access the page
605
+ self.assertIn("You do not have permission to access this page.", response_body)
606
+
607
+ # View should be successfully accessed
608
+ self.user.is_staff = True
609
+ self.user.save()
610
+ response = self.client.get(url)
611
+ self.assertHttpStatus(response, 200)
612
+ response_body = response.content.decode(response.charset)
613
+ self.assertIn("You are viewing a table of example models", response_body)
@@ -17,7 +17,7 @@ from django.db.models import ManyToManyField, ProtectedError, Q
17
17
  from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput
18
18
  from django.http import HttpResponse
19
19
  from django.shortcuts import get_object_or_404, redirect, render
20
- from django.urls import reverse
20
+ from django.urls import resolve, reverse
21
21
  from django.utils.encoding import iri_to_uri
22
22
  from django.utils.html import format_html
23
23
  from django.utils.http import url_has_allowed_host_and_scheme
@@ -205,17 +205,16 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
205
205
  filter_form = None
206
206
  hide_hierarchy_ui = False
207
207
  clear_view = request.GET.get("clear_view", False)
208
+ resolved_path = resolve(request.path)
209
+ list_url = f"{resolved_path.app_name}:{resolved_path.url_name}"
208
210
 
209
211
  # If the user clicks on the clear view button, we do not check for global or user defaults
210
212
  if not clear_view and not request.GET.get("saved_view"):
211
213
  # Check if there is a default for this view for this specific user
212
- app_label, model_name = model._meta.label.split(".")
213
- view_name = f"{app_label}:{model_name.lower()}_list"
214
-
215
214
  if not isinstance(user, AnonymousUser):
216
215
  try:
217
216
  user_default_saved_view_pk = UserSavedViewAssociation.objects.get(
218
- user=user, view_name=view_name
217
+ user=user, view_name=list_url
219
218
  ).saved_view.pk
220
219
  # Saved view should either belong to the user or be public
221
220
  SavedView.objects.get(
@@ -229,7 +228,7 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
229
228
 
230
229
  # Check if there is a global default for this view
231
230
  try:
232
- global_saved_view = SavedView.objects.get(view=view_name, is_global_default=True)
231
+ global_saved_view = SavedView.objects.get(view=list_url, is_global_default=True)
233
232
  return redirect(reverse("extras:savedview", kwargs={"pk": global_saved_view.pk}))
234
233
  except ObjectDoesNotExist:
235
234
  pass
@@ -302,7 +301,6 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
302
301
  table_config_form = None
303
302
  current_saved_view = None
304
303
  current_saved_view_pk = self.request.GET.get("saved_view", None)
305
- list_url = validated_viewname(model, "list")
306
304
  # We are not using .restrict(request.user, "view") here
307
305
  # User should be able to see any saved view that he has the list view access to.
308
306
  if user.has_perms(["extras.view_savedview"]):
@@ -315,15 +313,17 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
315
313
  SavedView.objects.filter(view=list_url, owner=user).order_by("name").only("pk", "name")
316
314
  )
317
315
  saved_views = shared_saved_views | user_owned_saved_views
316
+
317
+ if current_saved_view_pk:
318
+ try:
319
+ # We are not using .restrict(request.user, "view") here
320
+ # User should be able to see any saved view that he has the list view access to.
321
+ current_saved_view = SavedView.objects.get(view=list_url, pk=current_saved_view_pk)
322
+ except ObjectDoesNotExist:
323
+ messages.error(request, f"Saved view {current_saved_view_pk} not found")
324
+
325
+ # Construct the objects table
318
326
  if self.table:
319
- # Construct the objects table
320
- if current_saved_view_pk:
321
- try:
322
- # We are not using .restrict(request.user, "view") here
323
- # User should be able to see any saved view that he has the list view access to.
324
- current_saved_view = SavedView.objects.get(view=list_url, pk=current_saved_view_pk)
325
- except ObjectDoesNotExist:
326
- messages.error(request, f"Saved view {current_saved_view_pk} not found")
327
327
  if self.request.GET.getlist("sort") or (
328
328
  current_saved_view is not None and current_saved_view.config.get("sort_order")
329
329
  ):
@@ -234,6 +234,7 @@ class NautobotViewSetMixin(GenericViewSet, AccessMixin, GetReturnURLMixin, FormV
234
234
  serializer_class = None
235
235
  table_class = None
236
236
  notes_form_class = NoteForm
237
+ permission_classes = []
237
238
 
238
239
  def get_permissions_for_model(self, model, actions):
239
240
  """
@@ -267,7 +268,7 @@ class NautobotViewSetMixin(GenericViewSet, AccessMixin, GetReturnURLMixin, FormV
267
268
  """
268
269
  user = self.request.user
269
270
  permission_required = self.get_required_permission()
270
- # Check that the user has been granted the required permission(s) one by one.
271
+ # Check that the user has been granted the required Nautobot-specific object permission(s) one by one.
271
272
  # In case the permission has `message` or `code`` attribute, we want to include those information in the permission_denied error.
272
273
  for permission in permission_required:
273
274
  # If the user does not have the permission required, we raise DRF's `NotAuthenticated` or `PermissionDenied` exception
@@ -280,6 +281,16 @@ class NautobotViewSetMixin(GenericViewSet, AccessMixin, GetReturnURLMixin, FormV
280
281
  code=getattr(permission, "code", None),
281
282
  )
282
283
 
284
+ # Check for drf-specific permissions (IsAutheticated, etc) in permission_classes which is empty by default.
285
+ # self.get_permissions() iterates through permissions specified in the `permission_classes` attribute.
286
+ for permission in self.get_permissions():
287
+ # If the user does not have the permission required, we raise DRF's `NotAuthenticated` or `PermissionDenied` exception
288
+ # which will be handled by self.handle_no_permission() in the UI appropriately in the dispatch() method
289
+ if not permission.has_permission(request, self):
290
+ self.permission_denied(
291
+ request, message=getattr(permission, "message", None), code=getattr(permission, "code", None)
292
+ )
293
+
283
294
  def dispatch(self, request, *args, **kwargs):
284
295
  """
285
296
  Override the default dispatch() method to check permissions first.
@@ -5,6 +5,7 @@ from django.contrib.contenttypes.models import ContentType
5
5
  from django.core.exceptions import ObjectDoesNotExist
6
6
  from django.db.models import Q
7
7
  from django.template import engines, loader
8
+ from django.urls import resolve
8
9
  from django_tables2 import RequestConfig
9
10
  from rest_framework import renderers
10
11
 
@@ -295,7 +296,8 @@ class NautobotHTMLRenderer(renderers.BrowsableAPIRenderer):
295
296
  # Construct valid actions for list view.
296
297
  valid_actions = self.validate_action_buttons(view, request)
297
298
  # Query SavedViews for dropdown button
298
- list_url = validated_viewname(model, "list")
299
+ resolved_path = resolve(request.path)
300
+ list_url = f"{resolved_path.app_name}:{resolved_path.url_name}"
299
301
  saved_views = None
300
302
  if model.is_saved_view_model:
301
303
  # We are not using .restrict(request.user, "view") here
@@ -213,7 +213,7 @@ def handle_protectederror(obj_list, request, e):
213
213
  protected_objects = list(e.protected_objects)
214
214
  protected_count = len(protected_objects) if len(protected_objects) <= 50 else "More than 50"
215
215
  err_message = format_html(
216
- "Unable to delete <strong>{}</strong>. {} dependent objects were found: ",
216
+ str(e.args[0]) if e.args else "Unable to delete <strong>{}</strong>. {} dependent objects were found: ",
217
217
  ", ".join(str(obj) for obj in obj_list),
218
218
  protected_count,
219
219
  )
@@ -389,6 +389,7 @@ class RackElevationDetailFilterSerializer(serializers.Serializer):
389
389
  expand_devices = serializers.BooleanField(required=False, default=True)
390
390
  include_images = serializers.BooleanField(required=False, default=True)
391
391
  display_fullname = serializers.BooleanField(required=False, default=True)
392
+ is_occupied = serializers.BooleanField(required=False, allow_null=True, default=None)
392
393
 
393
394
  def validate(self, attrs):
394
395
  attrs.setdefault("unit_width", get_settings_or_config("RACK_ELEVATION_DEFAULT_UNIT_WIDTH"))
@@ -231,6 +231,8 @@ class RackViewSet(NautobotModelViewSet):
231
231
  exclude=data["exclude"],
232
232
  expand_devices=data["expand_devices"],
233
233
  )
234
+ if data["is_occupied"] is not None:
235
+ elevation = [u for u in elevation if u["occupied"] == data["is_occupied"]]
234
236
 
235
237
  # Enable filtering rack units by ID
236
238
  q = data["q"]
nautobot/dcim/forms.py CHANGED
@@ -1900,7 +1900,7 @@ class DeviceForm(LocatableModelFormMixin, NautobotModelForm, TenancyForm, LocalC
1900
1900
  widget=APISelect(
1901
1901
  api_url="/api/dcim/racks/{{rack}}/elevation/",
1902
1902
  attrs={
1903
- "disabled-indicator": "device",
1903
+ "disabled-indicator": "occupied",
1904
1904
  "data-query-param-face": '["$face"]',
1905
1905
  },
1906
1906
  ),
@@ -16,7 +16,7 @@
16
16
  </tr>
17
17
  <tr>
18
18
  <td>Device Types</td>
19
- <td><a href="{% url 'dcim:devicetype_list' %}?device_family={{ object.name }}">{{ object.device_type_count }}</a></td>
19
+ <td><a href="{% url 'dcim:devicetype_list' %}?device_family={{ object.name }}">{{ device_type_count }}</a></td>
20
20
  </tr>
21
21
  <tr>
22
22
  <td>Total Devices</td>
@@ -139,7 +139,7 @@ class Mixins:
139
139
  super().setUpTestData()
140
140
  cls.device_type = DeviceType.objects.first()
141
141
  cls.manufacturer = cls.device_type.manufacturer
142
- cls.location = Location.objects.filter(location_type=LocationType.objects.get(name="Campus")).first()
142
+ cls.location = Location.objects.filter(location_type__name="Campus").first()
143
143
  cls.device_role = Role.objects.get_for_model(Device).first()
144
144
  cls.device_status = Status.objects.get_for_model(Device).first()
145
145
  cls.device = Device.objects.create(
@@ -539,6 +539,8 @@ class RackGroupTest(APIViewTestCases.APIViewTestCase, APIViewTestCases.TreeModel
539
539
  def setUpTestData(cls):
540
540
  cls.status = Status.objects.get_for_model(Location).first()
541
541
  location_type = LocationType.objects.create(name="Location Type 1")
542
+ location_type.content_types.add(ContentType.objects.get_for_model(RackGroup))
543
+
542
544
  cls.locations = (
543
545
  Location.objects.create(name="Location 1", location_type=location_type, status=cls.status),
544
546
  Location.objects.create(name="Location 2", location_type=location_type, status=cls.status),
@@ -548,8 +550,6 @@ class RackGroupTest(APIViewTestCases.APIViewTestCase, APIViewTestCases.TreeModel
548
550
  RackGroup.objects.create(location=cls.locations[1], name="Parent Rack Group 2"),
549
551
  )
550
552
 
551
- location_type.content_types.add(ContentType.objects.get_for_model(RackGroup))
552
-
553
553
  RackGroup.objects.create(
554
554
  location=cls.locations[0],
555
555
  name="Rack Group 1",
@@ -643,7 +643,9 @@ class RackTest(APIViewTestCases.APIViewTestCase):
643
643
 
644
644
  @classmethod
645
645
  def setUpTestData(cls):
646
- locations = Location.objects.all()[:2]
646
+ locations = Location.objects.filter(devices__isnull=False)[:2]
647
+ for location in locations:
648
+ location.location_type.content_types.add(ContentType.objects.get_for_model(RackGroup))
647
649
 
648
650
  rack_groups = (
649
651
  RackGroup.objects.create(location=locations[0], name="Rack Group 1"),
@@ -675,6 +677,20 @@ class RackTest(APIViewTestCases.APIViewTestCase):
675
677
  status=statuses[0],
676
678
  )
677
679
 
680
+ populated_rack = Rack.objects.create(
681
+ location=locations[0],
682
+ rack_group=rack_groups[0],
683
+ role=rack_roles[0],
684
+ name="Populated Rack",
685
+ status=statuses[0],
686
+ )
687
+ # Place a device in Rack 4
688
+ device = Device.objects.filter(location=populated_rack.location, rack=None).first()
689
+ device.rack = populated_rack
690
+ device.face = "front"
691
+ device.position = 10
692
+ device.save()
693
+
678
694
  cls.create_data = [
679
695
  {
680
696
  "name": "Test Rack 4",
@@ -742,6 +758,44 @@ class RackTest(APIViewTestCases.APIViewTestCase):
742
758
  response = self.client.get(url, params, **self.header)
743
759
  self.assertHttpStatus(response, 200)
744
760
 
761
+ def test_filter_rack_elevation_is_occupied(self):
762
+ """
763
+ Test filtering the list of rack elevations by occupied status.
764
+ """
765
+ rack = Rack.objects.get(name="Populated Rack")
766
+ self.add_permissions("dcim.view_rack")
767
+ url = reverse("dcim-api:rack-elevation", kwargs={"pk": rack.pk})
768
+ # Get all units first
769
+ params = {"face": "front"}
770
+ response = self.client.get(url, params, **self.header)
771
+ all_units = response.data["results"]
772
+ # Assert the count is equal to the number of units in the rack
773
+ self.assertEqual(len(all_units), rack.u_height)
774
+
775
+ # Next get only unoccupied units
776
+ params = {"face": "front", "is_occupied": False}
777
+ response = self.client.get(url, params, **self.header)
778
+ unoccupied_units = response.data["results"]
779
+ # Assert the count is more than 0
780
+ self.assertGreater(len(unoccupied_units), 0)
781
+ # Assert the unoccupied count is less than the total number of units
782
+ self.assertLess(len(unoccupied_units), len(all_units))
783
+
784
+ # Next get only occupied units
785
+ params = {"face": "front", "is_occupied": True}
786
+ response = self.client.get(url, params, **self.header)
787
+ occupied_units = response.data["results"]
788
+ # Assert the count is more than 0
789
+ self.assertGreater(len(occupied_units), 0)
790
+ # Assert the occupied count is less than the total number of units
791
+ self.assertLess(len(occupied_units), len(all_units))
792
+
793
+ # Assert that the sum of unoccupied and occupied units is equal to the total number of units
794
+ self.assertEqual(len(unoccupied_units) + len(occupied_units), len(all_units))
795
+ # Assert that the lists are mutually exclusive
796
+ self.assertEqual(len([unit for unit in unoccupied_units if unit in occupied_units]), 0)
797
+ self.assertEqual(len([unit for unit in occupied_units if unit in unoccupied_units]), 0)
798
+
745
799
  def test_get_rack_elevation_svg(self):
746
800
  """
747
801
  GET a single rack elevation in SVG format.
@@ -65,7 +65,6 @@ from nautobot.dcim.models import (
65
65
  from nautobot.extras import context_managers
66
66
  from nautobot.extras.choices import CustomFieldTypeChoices
67
67
  from nautobot.extras.models import CustomField, Role, SecretsGroup, Status
68
- from nautobot.ipam.factory import VLANGroupFactory
69
68
  from nautobot.ipam.models import IPAddress, IPAddressToInterface, Namespace, Prefix, VLAN, VLANGroup
70
69
  from nautobot.tenancy.models import Tenant
71
70
  from nautobot.users.models import User
@@ -2351,7 +2350,6 @@ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
2351
2350
  vid=100,
2352
2351
  location=location_2,
2353
2352
  status=vlan_status,
2354
- vlan_group=VLANGroupFactory.create(location=location_2),
2355
2353
  )
2356
2354
 
2357
2355
  cls.namespace = Namespace.objects.create(name="dcim_test_interface_ip_addresses")
nautobot/dcim/views.py CHANGED
@@ -3148,7 +3148,7 @@ class DeviceBayPopulateView(generic.ObjectEditView):
3148
3148
  f"Added {device_bay.installed_device} to {device_bay}.",
3149
3149
  )
3150
3150
 
3151
- return redirect("dcim:device", pk=device_bay.device.pk)
3151
+ return redirect("dcim:device_devicebays", pk=device_bay.device.pk)
3152
3152
 
3153
3153
  return render(
3154
3154
  request,
@@ -3191,7 +3191,7 @@ class DeviceBayDepopulateView(generic.ObjectEditView):
3191
3191
  f"Removed {removed_device} from {device_bay}.",
3192
3192
  )
3193
3193
 
3194
- return redirect("dcim:device", pk=device_bay.device.pk)
3194
+ return redirect("dcim:device_devicebays", pk=device_bay.device.pk)
3195
3195
 
3196
3196
  return render(
3197
3197
  request,
@@ -4139,9 +4139,12 @@ class DeviceFamilyUIViewSet(NautobotUIViewSet):
4139
4139
  context["device_type_table"] = device_type_table
4140
4140
 
4141
4141
  total_devices = 0
4142
+ device_type_count = 0
4142
4143
  for device_type in device_types:
4143
4144
  total_devices += device_type.device_count
4145
+ device_type_count += 1
4144
4146
  context["total_devices"] = total_devices
4147
+ context["device_type_count"] = device_type_count
4145
4148
 
4146
4149
  return context
4147
4150
 
@@ -1,5 +1,6 @@
1
1
  from django.conf import settings
2
2
  from django.contrib.contenttypes.models import ContentType
3
+ from django.db.models import ProtectedError
3
4
  from django.forms import ValidationError as FormsValidationError
4
5
  from django.http import FileResponse, Http404
5
6
  from django.shortcuts import get_object_or_404
@@ -729,6 +730,14 @@ class JobViewSet(
729
730
  ):
730
731
  lookup_value_regex = r"[-0-9a-fA-F]+"
731
732
 
733
+ def perform_destroy(self, obj):
734
+ if obj.module_name.startswith("nautobot."):
735
+ raise ProtectedError(
736
+ f"Unable to delete Job {obj}. System Job cannot be deleted",
737
+ [],
738
+ )
739
+ super().perform_destroy(obj)
740
+
732
741
 
733
742
  @extend_schema_view(
734
743
  destroy=extend_schema(operation_id="extras_jobs_destroy_by_name"),
@@ -11,7 +11,7 @@ from django.contrib.contenttypes.models import ContentType
11
11
  from django.core.exceptions import ValidationError
12
12
  from django.core.validators import MinValueValidator
13
13
  from django.db import models, transaction
14
- from django.db.models import signals
14
+ from django.db.models import ProtectedError, signals
15
15
  from django.utils import timezone
16
16
  from django.utils.functional import cached_property
17
17
  from django_celery_beat.clockedschedule import clocked
@@ -234,6 +234,14 @@ class Job(PrimaryModel):
234
234
  def __str__(self):
235
235
  return self.name
236
236
 
237
+ def delete(self):
238
+ if self.module_name.startswith("nautobot."):
239
+ raise ProtectedError(
240
+ f"Unable to delete Job {self}. System Job cannot be deleted",
241
+ [],
242
+ )
243
+ super().delete()
244
+
237
245
  @property
238
246
  def job_class(self):
239
247
  """
@@ -1,6 +1,6 @@
1
1
  from django.conf import settings
2
2
  from django.contrib.contenttypes.models import ContentType
3
- from django.db.models import F, Model, OuterRef, Q, Subquery
3
+ from django.db.models import F, Model, OuterRef, ProtectedError, Q, Subquery
4
4
  from django.db.models.functions import JSONObject
5
5
 
6
6
  from nautobot.core.models.query_functions import EmptyGroupByJSONBAgg
@@ -224,6 +224,15 @@ class JobQuerySet(RestrictedQuerySet):
224
224
  Extend the standard queryset with a get_for_class_path method.
225
225
  """
226
226
 
227
+ def delete(self):
228
+ for job in self:
229
+ if job.module_name.startswith("nautobot."):
230
+ raise ProtectedError(
231
+ f"Unable to delete Job {job}. System Job cannot be deleted",
232
+ [],
233
+ )
234
+ return super().delete()
235
+
227
236
  def get_for_class_path(self, class_path):
228
237
  try:
229
238
  module_name, job_class_name = class_path.rsplit(".", 1)
nautobot/extras/tables.py CHANGED
@@ -1031,6 +1031,9 @@ class ObjectMetadataTable(BaseTable):
1031
1031
  class NoteTable(BaseTable):
1032
1032
  actions = ButtonsColumn(Note)
1033
1033
  created = tables.LinkColumn()
1034
+ note = tables.Column(
1035
+ attrs={"td": {"class": "rendered-markdown"}},
1036
+ )
1034
1037
 
1035
1038
  class Meta(BaseTable.Meta):
1036
1039
  model = Note
@@ -1388,6 +1388,42 @@ class JobTest(
1388
1388
  job_model = Job.objects.get_for_class_path(class_path)
1389
1389
  return reverse("extras-api:job-run", kwargs={"pk": job_model.pk})
1390
1390
 
1391
+ def get_deletable_object(self):
1392
+ """
1393
+ Get an instance that can be deleted.
1394
+ Exclude system jobs
1395
+ """
1396
+ # filter out the system jobs:
1397
+ queryset = self._get_queryset().exclude(module_name__startswith="nautobot.")
1398
+ instance = get_deletable_objects(self.model, queryset).first()
1399
+ if instance is None:
1400
+ self.fail("Couldn't find a single deletable object!")
1401
+ return instance
1402
+
1403
+ def get_deletable_object_pks(self):
1404
+ """
1405
+ Get a list of PKs corresponding to jobs that can be safely bulk-deleted.
1406
+ Exclude system jobs
1407
+ """
1408
+ queryset = self._get_queryset().exclude(module_name__startswith="nautobot.")
1409
+ instances = get_deletable_objects(self.model, queryset).values_list("pk", flat=True)[:3]
1410
+ if len(instances) < 3:
1411
+ self.fail(f"Couldn't find 3 deletable objects, only found {len(instances)}!")
1412
+ return instances
1413
+
1414
+ def test_delete_system_jobs_fail(self):
1415
+ self.add_permissions("extras.delete_job")
1416
+ instance = self._get_queryset().filter(module_name__startswith="nautobot.").first()
1417
+ job_name = instance.name
1418
+ url = self._get_detail_url(instance)
1419
+ self.client.delete(url, **self.header)
1420
+ # assert Job still exists
1421
+ self.assertTrue(self._get_queryset().filter(name=job_name).exists())
1422
+ self.user.is_superuser = True
1423
+ self.client.delete(url, **self.header)
1424
+ # assert Job still exists
1425
+ self.assertTrue(self._get_queryset().filter(name=job_name).exists())
1426
+
1391
1427
  def test_get_job_variables(self):
1392
1428
  """Test the job/<pk>/variables API endpoint."""
1393
1429
  self.add_permissions("extras.view_job")
@@ -18,7 +18,7 @@ from nautobot.core.choices import ColorChoices
18
18
  from nautobot.core.models.fields import slugify_dashes_to_underscores
19
19
  from nautobot.core.templatetags.helpers import bettertitle
20
20
  from nautobot.core.testing import extract_form_failures, extract_page_body, ModelViewTestCase, TestCase, ViewTestCases
21
- from nautobot.core.testing.utils import disable_warnings, post_data
21
+ from nautobot.core.testing.utils import disable_warnings, get_deletable_objects, post_data
22
22
  from nautobot.core.utils.permissions import get_permission_for_model
23
23
  from nautobot.dcim.models import (
24
24
  ConsolePort,
@@ -1840,6 +1840,9 @@ class ApprovalQueueTestCase(
1840
1840
  return reverse("extras:scheduledjob_approval_request_view", kwargs={"pk": instance.pk})
1841
1841
  raise ValueError("This override is only valid for list and view test cases")
1842
1842
 
1843
+ def get_list_url(self):
1844
+ return reverse("extras:scheduledjob_approval_queue_list")
1845
+
1843
1846
  def setUp(self):
1844
1847
  super().setUp()
1845
1848
  self.job_model = Job.objects.get_for_class_path("dry_run.TestDryRun")
@@ -2363,6 +2366,78 @@ class JobTestCase(
2363
2366
  "clear_task_queues_override": False,
2364
2367
  }
2365
2368
 
2369
+ def get_deletable_object(self):
2370
+ """
2371
+ Get an instance that can be deleted.
2372
+ Exclude system jobs
2373
+ """
2374
+ # filter out the system jobs:
2375
+ queryset = self._get_queryset().exclude(module_name__startswith="nautobot.")
2376
+ return get_deletable_objects(self.model, queryset).first()
2377
+
2378
+ def get_deletable_object_pks(self):
2379
+ """
2380
+ Get a list of PKs corresponding to jobs that can be safely bulk-deleted.
2381
+ Excluding system jobs
2382
+ """
2383
+ queryset = self._get_queryset().exclude(module_name__startswith="nautobot.")
2384
+ return get_deletable_objects(self.model, queryset).values_list("pk", flat=True)[:3]
2385
+
2386
+ def test_delete_system_jobs_fail(self):
2387
+ instance = self._get_queryset().filter(module_name__startswith="nautobot.").first()
2388
+ job_name = instance.name
2389
+ request = {
2390
+ "path": self._get_url("delete", instance),
2391
+ "data": post_data({"confirm": True}),
2392
+ }
2393
+
2394
+ # Try delete with delete job permission
2395
+ self.add_permissions("extras.delete_job")
2396
+ response = self.client.post(**request, follow=True)
2397
+ self.assertHttpStatus(response, 403)
2398
+ response_body = extract_page_body(response.content.decode(response.charset))
2399
+ self.assertIn(f"Unable to delete Job {instance}. System Job cannot be deleted", response_body)
2400
+ # assert Job still exists
2401
+ self.assertTrue(self._get_queryset().filter(name=job_name).exists())
2402
+
2403
+ # Try delete as a superuser
2404
+ self.user.is_superuser = True
2405
+ response = self.client.post(**request, follow=True)
2406
+ self.assertHttpStatus(response, 403)
2407
+ response_body = extract_page_body(response.content.decode(response.charset))
2408
+ self.assertIn(f"Unable to delete Job {instance}. System Job cannot be deleted", response_body)
2409
+ # assert Job still exists
2410
+ self.assertTrue(self._get_queryset().filter(name=job_name).exists())
2411
+
2412
+ def test_bulk_delete_system_jobs_fail(self):
2413
+ system_job_queryset = self.model.objects.filter(module_name__startswith="nautobot.")
2414
+ pk_list = system_job_queryset.values_list("pk", flat=True)[:3]
2415
+ initial_count = self._get_queryset().count()
2416
+ data = {
2417
+ "pk": pk_list,
2418
+ "confirm": True,
2419
+ "_confirm": True, # Form button
2420
+ }
2421
+ # Try bulk delete with delete job permission
2422
+ self.add_permissions("extras.delete_job")
2423
+ response = self.client.post(self._get_url("bulk_delete"), data, follow=True)
2424
+ self.assertHttpStatus(response, 403)
2425
+ self.assertEqual(self._get_queryset().count(), initial_count)
2426
+ response_body = extract_page_body(response.content.decode(response.charset))
2427
+ self.assertIn(
2428
+ f"Unable to delete Job {system_job_queryset.first()}. System Job cannot be deleted", response_body
2429
+ )
2430
+
2431
+ # Try bulk delete as a superuser
2432
+ self.user.is_superuser = True
2433
+ response = self.client.post(self._get_url("bulk_delete"), data, follow=True)
2434
+ self.assertHttpStatus(response, 403)
2435
+ self.assertEqual(self._get_queryset().count(), initial_count)
2436
+ response_body = extract_page_body(response.content.decode(response.charset))
2437
+ self.assertIn(
2438
+ f"Unable to delete Job {system_job_queryset.first()}. System Job cannot be deleted", response_body
2439
+ )
2440
+
2366
2441
  def validate_job_data_after_bulk_edit(self, pk_list, old_data):
2367
2442
  # Name is bulk-editable
2368
2443
  overridable_fields = [field for field in JOB_OVERRIDABLE_FIELDS if field != "name"]