nautobot 2.4.10__py3-none-any.whl → 2.4.11__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 (416) hide show
  1. nautobot/cloud/tests/test_views.py +13 -1
  2. nautobot/cloud/views.py +39 -9
  3. nautobot/core/celery/__init__.py +21 -0
  4. nautobot/core/celery/encoders.py +3 -0
  5. nautobot/core/forms/forms.py +4 -1
  6. nautobot/core/jobs/bulk_actions.py +8 -8
  7. nautobot/core/jobs/cleanup.py +11 -0
  8. nautobot/core/management/commands/generate_test_data.py +2 -1
  9. nautobot/core/templates/generic/object_retrieve.html +1 -1
  10. nautobot/core/testing/mixins.py +19 -1
  11. nautobot/core/testing/views.py +104 -8
  12. nautobot/core/tests/test_jobs.py +20 -4
  13. nautobot/core/tests/test_utils.py +193 -0
  14. nautobot/core/tests/test_views_utils.py +53 -2
  15. nautobot/core/ui/object_detail.py +4 -0
  16. nautobot/core/utils/lookup.py +4 -2
  17. nautobot/core/utils/module_loading.py +86 -58
  18. nautobot/core/views/generic.py +2 -12
  19. nautobot/core/views/mixins.py +19 -1
  20. nautobot/core/views/renderers.py +4 -13
  21. nautobot/core/views/utils.py +16 -0
  22. nautobot/dcim/api/serializers.py +13 -0
  23. nautobot/dcim/api/urls.py +1 -0
  24. nautobot/dcim/api/views.py +20 -0
  25. nautobot/dcim/apps.py +1 -0
  26. nautobot/dcim/factory.py +11 -0
  27. nautobot/dcim/filters/__init__.py +110 -0
  28. nautobot/dcim/forms.py +205 -19
  29. nautobot/dcim/migrations/0070_modulefamily_models.py +92 -0
  30. nautobot/dcim/models/__init__.py +2 -0
  31. nautobot/dcim/models/device_component_templates.py +14 -0
  32. nautobot/dcim/models/device_components.py +13 -1
  33. nautobot/dcim/models/devices.py +62 -0
  34. nautobot/dcim/navigation.py +16 -0
  35. nautobot/dcim/tables/__init__.py +2 -0
  36. nautobot/dcim/tables/devices.py +48 -0
  37. nautobot/dcim/tables/devicetypes.py +35 -1
  38. nautobot/dcim/tables/template_code.py +2 -0
  39. nautobot/dcim/templates/dcim/controllermanageddevicegroup_retrieve.html +1 -90
  40. nautobot/dcim/templates/dcim/inc/cable_toggle_buttons.html +1 -1
  41. nautobot/dcim/templates/dcim/interfaceredundancygroup_retrieve.html +1 -63
  42. nautobot/dcim/templates/dcim/location.html +2 -249
  43. nautobot/dcim/templates/dcim/location_edit.html +2 -38
  44. nautobot/dcim/templates/dcim/location_retrieve.html +249 -0
  45. nautobot/dcim/templates/dcim/location_update.html +38 -0
  46. nautobot/dcim/templates/dcim/module_update.html +1 -0
  47. nautobot/dcim/templates/dcim/modulebay_retrieve.html +93 -1
  48. nautobot/dcim/templates/dcim/modulefamily_retrieve.html +31 -0
  49. nautobot/dcim/templates/dcim/moduletype_retrieve.html +6 -0
  50. nautobot/dcim/templates/dcim/powerfeed_retrieve.html +1 -160
  51. nautobot/dcim/tests/test_api.py +35 -0
  52. nautobot/dcim/tests/test_filters.py +102 -3
  53. nautobot/dcim/tests/test_models.py +146 -0
  54. nautobot/dcim/tests/test_views.py +70 -97
  55. nautobot/dcim/urls.py +4 -22
  56. nautobot/dcim/views.py +439 -153
  57. nautobot/extras/api/views.py +9 -2
  58. nautobot/extras/datasources/git.py +11 -3
  59. nautobot/extras/forms/forms.py +9 -5
  60. nautobot/extras/jobs.py +4 -2
  61. nautobot/extras/models/datasources.py +5 -8
  62. nautobot/extras/models/jobs.py +5 -0
  63. nautobot/extras/plugins/__init__.py +3 -0
  64. nautobot/extras/tables.py +40 -3
  65. nautobot/extras/templates/extras/configcontext.html +2 -220
  66. nautobot/extras/templates/extras/configcontext_edit.html +2 -50
  67. nautobot/extras/templates/extras/configcontext_retrieve.html +2 -0
  68. nautobot/extras/templates/extras/configcontext_update.html +50 -0
  69. nautobot/extras/templates/extras/configcontextschema.html +2 -48
  70. nautobot/extras/templates/extras/configcontextschema_edit.html +2 -19
  71. nautobot/extras/templates/extras/configcontextschema_retrieve.html +48 -0
  72. nautobot/extras/templates/extras/configcontextschema_update.html +19 -0
  73. nautobot/extras/templates/extras/inc/configcontext_data.html +1 -0
  74. nautobot/extras/templates/extras/inc/json_data.html +1 -1
  75. nautobot/extras/templates/extras/inc/json_format.html +2 -2
  76. nautobot/extras/templates/extras/job_edit.html +12 -6
  77. nautobot/extras/templates/extras/tag.html +2 -52
  78. nautobot/extras/templates/extras/tag_edit.html +2 -15
  79. nautobot/extras/templates/extras/tag_retrieve.html +52 -0
  80. nautobot/extras/templates/extras/tag_update.html +15 -0
  81. nautobot/extras/templates/extras/team_retrieve.html +2 -2
  82. nautobot/extras/tests/test_api.py +15 -15
  83. nautobot/extras/tests/test_filters.py +4 -4
  84. nautobot/extras/tests/test_jobs.py +23 -10
  85. nautobot/extras/tests/test_models.py +19 -8
  86. nautobot/extras/tests/test_plugins.py +6 -3
  87. nautobot/extras/tests/test_views.py +66 -11
  88. nautobot/extras/urls.py +4 -134
  89. nautobot/extras/views.py +113 -158
  90. nautobot/ipam/models.py +19 -4
  91. nautobot/ipam/tables.py +19 -0
  92. nautobot/ipam/templates/ipam/vlan.html +2 -84
  93. nautobot/ipam/templates/ipam/vlan_edit.html +2 -24
  94. nautobot/ipam/templates/ipam/vlan_retrieve.html +84 -0
  95. nautobot/ipam/templates/ipam/vlan_update.html +24 -0
  96. nautobot/ipam/tests/test_views.py +5 -0
  97. nautobot/ipam/urls.py +1 -21
  98. nautobot/ipam/views.py +45 -70
  99. nautobot/project-static/docs/404.html +31 -8
  100. nautobot/project-static/docs/apps/index.html +31 -8
  101. nautobot/project-static/docs/apps/nautobot-apps.html +31 -8
  102. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +31 -8
  103. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +31 -8
  104. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +31 -8
  105. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +31 -8
  106. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +31 -8
  107. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +31 -8
  108. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +31 -8
  109. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +31 -8
  110. nautobot/project-static/docs/code-reference/nautobot/apps/events.html +31 -8
  111. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +31 -8
  112. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +31 -8
  113. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +31 -8
  114. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +31 -8
  115. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +31 -8
  116. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +31 -8
  117. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +31 -8
  118. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +31 -8
  119. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +31 -8
  120. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +31 -8
  121. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +120 -8
  122. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +31 -8
  123. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +31 -8
  124. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +31 -8
  125. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +31 -8
  126. nautobot/project-static/docs/development/apps/api/configuration-view.html +31 -8
  127. nautobot/project-static/docs/development/apps/api/database-backend-config.html +31 -8
  128. nautobot/project-static/docs/development/apps/api/models/django-admin.html +31 -8
  129. nautobot/project-static/docs/development/apps/api/models/global-search.html +31 -8
  130. nautobot/project-static/docs/development/apps/api/models/graphql.html +31 -8
  131. nautobot/project-static/docs/development/apps/api/models/index.html +31 -8
  132. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +40 -8
  133. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +31 -8
  134. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +31 -8
  135. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +31 -8
  136. nautobot/project-static/docs/development/apps/api/platform-features/index.html +31 -8
  137. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +31 -8
  138. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +31 -8
  139. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +31 -8
  140. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +32 -9
  141. nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +31 -8
  142. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +31 -8
  143. nautobot/project-static/docs/development/apps/api/prometheus.html +31 -8
  144. nautobot/project-static/docs/development/apps/api/setup.html +31 -8
  145. nautobot/project-static/docs/development/apps/api/testing.html +31 -8
  146. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +31 -8
  147. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +31 -8
  148. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +31 -8
  149. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +31 -8
  150. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +31 -8
  151. nautobot/project-static/docs/development/apps/api/views/base-template.html +31 -8
  152. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +31 -8
  153. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +31 -8
  154. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +31 -8
  155. nautobot/project-static/docs/development/apps/api/views/index.html +31 -8
  156. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +31 -8
  157. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +31 -8
  158. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +31 -8
  159. nautobot/project-static/docs/development/apps/api/views/notes.html +31 -8
  160. nautobot/project-static/docs/development/apps/api/views/rest-api.html +31 -8
  161. nautobot/project-static/docs/development/apps/api/views/urls.html +31 -8
  162. nautobot/project-static/docs/development/apps/index.html +31 -8
  163. nautobot/project-static/docs/development/apps/migration/code-updates.html +31 -8
  164. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +31 -8
  165. nautobot/project-static/docs/development/apps/migration/from-v1.html +31 -8
  166. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +31 -8
  167. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +31 -8
  168. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +31 -8
  169. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +31 -8
  170. nautobot/project-static/docs/development/apps/migration/ui-component-framework/best-practices.html +31 -8
  171. nautobot/project-static/docs/development/apps/migration/ui-component-framework/custom-content.html +31 -8
  172. nautobot/project-static/docs/development/apps/migration/ui-component-framework/index.html +31 -8
  173. nautobot/project-static/docs/development/apps/migration/ui-component-framework/migration-steps.html +31 -8
  174. nautobot/project-static/docs/development/apps/porting-from-netbox.html +31 -8
  175. nautobot/project-static/docs/development/core/application-registry.html +31 -8
  176. nautobot/project-static/docs/development/core/best-practices.html +31 -8
  177. nautobot/project-static/docs/development/core/bootstrap-ui.html +31 -8
  178. nautobot/project-static/docs/development/core/caching.html +31 -8
  179. nautobot/project-static/docs/development/core/controllers.html +31 -8
  180. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +31 -8
  181. nautobot/project-static/docs/development/core/generic-views.html +31 -8
  182. nautobot/project-static/docs/development/core/getting-started.html +31 -8
  183. nautobot/project-static/docs/development/core/homepage.html +31 -8
  184. nautobot/project-static/docs/development/core/index.html +31 -8
  185. nautobot/project-static/docs/development/core/minikube-dev-environment-for-k8s-jobs.html +31 -8
  186. nautobot/project-static/docs/development/core/model-checklist.html +31 -8
  187. nautobot/project-static/docs/development/core/model-features.html +31 -8
  188. nautobot/project-static/docs/development/core/natural-keys.html +31 -8
  189. nautobot/project-static/docs/development/core/navigation-menu.html +31 -8
  190. nautobot/project-static/docs/development/core/release-checklist.html +31 -8
  191. nautobot/project-static/docs/development/core/role-internals.html +31 -8
  192. nautobot/project-static/docs/development/core/settings.html +31 -8
  193. nautobot/project-static/docs/development/core/style-guide.html +31 -8
  194. nautobot/project-static/docs/development/core/templates.html +31 -8
  195. nautobot/project-static/docs/development/core/testing.html +31 -8
  196. nautobot/project-static/docs/development/core/ui-component-framework.html +31 -8
  197. nautobot/project-static/docs/development/core/user-preferences.html +31 -8
  198. nautobot/project-static/docs/development/index.html +31 -8
  199. nautobot/project-static/docs/development/jobs/getting-started.html +35 -8
  200. nautobot/project-static/docs/development/jobs/index.html +31 -8
  201. nautobot/project-static/docs/development/jobs/installation.html +31 -8
  202. nautobot/project-static/docs/development/jobs/job-extensions.html +31 -8
  203. nautobot/project-static/docs/development/jobs/job-logging.html +31 -8
  204. nautobot/project-static/docs/development/jobs/job-patterns.html +31 -8
  205. nautobot/project-static/docs/development/jobs/job-structure.html +31 -8
  206. nautobot/project-static/docs/development/jobs/migration/from-v1.html +31 -8
  207. nautobot/project-static/docs/development/jobs/testing.html +31 -8
  208. nautobot/project-static/docs/index.html +31 -8
  209. nautobot/project-static/docs/insert-analytics.sh +36 -0
  210. nautobot/project-static/docs/objects.inv +0 -0
  211. nautobot/project-static/docs/overview/application_stack.html +31 -8
  212. nautobot/project-static/docs/overview/design_philosophy.html +31 -8
  213. nautobot/project-static/docs/release-notes/index.html +31 -8
  214. nautobot/project-static/docs/release-notes/version-1.0.html +31 -8
  215. nautobot/project-static/docs/release-notes/version-1.1.html +31 -8
  216. nautobot/project-static/docs/release-notes/version-1.2.html +31 -8
  217. nautobot/project-static/docs/release-notes/version-1.3.html +31 -8
  218. nautobot/project-static/docs/release-notes/version-1.4.html +31 -8
  219. nautobot/project-static/docs/release-notes/version-1.5.html +31 -8
  220. nautobot/project-static/docs/release-notes/version-1.6.html +31 -8
  221. nautobot/project-static/docs/release-notes/version-2.0.html +31 -8
  222. nautobot/project-static/docs/release-notes/version-2.1.html +31 -8
  223. nautobot/project-static/docs/release-notes/version-2.2.html +31 -8
  224. nautobot/project-static/docs/release-notes/version-2.3.html +31 -8
  225. nautobot/project-static/docs/release-notes/version-2.4.html +252 -8
  226. nautobot/project-static/docs/search/search_index.json +1 -1
  227. nautobot/project-static/docs/sitemap.xml +302 -298
  228. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  229. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +31 -8
  230. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +31 -8
  231. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +31 -8
  232. nautobot/project-static/docs/user-guide/administration/configuration/index.html +31 -8
  233. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +31 -8
  234. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +31 -8
  235. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +31 -8
  236. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +31 -8
  237. nautobot/project-static/docs/user-guide/administration/guides/docker.html +31 -8
  238. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +31 -8
  239. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +31 -8
  240. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +31 -8
  241. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +31 -8
  242. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +31 -8
  243. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +31 -8
  244. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +31 -8
  245. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +31 -8
  246. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +31 -8
  247. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +31 -8
  248. nautobot/project-static/docs/user-guide/administration/installation/index.html +31 -8
  249. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +31 -8
  250. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +31 -8
  251. nautobot/project-static/docs/user-guide/administration/installation/services.html +31 -8
  252. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +31 -8
  253. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +31 -8
  254. nautobot/project-static/docs/user-guide/administration/security/index.html +31 -8
  255. nautobot/project-static/docs/user-guide/administration/security/notices.html +31 -8
  256. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +31 -8
  257. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +31 -8
  258. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +31 -8
  259. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +31 -8
  260. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +31 -8
  261. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +31 -8
  262. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +31 -8
  263. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +31 -8
  264. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +31 -8
  265. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +31 -8
  266. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +31 -8
  267. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +31 -8
  268. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +31 -8
  269. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +31 -8
  270. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +31 -8
  271. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +31 -8
  272. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +31 -8
  273. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +31 -8
  274. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +31 -8
  275. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +31 -8
  276. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +31 -8
  277. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +31 -8
  278. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +31 -8
  279. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +31 -8
  280. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +31 -8
  281. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +31 -8
  282. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +31 -8
  283. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +31 -8
  284. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +31 -8
  285. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +31 -8
  286. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +31 -8
  287. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +31 -8
  288. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +31 -8
  289. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +31 -8
  290. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +43 -20
  291. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +31 -8
  292. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +31 -8
  293. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +31 -8
  294. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +31 -8
  295. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +31 -8
  296. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +31 -8
  297. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +31 -8
  298. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +31 -8
  299. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +31 -8
  300. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +31 -8
  301. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +35 -8
  302. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +35 -8
  303. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +35 -8
  304. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulefamily.html +10261 -0
  305. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +34 -11
  306. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +31 -8
  307. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +31 -8
  308. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +31 -8
  309. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +31 -8
  310. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +31 -8
  311. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +31 -8
  312. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +31 -8
  313. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +31 -8
  314. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +31 -8
  315. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +31 -8
  316. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +31 -8
  317. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +31 -8
  318. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +31 -8
  319. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +31 -8
  320. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +31 -8
  321. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualdevicecontext.html +31 -8
  322. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +31 -8
  323. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +31 -8
  324. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +31 -8
  325. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +31 -8
  326. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +31 -8
  327. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +31 -8
  328. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +31 -8
  329. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +31 -8
  330. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +31 -8
  331. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +31 -8
  332. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +31 -8
  333. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +31 -8
  334. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +31 -8
  335. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +31 -8
  336. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +31 -8
  337. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +31 -8
  338. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +31 -8
  339. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +31 -8
  340. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +31 -8
  341. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +31 -8
  342. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +31 -8
  343. nautobot/project-static/docs/user-guide/core-data-model/wireless/index.html +31 -8
  344. nautobot/project-static/docs/user-guide/core-data-model/wireless/radioprofile.html +31 -8
  345. nautobot/project-static/docs/user-guide/core-data-model/wireless/supporteddatarate.html +31 -8
  346. nautobot/project-static/docs/user-guide/core-data-model/wireless/wirelessnetwork.html +31 -8
  347. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +31 -8
  348. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +31 -8
  349. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +31 -8
  350. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +31 -8
  351. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +31 -8
  352. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +31 -8
  353. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +31 -8
  354. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +31 -8
  355. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +31 -8
  356. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +31 -8
  357. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +31 -8
  358. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +41 -15
  359. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +31 -8
  360. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +31 -8
  361. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +31 -8
  362. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +31 -8
  363. nautobot/project-static/docs/user-guide/feature-guides/wireless-networks-and-controllers.html +31 -8
  364. nautobot/project-static/docs/user-guide/index.html +31 -8
  365. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +31 -8
  366. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +31 -8
  367. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +31 -8
  368. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +31 -8
  369. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +31 -8
  370. nautobot/project-static/docs/user-guide/platform-functionality/events.html +31 -8
  371. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +31 -8
  372. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +31 -8
  373. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +37 -9
  374. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +31 -8
  375. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +31 -8
  376. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +31 -8
  377. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +31 -8
  378. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +31 -8
  379. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +31 -8
  380. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +31 -8
  381. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +31 -8
  382. nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +31 -8
  383. nautobot/project-static/docs/user-guide/platform-functionality/jobs/managing-jobs.html +31 -8
  384. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +31 -8
  385. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +31 -8
  386. nautobot/project-static/docs/user-guide/platform-functionality/note.html +31 -8
  387. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +31 -8
  388. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +31 -8
  389. nautobot/project-static/docs/user-guide/platform-functionality/rendering-jinja-templates.html +31 -8
  390. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +31 -8
  391. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +31 -8
  392. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +31 -8
  393. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +31 -8
  394. nautobot/project-static/docs/user-guide/platform-functionality/role.html +31 -8
  395. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +31 -8
  396. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +31 -8
  397. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +31 -8
  398. nautobot/project-static/docs/user-guide/platform-functionality/status.html +31 -8
  399. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +31 -8
  400. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +31 -8
  401. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +31 -8
  402. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +31 -8
  403. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +31 -8
  404. nautobot/tenancy/tables.py +2 -0
  405. nautobot/virtualization/tests/test_views.py +1 -1
  406. nautobot/wireless/forms.py +0 -1
  407. nautobot/wireless/models.py +1 -1
  408. nautobot/wireless/tables.py +7 -0
  409. {nautobot-2.4.10.dist-info → nautobot-2.4.11.dist-info}/METADATA +4 -4
  410. {nautobot-2.4.10.dist-info → nautobot-2.4.11.dist-info}/RECORD +416 -401
  411. /nautobot/dcim/templates/dcim/{platform_edit.html → platform_create.html} +0 -0
  412. /nautobot/extras/test_jobs/{pass.py → pass_job.py} +0 -0
  413. {nautobot-2.4.10.dist-info → nautobot-2.4.11.dist-info}/LICENSE.txt +0 -0
  414. {nautobot-2.4.10.dist-info → nautobot-2.4.11.dist-info}/NOTICE +0 -0
  415. {nautobot-2.4.10.dist-info → nautobot-2.4.11.dist-info}/WHEEL +0 -0
  416. {nautobot-2.4.10.dist-info → nautobot-2.4.11.dist-info}/entry_points.txt +0 -0
@@ -34,7 +34,6 @@ class CloudAccountTestCase(ViewTestCases.PrimaryObjectViewTestCase):
34
34
  "provider": providers[1].pk,
35
35
  "secrets_group": secrets_groups[1].pk,
36
36
  "description": "New description",
37
- "comments": "New comments",
38
37
  }
39
38
 
40
39
  def test_post_without_secrets_group(self):
@@ -56,6 +55,12 @@ class CloudAccountTestCase(ViewTestCases.PrimaryObjectViewTestCase):
56
55
 
57
56
  class CloudNetworkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
58
57
  model = CloudNetwork
58
+ custom_action_required_permissions = {
59
+ "cloud:cloudnetwork_children": ["cloud.view_cloudnetwork"],
60
+ "cloud:cloudnetwork_prefixes": ["cloud.view_cloudnetwork", "ipam.view_prefix"],
61
+ "cloud:cloudnetwork_circuits": ["cloud.view_cloudnetwork", "circuits.view_circuit"],
62
+ "cloud:cloudnetwork_cloud_services": ["cloud.view_cloudnetwork", "cloud.view_cloudservice"],
63
+ }
59
64
 
60
65
  @classmethod
61
66
  def setUpTestData(cls):
@@ -104,6 +109,10 @@ class CloudNetworkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
104
109
 
105
110
  class CloudResourceTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
106
111
  model = CloudResourceType
112
+ custom_action_required_permissions = {
113
+ "cloud:cloudresourcetype_services": ["cloud.view_cloudresourcetype", "cloud.view_cloudservice"],
114
+ "cloud:cloudresourcetype_networks": ["cloud.view_cloudresourcetype", "cloud.view_cloudnetwork"],
115
+ }
107
116
 
108
117
  @classmethod
109
118
  def setUpTestData(cls):
@@ -133,6 +142,9 @@ class CloudResourceTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
133
142
 
134
143
  class CloudServiceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
135
144
  model = CloudService
145
+ custom_action_required_permissions = {
146
+ "cloud:cloudservice_cloud_networks": ["cloud.view_cloudservice", "cloud.view_cloudnetwork"],
147
+ }
136
148
 
137
149
  @classmethod
138
150
  def setUpTestData(cls):
nautobot/cloud/views.py CHANGED
@@ -64,7 +64,6 @@ class CloudNetworkUIViewSet(NautobotUIViewSet):
64
64
  table_class = CloudNetworkTable
65
65
  form_class = CloudNetworkForm
66
66
  bulk_update_form_class = CloudNetworkBulkEditForm
67
-
68
67
  object_detail_content = object_detail.ObjectDetailContent(
69
68
  panels=(
70
69
  object_detail.ObjectFieldsPanel(
@@ -153,19 +152,35 @@ class CloudNetworkUIViewSet(NautobotUIViewSet):
153
152
  ),
154
153
  )
155
154
 
156
- @action(detail=True, url_path="children")
155
+ @action(detail=True, url_path="children", custom_view_base_action="view")
157
156
  def children(self, request, *args, **kwargs):
158
157
  return Response({})
159
158
 
160
- @action(detail=True, url_path="prefixes")
159
+ @action(
160
+ detail=True,
161
+ url_path="prefixes",
162
+ custom_view_base_action="view",
163
+ custom_view_additional_permissions=["ipam.view_prefix"],
164
+ )
161
165
  def prefixes(self, request, *args, **kwargs):
162
166
  return Response({})
163
167
 
164
- @action(detail=True, url_path="circuits")
168
+ @action(
169
+ detail=True,
170
+ url_path="circuits",
171
+ custom_view_base_action="view",
172
+ custom_view_additional_permissions=["circuits.view_circuit"],
173
+ )
165
174
  def circuits(self, request, *args, **kwargs):
166
175
  return Response({})
167
176
 
168
- @action(detail=True, url_path="cloud-services", url_name="cloud_services")
177
+ @action(
178
+ detail=True,
179
+ url_path="cloud-services",
180
+ url_name="cloud_services",
181
+ custom_view_base_action="view",
182
+ custom_view_additional_permissions=["cloud.view_cloudservice"],
183
+ )
169
184
  def cloud_services(self, request, *args, **kwargs):
170
185
  return Response({})
171
186
 
@@ -178,7 +193,6 @@ class CloudResourceTypeUIViewSet(NautobotUIViewSet):
178
193
  table_class = CloudResourceTypeTable
179
194
  form_class = CloudResourceTypeForm
180
195
  bulk_update_form_class = CloudResourceTypeBulkEditForm
181
-
182
196
  object_detail_content = object_detail.ObjectDetailContent(
183
197
  panels=(
184
198
  object_detail.ObjectFieldsPanel(
@@ -230,11 +244,21 @@ class CloudResourceTypeUIViewSet(NautobotUIViewSet):
230
244
  ),
231
245
  )
232
246
 
233
- @action(detail=True, url_path="networks")
247
+ @action(
248
+ detail=True,
249
+ url_path="networks",
250
+ custom_view_base_action="view",
251
+ custom_view_additional_permissions=["cloud.view_cloudnetwork"],
252
+ )
234
253
  def networks(self, request, *args, **kwargs):
235
254
  return Response({})
236
255
 
237
- @action(detail=True, url_path="services")
256
+ @action(
257
+ detail=True,
258
+ url_path="services",
259
+ custom_view_base_action="view",
260
+ custom_view_additional_permissions=["cloud.view_cloudservice"],
261
+ )
238
262
  def services(self, request, *args, **kwargs):
239
263
  return Response({})
240
264
 
@@ -284,6 +308,12 @@ class CloudServiceUIViewSet(NautobotUIViewSet):
284
308
  ),
285
309
  )
286
310
 
287
- @action(detail=True, url_path="cloud-networks", url_name="cloud_networks")
311
+ @action(
312
+ detail=True,
313
+ url_path="cloud-networks",
314
+ url_name="cloud_networks",
315
+ custom_view_base_action="view",
316
+ custom_view_additional_permissions=["cloud.view_cloudnetwork"],
317
+ )
288
318
  def cloud_networks(self, request, *args, **kwargs):
289
319
  return Response({})
@@ -3,10 +3,12 @@ import logging
3
3
  import os
4
4
  from pathlib import Path
5
5
  import shutil
6
+ import sys
6
7
 
7
8
  from celery import Celery, shared_task, signals
8
9
  from celery.app.log import TaskFormatter
9
10
  from celery.utils.log import get_logger
11
+ from django.apps import apps
10
12
  from django.conf import settings
11
13
  from django.db.utils import OperationalError, ProgrammingError
12
14
  from django.utils.functional import SimpleLazyObject
@@ -19,6 +21,7 @@ from nautobot.core.celery.control import discard_git_repository, refresh_git_rep
19
21
  from nautobot.core.celery.encoders import NautobotKombuJSONEncoder
20
22
  from nautobot.core.celery.log import NautobotDatabaseHandler
21
23
  from nautobot.core.utils.module_loading import import_modules_privately
24
+ from nautobot.extras.plugins.utils import import_object
22
25
  from nautobot.extras.registry import registry
23
26
 
24
27
  logger = logging.getLogger(__name__)
@@ -47,12 +50,14 @@ app.autodiscover_tasks()
47
50
  def import_jobs(sender=None, **kwargs):
48
51
  """
49
52
  Import system Jobs into Nautobot as well as Jobs from JOBS_ROOT and GIT_ROOT.
53
+ Import app-provided jobs if the app provides dynamic jobs.
50
54
 
51
55
  Note that app-provided jobs are automatically imported at startup time via NautobotAppConfig.ready()
52
56
  """
53
57
  import nautobot.core.jobs # noqa: F401
54
58
 
55
59
  _import_jobs_from_jobs_root()
60
+ _import_dynamic_jobs_from_apps()
56
61
 
57
62
  try:
58
63
  _import_jobs_from_git_repositories()
@@ -120,6 +125,22 @@ def _import_jobs_from_git_repositories():
120
125
  refresh_git_repository(state=None, repository_pk=repo.pk, head=repo.current_head)
121
126
 
122
127
 
128
+ def _import_dynamic_jobs_from_apps():
129
+ for app_name in settings.PLUGINS:
130
+ app_config = apps.get_app_config(app_name)
131
+ if not getattr(app_config, "provides_dynamic_jobs", False):
132
+ continue
133
+
134
+ # Unload job modules from sys.modules if they were previously loaded
135
+ app_jobs = getattr(app_config, "features", {}).get("jobs", [])
136
+ for job in app_jobs:
137
+ if job.__module__ in sys.modules:
138
+ del sys.modules[job.__module__]
139
+
140
+ # Load app jobs
141
+ app_config.features["jobs"] = import_object(f"{app_config.__module__}.{app_config.jobs}")
142
+
143
+
123
144
  def add_nautobot_log_handler(logger_instance, log_format=None):
124
145
  """Add NautobotDatabaseHandler to logger and update logger level filtering to send all log levels to our handler."""
125
146
  if any(isinstance(h, NautobotDatabaseHandler) for h in logger_instance.handlers):
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ from zoneinfo import ZoneInfo
2
3
 
3
4
  from django.db import models
4
5
  from rest_framework.utils.encoders import JSONEncoder
@@ -52,5 +53,7 @@ class NautobotKombuJSONEncoder(JSONEncoder):
52
53
  # JobResult.result uses NautobotKombuJSONEncoder as an encoder and expects a JSONSerializable object,
53
54
  # although an exception, such as a RuntimeException, can be supplied as the obj.
54
55
  return f"{obj.__class__.__name__}: {obj}"
56
+ elif isinstance(obj, ZoneInfo):
57
+ return obj.key
55
58
  else:
56
59
  return super().default(obj)
@@ -138,7 +138,10 @@ class BulkEditForm(forms.Form):
138
138
  # Handle M2M Save
139
139
  for key in self.cleaned_data.keys():
140
140
  if key.startswith(("add_", "remove_")):
141
- field_name = key.lstrip("add_")
141
+ if key.startswith("add_"):
142
+ field_name = key.lstrip("add_")
143
+ else:
144
+ field_name = key.lstrip("remove_")
142
145
  if field_name in m2m_field_names:
143
146
  continue
144
147
  with contextlib.suppress(FieldDoesNotExist):
@@ -96,12 +96,11 @@ class BulkEditObjects(Job):
96
96
  model_field = None
97
97
 
98
98
  # Handle nullification
99
- if nullified_fields:
100
- if field_name in form.nullable_fields and field_name in nullified_fields:
101
- if isinstance(model_field, ManyToManyField):
102
- getattr(obj, field_name).set([])
103
- else:
104
- setattr(obj, field_name, None if model_field is not None and model_field.null else "")
99
+ if nullified_fields and field_name in nullified_fields and field_name in form.nullable_fields:
100
+ if isinstance(model_field, ManyToManyField):
101
+ getattr(obj, field_name).set([])
102
+ else:
103
+ setattr(obj, field_name, None if model_field is not None and model_field.null else "")
105
104
 
106
105
  # ManyToManyFields
107
106
  elif isinstance(model_field, ManyToManyField):
@@ -109,7 +108,8 @@ class BulkEditObjects(Job):
109
108
  getattr(obj, field_name).set(form.cleaned_data[field_name])
110
109
  # Normal fields
111
110
  elif form.cleaned_data[field_name] not in (None, "", []):
112
- setattr(obj, field_name, form.cleaned_data[field_name])
111
+ if hasattr(obj, field_name):
112
+ setattr(obj, field_name, form.cleaned_data[field_name])
113
113
 
114
114
  # Update custom fields
115
115
  for field_name in form_custom_fields:
@@ -121,7 +121,7 @@ class BulkEditObjects(Job):
121
121
  obj.full_clean()
122
122
  obj.save()
123
123
  updated_objects_pk.append(obj.pk)
124
- form.post_save(obj)
124
+ form.post_save(obj) # handles M2M add_* and remove_* form fields
125
125
 
126
126
  if hasattr(form, "save_relationships") and callable(form.save_relationships):
127
127
  # Add/remove relationship associations
@@ -67,6 +67,17 @@ class LogsCleanup(Job):
67
67
  cascade_queryset = related_model.objects.filter(**{f"{related_field_name}__id__in": queryset})
68
68
  self.recursive_delete_with_cascade(cascade_queryset, deletion_summary)
69
69
 
70
+ genericrelation_related_fields = [
71
+ field for field in queryset.model._meta.private_fields if hasattr(field, "bulk_related_objects")
72
+ ]
73
+ for gr_related_field in genericrelation_related_fields:
74
+ related_model = gr_related_field.related_model
75
+ if related_model == queryset.model:
76
+ continue # avoid infinite recursion # TODO: this won't catch A<-B<-A<-B loops...
77
+ related_field_name = gr_related_field.related_query_name()
78
+ cascade_queryset = related_model.objects.filter(**{f"{related_field_name}__id__in": queryset})
79
+ self.recursive_delete_with_cascade(cascade_queryset, deletion_summary)
80
+
70
81
  deleted_count = queryset._raw_delete(using="default")
71
82
  if deleted_count:
72
83
  deletion_summary.update({queryset.model._meta.label: deleted_count})
@@ -225,7 +225,8 @@ class Command(BaseCommand):
225
225
  _create_batch(InterfaceTemplateFactory, 30)
226
226
  _create_batch(PowerPortTemplateFactory, 30)
227
227
  _create_batch(PowerOutletTemplateFactory, 30)
228
- _create_batch(ModuleBayTemplateFactory, 90)
228
+ _create_batch(ModuleBayTemplateFactory, 60, description="without module families", has_module_family=False)
229
+ _create_batch(ModuleBayTemplateFactory, 30, description="with module families", has_module_family=True)
229
230
  _create_batch(ManufacturerFactory, 2, description="without Platforms or DeviceTypes") # Last 2 hard-coded
230
231
  _create_batch(DeviceRedundancyGroupFactory, 20)
231
232
  _create_batch(DeviceFactory, 20)
@@ -144,7 +144,7 @@
144
144
  <div class="col-md-6">
145
145
  {% block content_left_page %}{% endblock content_left_page %}
146
146
  {% include 'inc/custom_fields/panel.html' with custom_fields=object.get_custom_field_groupings_basic custom_fields_advanced_ui=False computed_fields=object.get_computed_fields_grouping_basic computed_fields_advanced_ui=False %}
147
- {% include 'inc/relationships_panel.html' %}
147
+ {% include 'inc/relationships/panel_override.html' with relationships_fields_override=object.get_relationships_data_basic_fields %}
148
148
  {% include 'extras/inc/tags_panel.html' %}
149
149
  {% plugin_left_page object %}
150
150
  </div>
@@ -5,7 +5,7 @@ from django.apps import apps
5
5
  from django.contrib.auth import get_user_model
6
6
  from django.contrib.contenttypes.models import ContentType
7
7
  from django.core.cache import cache
8
- from django.core.exceptions import FieldDoesNotExist
8
+ from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist
9
9
  from django.db import connections, DEFAULT_DB_ALIAS
10
10
  from django.db.models import JSONField, ManyToManyField, ManyToManyRel
11
11
  from django.forms.models import model_to_dict
@@ -161,6 +161,24 @@ class NautobotTestCaseMixin:
161
161
  obj_perm.users.add(self.user)
162
162
  obj_perm.object_types.add(ct)
163
163
 
164
+ def remove_permissions(self, *names, **kwargs):
165
+ """
166
+ Remove a set of permissions. Accepts permission names in the form <app>.<action>_<model>.
167
+ Additional keyword arguments will be passed to the ObjectPermission constructor to allow creating more detailed permissions.
168
+
169
+ Examples:
170
+ >>> remove_permissions("ipam.add_vlangroup", "ipam.view_vlangroup")
171
+ >>> remove_permissions("ipam.add_vlangroup", "ipam.view_vlangroup", constraints={"pk": "uuid-1234"})
172
+ """
173
+ for name in names:
174
+ _, action, _ = permissions.resolve_permission(name)
175
+ try:
176
+ obj_perm = users_models.ObjectPermission.objects.get(name=name, actions=[action], **kwargs)
177
+ obj_perm.delete()
178
+ except ObjectDoesNotExist:
179
+ # Permission does not exist, so nothing to remove
180
+ pass
181
+
164
182
  #
165
183
  # Custom assertions
166
184
  #
@@ -1,7 +1,7 @@
1
1
  import contextlib
2
2
  import re
3
3
  from typing import Optional, Sequence
4
- from unittest import skipIf
4
+ from unittest import mock, skipIf
5
5
  import uuid
6
6
 
7
7
  from django.apps import apps
@@ -9,6 +9,7 @@ from django.conf import settings
9
9
  from django.contrib.contenttypes.models import ContentType
10
10
  from django.core.exceptions import ObjectDoesNotExist, ValidationError
11
11
  from django.core.validators import URLValidator
12
+ from django.db.models import ManyToManyField, Model, QuerySet
12
13
  from django.test import override_settings, tag, TestCase as _TestCase
13
14
  from django.urls import NoReverseMatch, reverse
14
15
  from django.utils.html import escape
@@ -16,6 +17,7 @@ from django.utils.http import urlencode
16
17
  from django.utils.text import slugify
17
18
  from tree_queries.models import TreeNode
18
19
 
20
+ from nautobot.core.jobs.bulk_actions import BulkEditObjects
19
21
  from nautobot.core.models.generics import PrimaryModel
20
22
  from nautobot.core.models.tree_queries import TreeModel
21
23
  from nautobot.core.templatetags import helpers
@@ -141,6 +143,8 @@ class ViewTestCases:
141
143
  Retrieve a single instance.
142
144
  """
143
145
 
146
+ custom_action_required_permissions = {}
147
+
144
148
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
145
149
  def test_get_object_anonymous(self):
146
150
  # Make the request as an unauthenticated user
@@ -246,6 +250,21 @@ class ViewTestCases:
246
250
  self.assertBodyContains(response, f"{instance.get_absolute_url()}#advanced")
247
251
  self.assertBodyContains(response, "Advanced")
248
252
 
253
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
254
+ def test_custom_actions(self):
255
+ instance = self._get_queryset().first()
256
+ for url_name, required_permissions in self.custom_action_required_permissions.items():
257
+ url = reverse(url_name, kwargs={"pk": instance.pk})
258
+ self.assertHttpStatus(self.client.get(url), 403)
259
+ for permission in required_permissions[:-1]:
260
+ self.add_permissions(permission)
261
+ self.assertHttpStatus(self.client.get(url), 403)
262
+
263
+ self.add_permissions(required_permissions[-1])
264
+ self.assertHttpStatus(self.client.get(url), 200)
265
+ # delete the permissions here so that repetitive calls to add_permissions do not create duplicate permissions.
266
+ self.remove_permissions(*required_permissions)
267
+
249
268
  class GetObjectChangelogViewTestCase(ModelViewTestCase):
250
269
  """
251
270
  View the changelog for an instance.
@@ -769,6 +788,17 @@ class ViewTestCases:
769
788
  response_body = response.content.decode(response.charset)
770
789
  self.assertIn("/login/?next=" + self._get_url("list"), response_body, msg=response_body)
771
790
 
791
+ def test_list_objects_anonymous_with_exempt_permission_for_one_view_only(self):
792
+ # Make the request as an unauthenticated user
793
+ self.client.logout()
794
+ # Test if AnonymousUser can properly render the whole list view
795
+ with override_settings(EXEMPT_VIEW_PERMISSIONS=[self.model._meta.label_lower]):
796
+ response = self.client.get(self._get_url("list"))
797
+ self.assertHttpStatus(response, 200)
798
+ # There should be some rows
799
+ self.assertBodyContains(response, '<tr class="even')
800
+ self.assertBodyContains(response, '<tr class="odd')
801
+
772
802
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
773
803
  def test_list_objects_filtered(self):
774
804
  instance1, instance2 = self._get_queryset().all()[:2]
@@ -1049,6 +1079,7 @@ class ViewTestCases:
1049
1079
  def validate_redirect_to_job_result(self, response):
1050
1080
  # Get the last Bulk Edit Objects JobResult created
1051
1081
  job_result = JobResult.objects.filter(name="Bulk Edit Objects").first()
1082
+ self.assertIsNotNone(job_result, "No JobResult was created - likely the bulk_edit_data is invalid!")
1052
1083
  # Assert redirect to Job Results
1053
1084
  self.assertRedirects(
1054
1085
  response,
@@ -1082,8 +1113,71 @@ class ViewTestCases:
1082
1113
  # Assign model-level permission
1083
1114
  self.add_permissions(f"{self.model._meta.app_label}.change_{self.model._meta.model_name}")
1084
1115
 
1085
- response = self.client.post(self._get_url("bulk_edit"), data)
1086
- self.validate_redirect_to_job_result(response)
1116
+ with mock.patch.object(JobResult, "enqueue_job", wraps=JobResult.enqueue_job) as mock_enqueue_job:
1117
+ response = self.client.post(self._get_url("bulk_edit"), data)
1118
+ self.validate_redirect_to_job_result(response)
1119
+ mock_enqueue_job.assert_called()
1120
+
1121
+ # Verify that the provided self.bulk_edit_data was passed through correctly to the job.
1122
+ # The below is a bit gross because of multiple layers of data encoding and decoding involved. Sorry!
1123
+ job_form = BulkEditObjects.as_form(BulkEditObjects.deserialize_data(mock_enqueue_job.call_args.kwargs))
1124
+ job_form.is_valid()
1125
+ job_kwargs = job_form.cleaned_data
1126
+
1127
+ bulk_edit_form_class = lookup.get_form_for_model(self.model, form_prefix="BulkEdit")
1128
+ bulk_edit_form = bulk_edit_form_class(self.model, job_kwargs["form_data"])
1129
+ bulk_edit_form.is_valid()
1130
+ passed_bulk_edit_data = bulk_edit_form.cleaned_data
1131
+
1132
+ for key, value in self.bulk_edit_data.items():
1133
+ with self.subTest(key=key):
1134
+ if isinstance(passed_bulk_edit_data.get(key), Model):
1135
+ self.assertEqual(passed_bulk_edit_data.get(key).pk, value)
1136
+ elif isinstance(passed_bulk_edit_data.get(key), QuerySet):
1137
+ self.assertEqual(
1138
+ sorted(passed_bulk_edit_data.get(key).values_list("pk", flat=True)), sorted(value)
1139
+ )
1140
+ else:
1141
+ self.assertEqual(passed_bulk_edit_data.get(key), bulk_edit_form.fields[key].clean(value))
1142
+
1143
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1144
+ def test_bulk_edit_objects_nullable_fields(self):
1145
+ """Assert that "set null" fields on the bulk-edit form are correctly passed through to the job."""
1146
+ bulk_edit_form_class = lookup.get_form_for_model(self.model, form_prefix="BulkEdit")
1147
+ bulk_edit_form = bulk_edit_form_class(self.model)
1148
+ if not getattr(bulk_edit_form, "nullable_fields", ()):
1149
+ self.skipTest(f"no nullable fields on {bulk_edit_form_class}")
1150
+
1151
+ for field_name in bulk_edit_form.nullable_fields:
1152
+ with self.subTest(field_name=field_name):
1153
+ if field_name.startswith("cf_"):
1154
+ # TODO check whether customfield is nullable
1155
+ continue
1156
+ if field_name.startswith("cr_"):
1157
+ # TODO check whether relationship is required
1158
+ continue
1159
+ model_field = self.model._meta.get_field(field_name)
1160
+ if isinstance(model_field, ManyToManyField):
1161
+ # always nullable
1162
+ continue
1163
+ self.assertTrue(model_field.null or model_field.blank)
1164
+
1165
+ pk_list = list(self._get_queryset().values_list("pk", flat=True)[:3])
1166
+ data = {
1167
+ "pk": pk_list,
1168
+ "_apply": True, # Form button
1169
+ "_nullify": list(bulk_edit_form.nullable_fields),
1170
+ }
1171
+
1172
+ # Assign model-level permission
1173
+ self.add_permissions(f"{self.model._meta.app_label}.change_{self.model._meta.model_name}")
1174
+
1175
+ with mock.patch.object(JobResult, "enqueue_job", wraps=JobResult.enqueue_job) as mock_enqueue_job:
1176
+ response = self.client.post(self._get_url("bulk_edit"), data)
1177
+ self.validate_redirect_to_job_result(response)
1178
+ mock_enqueue_job.assert_called()
1179
+
1180
+ self.assertEqual(mock_enqueue_job.call_args.kwargs["form_data"].get("_nullify"), data["_nullify"])
1087
1181
 
1088
1182
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1089
1183
  def test_bulk_edit_form_contains_all_pks(self):
@@ -1182,11 +1276,13 @@ class ViewTestCases:
1182
1276
  }
1183
1277
  data.update(utils.post_data(self.bulk_edit_data))
1184
1278
 
1185
- # Attempt to bulk edit permitted objects into a non-permitted state
1186
- response = self.client.post(self._get_url("bulk_edit"), data)
1187
- # NOTE: There is no way of testing constrained failure as bulk edit is a system Job;
1188
- # and can only be tested by checking JobLogs.
1189
- self.validate_redirect_to_job_result(response)
1279
+ with mock.patch.object(JobResult, "enqueue_job", wraps=JobResult.enqueue_job) as mock_enqueue_job:
1280
+ # Attempt to bulk edit permitted objects into a non-permitted state
1281
+ response = self.client.post(self._get_url("bulk_edit"), data)
1282
+ # NOTE: There is no way of testing constrained failure as bulk edit is a system Job;
1283
+ # and can only be tested by checking JobLogs.
1284
+ self.validate_redirect_to_job_result(response)
1285
+ mock_enqueue_job.assert_called()
1190
1286
 
1191
1287
  class BulkDeleteObjectsViewTestCase(ModelViewTestCase):
1192
1288
  """
@@ -689,10 +689,10 @@ class LogsCleanupTestCase(TransactionTestCase):
689
689
  cleanup_types=[CleanupTypes.JOB_RESULT],
690
690
  max_age=60,
691
691
  )
692
- self.assertFalse(JobResult.objects.filter(date_done__lt=cutoff).exists())
693
- self.assertTrue(JobResult.objects.filter(date_done__gte=cutoff).exists())
694
- self.assertTrue(ObjectChange.objects.filter(time__lt=cutoff).exists())
695
- self.assertTrue(ObjectChange.objects.filter(time__gte=cutoff).exists())
692
+ self.assertFalse(JobResult.objects.filter(date_done__lt=cutoff).exists(), cm.output)
693
+ self.assertTrue(JobResult.objects.filter(date_done__gte=cutoff).exists(), cm.output)
694
+ self.assertTrue(ObjectChange.objects.filter(time__lt=cutoff).exists(), cm.output)
695
+ self.assertTrue(ObjectChange.objects.filter(time__gte=cutoff).exists(), cm.output)
696
696
 
697
697
  started_logs = {
698
698
  "job_result_id": str(job_result.id),
@@ -840,6 +840,22 @@ class BulkEditTestCase(TransactionTestCase):
840
840
  )
841
841
  self._common_no_error_test_assertion(Role, job_result, Role.objects.all().count(), color="aa1409")
842
842
 
843
+ def test_bulk_edit_objects_nullify(self):
844
+ """
845
+ Bulk edit Role instances to nullify their weight.
846
+ """
847
+ self.add_permissions("extras.change_role", "extras.view_role")
848
+ job_result = create_job_result_and_run_job(
849
+ "nautobot.core.jobs.bulk_actions",
850
+ "BulkEditObjects",
851
+ content_type=self.role_ct.id,
852
+ edit_all=True,
853
+ filter_query_params={},
854
+ form_data={"_nullify": ["weight"]},
855
+ username=self.user.username,
856
+ )
857
+ self._common_no_error_test_assertion(Role, job_result, Role.objects.all().count(), weight__isnull=True)
858
+
843
859
  def test_bulk_edit_select_some(self):
844
860
  """
845
861
  Bulk edit selected Namespace instances.