nautobot 2.4.9__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 (433) hide show
  1. nautobot/cloud/tests/test_views.py +13 -1
  2. nautobot/cloud/views.py +39 -9
  3. nautobot/core/api/parsers.py +56 -2
  4. nautobot/core/celery/__init__.py +21 -0
  5. nautobot/core/celery/encoders.py +3 -0
  6. nautobot/core/forms/forms.py +4 -1
  7. nautobot/core/jobs/bulk_actions.py +8 -8
  8. nautobot/core/jobs/cleanup.py +11 -0
  9. nautobot/core/management/commands/generate_test_data.py +2 -1
  10. nautobot/core/models/__init__.py +2 -0
  11. nautobot/core/templates/generic/object_retrieve.html +1 -1
  12. nautobot/core/testing/mixins.py +19 -1
  13. nautobot/core/testing/views.py +104 -8
  14. nautobot/core/tests/test_csv.py +92 -1
  15. nautobot/core/tests/test_jinja_filters.py +59 -0
  16. nautobot/core/tests/test_jobs.py +20 -4
  17. nautobot/core/tests/test_utils.py +193 -0
  18. nautobot/core/tests/test_views.py +73 -0
  19. nautobot/core/tests/test_views_utils.py +53 -2
  20. nautobot/core/ui/object_detail.py +4 -0
  21. nautobot/core/urls.py +2 -2
  22. nautobot/core/utils/lookup.py +4 -2
  23. nautobot/core/utils/module_loading.py +86 -58
  24. nautobot/core/views/__init__.py +21 -0
  25. nautobot/core/views/generic.py +2 -12
  26. nautobot/core/views/mixins.py +19 -1
  27. nautobot/core/views/renderers.py +4 -13
  28. nautobot/core/views/utils.py +16 -0
  29. nautobot/dcim/api/serializers.py +13 -0
  30. nautobot/dcim/api/urls.py +1 -0
  31. nautobot/dcim/api/views.py +20 -0
  32. nautobot/dcim/apps.py +1 -0
  33. nautobot/dcim/factory.py +11 -0
  34. nautobot/dcim/filters/__init__.py +110 -0
  35. nautobot/dcim/forms.py +205 -19
  36. nautobot/dcim/migrations/0070_modulefamily_models.py +92 -0
  37. nautobot/dcim/models/__init__.py +2 -0
  38. nautobot/dcim/models/device_component_templates.py +18 -0
  39. nautobot/dcim/models/device_components.py +25 -1
  40. nautobot/dcim/models/devices.py +68 -0
  41. nautobot/dcim/navigation.py +16 -0
  42. nautobot/dcim/tables/__init__.py +2 -0
  43. nautobot/dcim/tables/devices.py +48 -0
  44. nautobot/dcim/tables/devicetypes.py +35 -1
  45. nautobot/dcim/tables/template_code.py +2 -0
  46. nautobot/dcim/templates/dcim/controllermanageddevicegroup_retrieve.html +1 -90
  47. nautobot/dcim/templates/dcim/inc/cable_toggle_buttons.html +1 -1
  48. nautobot/dcim/templates/dcim/interfaceredundancygroup_retrieve.html +1 -63
  49. nautobot/dcim/templates/dcim/location.html +2 -249
  50. nautobot/dcim/templates/dcim/location_edit.html +2 -38
  51. nautobot/dcim/templates/dcim/location_retrieve.html +249 -0
  52. nautobot/dcim/templates/dcim/location_update.html +38 -0
  53. nautobot/dcim/templates/dcim/module_update.html +1 -0
  54. nautobot/dcim/templates/dcim/modulebay_retrieve.html +93 -1
  55. nautobot/dcim/templates/dcim/modulefamily_retrieve.html +31 -0
  56. nautobot/dcim/templates/dcim/moduletype_retrieve.html +6 -0
  57. nautobot/dcim/templates/dcim/powerfeed_retrieve.html +1 -160
  58. nautobot/dcim/tests/test_api.py +35 -0
  59. nautobot/dcim/tests/test_filters.py +102 -3
  60. nautobot/dcim/tests/test_models.py +146 -0
  61. nautobot/dcim/tests/test_views.py +70 -97
  62. nautobot/dcim/urls.py +4 -22
  63. nautobot/dcim/views.py +439 -153
  64. nautobot/extras/api/views.py +9 -2
  65. nautobot/extras/context_managers.py +2 -2
  66. nautobot/extras/datasources/git.py +11 -3
  67. nautobot/extras/forms/forms.py +9 -5
  68. nautobot/extras/jobs.py +4 -2
  69. nautobot/extras/models/customfields.py +2 -0
  70. nautobot/extras/models/datasources.py +13 -8
  71. nautobot/extras/models/groups.py +18 -0
  72. nautobot/extras/models/jobs.py +19 -0
  73. nautobot/extras/models/metadata.py +2 -0
  74. nautobot/extras/models/models.py +4 -0
  75. nautobot/extras/models/secrets.py +7 -0
  76. nautobot/extras/plugins/__init__.py +3 -0
  77. nautobot/extras/secrets/__init__.py +14 -0
  78. nautobot/extras/tables.py +40 -3
  79. nautobot/extras/templates/extras/configcontext.html +2 -220
  80. nautobot/extras/templates/extras/configcontext_edit.html +2 -50
  81. nautobot/extras/templates/extras/configcontext_retrieve.html +2 -0
  82. nautobot/extras/templates/extras/configcontext_update.html +50 -0
  83. nautobot/extras/templates/extras/configcontextschema.html +2 -48
  84. nautobot/extras/templates/extras/configcontextschema_edit.html +2 -19
  85. nautobot/extras/templates/extras/configcontextschema_retrieve.html +48 -0
  86. nautobot/extras/templates/extras/configcontextschema_update.html +19 -0
  87. nautobot/extras/templates/extras/inc/configcontext_data.html +1 -0
  88. nautobot/extras/templates/extras/inc/json_data.html +1 -1
  89. nautobot/extras/templates/extras/inc/json_format.html +2 -2
  90. nautobot/extras/templates/extras/job_edit.html +12 -6
  91. nautobot/extras/templates/extras/tag.html +2 -52
  92. nautobot/extras/templates/extras/tag_edit.html +2 -15
  93. nautobot/extras/templates/extras/tag_retrieve.html +52 -0
  94. nautobot/extras/templates/extras/tag_update.html +15 -0
  95. nautobot/extras/templates/extras/team_retrieve.html +2 -2
  96. nautobot/extras/tests/test_api.py +15 -15
  97. nautobot/extras/tests/test_context_managers.py +20 -0
  98. nautobot/extras/tests/test_filters.py +4 -4
  99. nautobot/extras/tests/test_jobs.py +23 -10
  100. nautobot/extras/tests/test_models.py +45 -8
  101. nautobot/extras/tests/test_plugins.py +6 -3
  102. nautobot/extras/tests/test_views.py +66 -11
  103. nautobot/extras/urls.py +4 -134
  104. nautobot/extras/views.py +113 -158
  105. nautobot/ipam/models.py +51 -4
  106. nautobot/ipam/tables.py +19 -0
  107. nautobot/ipam/templates/ipam/vlan.html +2 -84
  108. nautobot/ipam/templates/ipam/vlan_edit.html +2 -24
  109. nautobot/ipam/templates/ipam/vlan_retrieve.html +84 -0
  110. nautobot/ipam/templates/ipam/vlan_update.html +24 -0
  111. nautobot/ipam/tests/test_views.py +5 -0
  112. nautobot/ipam/urls.py +1 -21
  113. nautobot/ipam/views.py +45 -70
  114. nautobot/project-static/docs/404.html +31 -8
  115. nautobot/project-static/docs/apps/index.html +31 -8
  116. nautobot/project-static/docs/apps/nautobot-apps.html +31 -8
  117. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +31 -8
  118. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +31 -8
  119. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +31 -8
  120. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +31 -8
  121. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +31 -8
  122. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +31 -8
  123. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +31 -8
  124. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +31 -8
  125. nautobot/project-static/docs/code-reference/nautobot/apps/events.html +31 -8
  126. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +31 -8
  127. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +31 -8
  128. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +31 -8
  129. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +31 -8
  130. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +31 -8
  131. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +31 -8
  132. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +31 -8
  133. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +31 -8
  134. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +31 -8
  135. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +31 -8
  136. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +120 -8
  137. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +31 -8
  138. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +31 -8
  139. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +31 -8
  140. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +31 -8
  141. nautobot/project-static/docs/development/apps/api/configuration-view.html +31 -8
  142. nautobot/project-static/docs/development/apps/api/database-backend-config.html +31 -8
  143. nautobot/project-static/docs/development/apps/api/models/django-admin.html +31 -8
  144. nautobot/project-static/docs/development/apps/api/models/global-search.html +31 -8
  145. nautobot/project-static/docs/development/apps/api/models/graphql.html +31 -8
  146. nautobot/project-static/docs/development/apps/api/models/index.html +31 -8
  147. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +40 -8
  148. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +31 -8
  149. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +31 -8
  150. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +31 -8
  151. nautobot/project-static/docs/development/apps/api/platform-features/index.html +31 -8
  152. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +31 -8
  153. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +31 -8
  154. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +31 -8
  155. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +70 -46
  156. nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +31 -8
  157. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +31 -8
  158. nautobot/project-static/docs/development/apps/api/prometheus.html +31 -8
  159. nautobot/project-static/docs/development/apps/api/setup.html +31 -8
  160. nautobot/project-static/docs/development/apps/api/testing.html +31 -8
  161. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +31 -8
  162. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +31 -8
  163. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +31 -8
  164. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +31 -8
  165. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +31 -8
  166. nautobot/project-static/docs/development/apps/api/views/base-template.html +31 -8
  167. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +31 -8
  168. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +31 -8
  169. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +31 -8
  170. nautobot/project-static/docs/development/apps/api/views/index.html +31 -8
  171. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +31 -8
  172. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +31 -8
  173. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +31 -8
  174. nautobot/project-static/docs/development/apps/api/views/notes.html +31 -8
  175. nautobot/project-static/docs/development/apps/api/views/rest-api.html +31 -8
  176. nautobot/project-static/docs/development/apps/api/views/urls.html +31 -8
  177. nautobot/project-static/docs/development/apps/index.html +31 -8
  178. nautobot/project-static/docs/development/apps/migration/code-updates.html +31 -8
  179. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +31 -8
  180. nautobot/project-static/docs/development/apps/migration/from-v1.html +31 -8
  181. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +31 -8
  182. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +31 -8
  183. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +31 -8
  184. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +31 -8
  185. nautobot/project-static/docs/development/apps/migration/ui-component-framework/best-practices.html +31 -8
  186. nautobot/project-static/docs/development/apps/migration/ui-component-framework/custom-content.html +31 -8
  187. nautobot/project-static/docs/development/apps/migration/ui-component-framework/index.html +31 -8
  188. nautobot/project-static/docs/development/apps/migration/ui-component-framework/migration-steps.html +31 -8
  189. nautobot/project-static/docs/development/apps/porting-from-netbox.html +31 -8
  190. nautobot/project-static/docs/development/core/application-registry.html +31 -8
  191. nautobot/project-static/docs/development/core/best-practices.html +31 -8
  192. nautobot/project-static/docs/development/core/bootstrap-ui.html +31 -8
  193. nautobot/project-static/docs/development/core/caching.html +31 -8
  194. nautobot/project-static/docs/development/core/controllers.html +31 -8
  195. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +31 -8
  196. nautobot/project-static/docs/development/core/generic-views.html +31 -8
  197. nautobot/project-static/docs/development/core/getting-started.html +31 -8
  198. nautobot/project-static/docs/development/core/homepage.html +31 -8
  199. nautobot/project-static/docs/development/core/index.html +31 -8
  200. nautobot/project-static/docs/development/core/minikube-dev-environment-for-k8s-jobs.html +31 -8
  201. nautobot/project-static/docs/development/core/model-checklist.html +31 -8
  202. nautobot/project-static/docs/development/core/model-features.html +31 -8
  203. nautobot/project-static/docs/development/core/natural-keys.html +31 -8
  204. nautobot/project-static/docs/development/core/navigation-menu.html +31 -8
  205. nautobot/project-static/docs/development/core/release-checklist.html +31 -8
  206. nautobot/project-static/docs/development/core/role-internals.html +31 -8
  207. nautobot/project-static/docs/development/core/settings.html +31 -8
  208. nautobot/project-static/docs/development/core/style-guide.html +31 -8
  209. nautobot/project-static/docs/development/core/templates.html +31 -8
  210. nautobot/project-static/docs/development/core/testing.html +31 -8
  211. nautobot/project-static/docs/development/core/ui-component-framework.html +31 -8
  212. nautobot/project-static/docs/development/core/user-preferences.html +31 -8
  213. nautobot/project-static/docs/development/index.html +31 -8
  214. nautobot/project-static/docs/development/jobs/getting-started.html +35 -8
  215. nautobot/project-static/docs/development/jobs/index.html +31 -8
  216. nautobot/project-static/docs/development/jobs/installation.html +31 -8
  217. nautobot/project-static/docs/development/jobs/job-extensions.html +31 -8
  218. nautobot/project-static/docs/development/jobs/job-logging.html +31 -8
  219. nautobot/project-static/docs/development/jobs/job-patterns.html +31 -8
  220. nautobot/project-static/docs/development/jobs/job-structure.html +31 -8
  221. nautobot/project-static/docs/development/jobs/migration/from-v1.html +31 -8
  222. nautobot/project-static/docs/development/jobs/testing.html +31 -8
  223. nautobot/project-static/docs/index.html +31 -8
  224. nautobot/project-static/docs/insert-analytics.sh +36 -0
  225. nautobot/project-static/docs/objects.inv +0 -0
  226. nautobot/project-static/docs/overview/application_stack.html +31 -8
  227. nautobot/project-static/docs/overview/design_philosophy.html +31 -8
  228. nautobot/project-static/docs/release-notes/index.html +31 -8
  229. nautobot/project-static/docs/release-notes/version-1.0.html +31 -8
  230. nautobot/project-static/docs/release-notes/version-1.1.html +31 -8
  231. nautobot/project-static/docs/release-notes/version-1.2.html +31 -8
  232. nautobot/project-static/docs/release-notes/version-1.3.html +31 -8
  233. nautobot/project-static/docs/release-notes/version-1.4.html +31 -8
  234. nautobot/project-static/docs/release-notes/version-1.5.html +31 -8
  235. nautobot/project-static/docs/release-notes/version-1.6.html +328 -8
  236. nautobot/project-static/docs/release-notes/version-2.0.html +31 -8
  237. nautobot/project-static/docs/release-notes/version-2.1.html +31 -8
  238. nautobot/project-static/docs/release-notes/version-2.2.html +31 -8
  239. nautobot/project-static/docs/release-notes/version-2.3.html +31 -8
  240. nautobot/project-static/docs/release-notes/version-2.4.html +353 -8
  241. nautobot/project-static/docs/search/search_index.json +1 -1
  242. nautobot/project-static/docs/sitemap.xml +302 -298
  243. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  244. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +31 -8
  245. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +31 -8
  246. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +31 -8
  247. nautobot/project-static/docs/user-guide/administration/configuration/index.html +31 -8
  248. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +31 -8
  249. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +31 -8
  250. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +31 -8
  251. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +31 -8
  252. nautobot/project-static/docs/user-guide/administration/guides/docker.html +31 -8
  253. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +31 -8
  254. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +31 -8
  255. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +31 -8
  256. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +31 -8
  257. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +31 -8
  258. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +31 -8
  259. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +31 -8
  260. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +31 -8
  261. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +31 -8
  262. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +31 -8
  263. nautobot/project-static/docs/user-guide/administration/installation/index.html +31 -8
  264. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +31 -8
  265. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +31 -8
  266. nautobot/project-static/docs/user-guide/administration/installation/services.html +31 -8
  267. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +31 -8
  268. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +31 -8
  269. nautobot/project-static/docs/user-guide/administration/security/index.html +31 -9
  270. nautobot/project-static/docs/user-guide/administration/security/notices.html +144 -9
  271. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +31 -8
  272. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +31 -8
  273. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +31 -8
  274. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +31 -8
  275. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +31 -8
  276. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +31 -8
  277. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +31 -8
  278. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +31 -8
  279. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +31 -8
  280. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +31 -8
  281. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +31 -8
  282. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +31 -8
  283. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +31 -8
  284. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +31 -8
  285. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +31 -8
  286. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +31 -8
  287. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +31 -8
  288. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +31 -8
  289. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +31 -8
  290. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +31 -8
  291. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +31 -8
  292. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +31 -8
  293. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +31 -8
  294. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +31 -8
  295. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +31 -8
  296. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +31 -8
  297. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +31 -8
  298. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +31 -8
  299. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +31 -8
  300. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +31 -8
  301. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +31 -8
  302. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +31 -8
  303. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +31 -8
  304. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +31 -8
  305. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +43 -20
  306. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +31 -8
  307. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +31 -8
  308. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +31 -8
  309. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +31 -8
  310. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +31 -8
  311. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +31 -8
  312. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +31 -8
  313. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +31 -8
  314. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +31 -8
  315. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +31 -8
  316. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +35 -8
  317. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +35 -8
  318. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +35 -8
  319. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulefamily.html +10261 -0
  320. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +34 -11
  321. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +31 -8
  322. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +31 -8
  323. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +31 -8
  324. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +31 -8
  325. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +31 -8
  326. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +31 -8
  327. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +31 -8
  328. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +31 -8
  329. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +31 -8
  330. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +31 -8
  331. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +31 -8
  332. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +31 -8
  333. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +31 -8
  334. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +31 -8
  335. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +31 -8
  336. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualdevicecontext.html +31 -8
  337. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +31 -8
  338. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +31 -8
  339. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +31 -8
  340. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +31 -8
  341. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +31 -8
  342. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +31 -8
  343. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +31 -8
  344. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +31 -8
  345. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +31 -8
  346. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +31 -8
  347. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +31 -8
  348. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +31 -8
  349. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +31 -8
  350. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +31 -8
  351. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +31 -8
  352. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +31 -8
  353. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +31 -8
  354. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +31 -8
  355. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +31 -8
  356. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +31 -8
  357. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +31 -8
  358. nautobot/project-static/docs/user-guide/core-data-model/wireless/index.html +31 -8
  359. nautobot/project-static/docs/user-guide/core-data-model/wireless/radioprofile.html +31 -8
  360. nautobot/project-static/docs/user-guide/core-data-model/wireless/supporteddatarate.html +31 -8
  361. nautobot/project-static/docs/user-guide/core-data-model/wireless/wirelessnetwork.html +31 -8
  362. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +31 -8
  363. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +31 -8
  364. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +31 -8
  365. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +31 -8
  366. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +31 -8
  367. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +31 -8
  368. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +31 -8
  369. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +31 -8
  370. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +31 -8
  371. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +31 -8
  372. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +31 -8
  373. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +41 -15
  374. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +31 -8
  375. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +31 -8
  376. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +31 -8
  377. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +31 -8
  378. nautobot/project-static/docs/user-guide/feature-guides/wireless-networks-and-controllers.html +31 -8
  379. nautobot/project-static/docs/user-guide/index.html +31 -8
  380. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +31 -8
  381. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +31 -8
  382. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +31 -8
  383. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +31 -8
  384. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +31 -8
  385. nautobot/project-static/docs/user-guide/platform-functionality/events.html +31 -8
  386. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +31 -8
  387. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +31 -8
  388. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +37 -9
  389. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +31 -8
  390. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +31 -8
  391. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +31 -8
  392. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +31 -8
  393. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +31 -8
  394. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +31 -8
  395. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +31 -8
  396. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +31 -8
  397. nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +31 -8
  398. nautobot/project-static/docs/user-guide/platform-functionality/jobs/managing-jobs.html +31 -8
  399. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +31 -8
  400. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +31 -8
  401. nautobot/project-static/docs/user-guide/platform-functionality/note.html +31 -8
  402. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +31 -8
  403. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +31 -8
  404. nautobot/project-static/docs/user-guide/platform-functionality/rendering-jinja-templates.html +31 -8
  405. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +31 -8
  406. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +31 -8
  407. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +31 -8
  408. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +31 -8
  409. nautobot/project-static/docs/user-guide/platform-functionality/role.html +31 -8
  410. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +31 -8
  411. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +31 -8
  412. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +31 -8
  413. nautobot/project-static/docs/user-guide/platform-functionality/status.html +31 -8
  414. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +31 -8
  415. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +31 -8
  416. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +31 -8
  417. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +31 -8
  418. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +31 -8
  419. nautobot/tenancy/tables.py +2 -0
  420. nautobot/users/models.py +4 -0
  421. nautobot/virtualization/models.py +4 -0
  422. nautobot/virtualization/tests/test_views.py +1 -1
  423. nautobot/wireless/forms.py +0 -1
  424. nautobot/wireless/models.py +1 -1
  425. nautobot/wireless/tables.py +7 -0
  426. {nautobot-2.4.9.dist-info → nautobot-2.4.11.dist-info}/METADATA +4 -4
  427. {nautobot-2.4.9.dist-info → nautobot-2.4.11.dist-info}/RECORD +433 -418
  428. /nautobot/dcim/templates/dcim/{platform_edit.html → platform_create.html} +0 -0
  429. /nautobot/extras/test_jobs/{pass.py → pass_job.py} +0 -0
  430. {nautobot-2.4.9.dist-info → nautobot-2.4.11.dist-info}/LICENSE.txt +0 -0
  431. {nautobot-2.4.9.dist-info → nautobot-2.4.11.dist-info}/NOTICE +0 -0
  432. {nautobot-2.4.9.dist-info → nautobot-2.4.11.dist-info}/WHEEL +0 -0
  433. {nautobot-2.4.9.dist-info → nautobot-2.4.11.dist-info}/entry_points.txt +0 -0
@@ -1,29 +1,13 @@
1
- from contextlib import contextmanager
2
- import importlib
3
- from importlib.util import find_spec, module_from_spec
1
+ from importlib.machinery import FileFinder, SOURCE_SUFFIXES, SourceFileLoader
2
+ from importlib.util import module_from_spec
3
+ from keyword import iskeyword
4
4
  import logging
5
- import os
6
5
  import pkgutil
7
6
  import sys
8
7
 
9
8
  logger = logging.getLogger(__name__)
10
9
 
11
10
 
12
- @contextmanager
13
- def _temporarily_add_to_sys_path(path):
14
- """
15
- Allow loading of modules and packages from within the provided directory by temporarily modifying `sys.path`.
16
-
17
- On exit, it restores the original `sys.path` value.
18
- """
19
- old_sys_path = sys.path.copy()
20
- sys.path.insert(0, path)
21
- try:
22
- yield
23
- finally:
24
- sys.path = old_sys_path
25
-
26
-
27
11
  def clear_module_from_sys_modules(module_name):
28
12
  """
29
13
  Remove the module and all its submodules from sys.modules.
@@ -33,7 +17,29 @@ def clear_module_from_sys_modules(module_name):
33
17
  del sys.modules[name]
34
18
 
35
19
 
36
- def import_modules_privately(path, module_path=None, ignore_import_errors=True):
20
+ def check_name_safe_to_import_privately(name: str) -> tuple[bool, str]:
21
+ """
22
+ Make sure the given package/module name is "safe" to import from the filesystem.
23
+
24
+ In other words, make sure it's:
25
+ - a valid Python identifier and not a reserved keyword
26
+ - not the name of an existing "real" Python package or builtin
27
+
28
+ Returns:
29
+ (bool, str): Whether safe to load, and an explanatory string fragment for logging/exception messages.
30
+ """
31
+ if not name.isidentifier():
32
+ return False, "not a valid identifier"
33
+ if iskeyword(name):
34
+ return False, "a reserved keyword"
35
+ if name in sys.builtin_module_names:
36
+ return False, "a Python builtin"
37
+ if any(module_info.name == name for module_info in pkgutil.iter_modules()):
38
+ return False, "the name of an installed Python package"
39
+ return True, "a valid and non-conflicting module name"
40
+
41
+
42
+ def import_modules_privately(path, module_path=None, module_prefix="", ignore_import_errors=True):
37
43
  """
38
44
  Import modules from the filesystem without adding the path permanently to `sys.path`.
39
45
 
@@ -49,49 +55,71 @@ def import_modules_privately(path, module_path=None, ignore_import_errors=True):
49
55
  ignore_import_errors (bool): Exceptions raised while importing modules will be caught and logged.
50
56
  If this is set as False, they will then be re-raised to be handled by the caller of this function.
51
57
  """
52
- if module_path is None:
53
- module_path = []
54
- module_prefix = None
58
+ loaded_modules = []
59
+ # We formerly used pkgutil.walk_packages() here to handle submodule loading with a multi-entry module_path,
60
+ # but that has the downside (and risk!) of automatically importing all packages that it finds in the given path,
61
+ # whether or not we actually want to do so. So instead, for the case where a module_path is provided, we need to
62
+ # iteratively import each submodule ourselves.
63
+ if module_path:
64
+ # Git repository case - e.g. import_modules_privately(settings.GIT_ROOT, module_path=[repository_slug, "jobs"])
65
+ # Here we want to ONLY auto-load the module sequence identified by module_path.
66
+ permitted, reason = check_name_safe_to_import_privately(module_path[0])
67
+ if not permitted:
68
+ logger.error("Unable to load module %r from %s as it is %s", module_path[0], path, reason)
69
+ else:
70
+ module = None
71
+ module_name = module_path.pop(0)
72
+ submodule_name = module_name
73
+ try:
74
+ while True:
75
+ finder = FileFinder(path, (SourceFileLoader, SOURCE_SUFFIXES))
76
+ finder.invalidate_caches()
77
+ spec = finder.find_spec(module_name)
78
+ if spec is None or spec.loader is None:
79
+ logger.error("Unable to find module spec and/or loader for %r", submodule_name)
80
+ break
81
+ spec.name = submodule_name
82
+ spec.loader.name = submodule_name
83
+ submodule = module_from_spec(spec)
84
+ sys.modules[submodule_name] = submodule
85
+ spec.loader.exec_module(submodule)
86
+ if module is not None:
87
+ setattr(module, module_name, submodule)
88
+ module = submodule
89
+ loaded_modules.append(module)
90
+ if module_path:
91
+ submodule_name = f"{module_name}.{module_path[0]}"
92
+ module_name = module_path.pop(0)
93
+ path = module.__path__[0]
94
+ else:
95
+ break
96
+ except Exception as exc:
97
+ logger.error("Unable to load module %s from %s: %s", module_name, path, exc)
98
+ if not ignore_import_errors:
99
+ raise
55
100
  else:
56
- module_prefix = ".".join(module_path)
57
- with _temporarily_add_to_sys_path(path):
58
- for finder, discovered_module_name, is_package in pkgutil.walk_packages([path], onerror=logger.error):
59
- if module_prefix and not (
60
- module_prefix.startswith(f"{discovered_module_name}.") # my_repo/__init__.py
61
- or discovered_module_name == module_prefix # my_repo/jobs.py
62
- or discovered_module_name.startswith(f"{module_prefix}.") # my_repo/jobs/foobar.py
63
- ):
101
+ # JOBS_ROOT case - import ALL top-level modules/packages that we can find in the given path;
102
+ # they can implement and import submodules as desired by themselves, but we only autoimport top-level ones.
103
+ for finder, discovered_module_name, _ in pkgutil.iter_modules([path]):
104
+ permitted, reason = check_name_safe_to_import_privately(discovered_module_name)
105
+ if not permitted:
106
+ logger.error("Unable to load module %r from %s as it is %s", discovered_module_name, path, reason)
64
107
  continue
65
- try:
66
- existing_module = find_spec(discovered_module_name)
67
- except (ModuleNotFoundError, ValueError):
68
- existing_module = None
69
- if existing_module is not None:
70
- existing_module_path = os.path.realpath(existing_module.origin)
71
- if not existing_module_path.startswith(path):
72
- logger.error(
73
- "Unable to load module %s from %s as it conflicts with existing module %s",
74
- discovered_module_name,
75
- path,
76
- existing_module_path,
77
- )
78
- continue
79
-
80
- if discovered_module_name in sys.modules:
81
- clear_module_from_sys_modules(discovered_module_name)
108
+ module_name = discovered_module_name
109
+ if module_name in sys.modules:
110
+ clear_module_from_sys_modules(module_name)
82
111
 
83
112
  try:
84
- if not is_package:
85
- spec = finder.find_spec(discovered_module_name)
86
- if spec is None:
87
- raise ValueError("Unable to find module spec")
88
- module = module_from_spec(spec)
89
- sys.modules[discovered_module_name] = module
90
- spec.loader.exec_module(module)
91
- else:
92
- module = importlib.import_module(discovered_module_name)
93
- importlib.reload(module)
113
+ spec = finder.find_spec(discovered_module_name)
114
+ if spec is None or spec.loader is None:
115
+ logger.error("Unable to find module spec and/or loader for %r", discovered_module_name)
116
+ continue
117
+ module = module_from_spec(spec)
118
+ sys.modules[module_name] = module
119
+ spec.loader.exec_module(module)
120
+ loaded_modules.append(module)
94
121
  except Exception as exc:
95
122
  logger.error("Unable to load module %s from %s: %s", discovered_module_name, path, exc)
96
123
  if not ignore_import_errors:
97
124
  raise
125
+ return loaded_modules
@@ -3,6 +3,7 @@ import datetime
3
3
  import logging
4
4
  import os
5
5
  import platform
6
+ import posixpath
6
7
  import re
7
8
  import sys
8
9
  import time
@@ -24,6 +25,7 @@ from django.views.csrf import csrf_failure as _csrf_failure
24
25
  from django.views.decorators.csrf import requires_csrf_token
25
26
  from django.views.defaults import ERROR_500_TEMPLATE_NAME, page_not_found
26
27
  from django.views.generic import TemplateView, View
28
+ from django.views.static import serve
27
29
  from graphene_django.views import GraphQLView
28
30
  from packaging import version
29
31
  from prometheus_client import (
@@ -133,6 +135,25 @@ class HomeView(AccessMixin, TemplateView):
133
135
  return self.render_to_response(context)
134
136
 
135
137
 
138
+ class MediaView(AccessMixin, View):
139
+ """
140
+ Serves media files while enforcing login restrictions.
141
+
142
+ This view wraps Django's `serve()` function to ensure that access to media files (with the exception of
143
+ branding files defined in `settings.BRANDING_FILEPATHS`) is restricted to authenticated users.
144
+ """
145
+
146
+ def get(self, request, path):
147
+ if request.user.is_authenticated:
148
+ return serve(request, path, document_root=settings.MEDIA_ROOT)
149
+
150
+ # Unauthenticated users can access BRANDING_FILEPATHS only
151
+ if posixpath.normpath(path).lstrip("/") in settings.BRANDING_FILEPATHS.values():
152
+ return serve(request, path, document_root=settings.MEDIA_ROOT)
153
+
154
+ return self.handle_no_permission()
155
+
156
+
136
157
  class WorkerStatusView(UserPassesTestMixin, TemplateView):
137
158
  template_name = "utilities/worker_status.html"
138
159
 
@@ -54,6 +54,7 @@ from nautobot.core.views.utils import (
54
54
  check_filter_for_display,
55
55
  common_detail_view_context,
56
56
  get_csv_form_fields_from_serializer_class,
57
+ get_saved_views_for_user,
57
58
  handle_protectederror,
58
59
  import_csv_helper,
59
60
  prepare_cloned_fields,
@@ -315,18 +316,7 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
315
316
  table_config_form = None
316
317
  current_saved_view = None
317
318
  current_saved_view_pk = self.request.GET.get("saved_view", None)
318
- # We are not using .restrict(request.user, "view") here
319
- # User should be able to see any saved view that he has the list view access to.
320
- if user.has_perms(["extras.view_savedview"]):
321
- saved_views = SavedView.objects.filter(view=list_url).order_by("name").only("pk", "name")
322
- else:
323
- shared_saved_views = (
324
- SavedView.objects.filter(view=list_url, is_shared=True).order_by("name").only("pk", "name")
325
- )
326
- user_owned_saved_views = (
327
- SavedView.objects.filter(view=list_url, owner=user).order_by("name").only("pk", "name")
328
- )
329
- saved_views = shared_saved_views | user_owned_saved_views
319
+ saved_views = get_saved_views_for_user(user, list_url)
330
320
 
331
321
  if current_saved_view_pk:
332
322
  try:
@@ -239,6 +239,9 @@ class NautobotViewSetMixin(GenericViewSet, AccessMixin, GetReturnURLMixin, FormV
239
239
  table_class = None
240
240
  notes_form_class = NoteForm
241
241
  permission_classes = []
242
+ # custom view attributes used for permission checks and handling
243
+ custom_view_base_action = None
244
+ custom_view_additional_permissions = None
242
245
 
243
246
  def get_permissions_for_model(self, model, actions):
244
247
  """
@@ -249,6 +252,10 @@ class NautobotViewSetMixin(GenericViewSet, AccessMixin, GetReturnURLMixin, FormV
249
252
  """
250
253
  model_permissions = []
251
254
  for action in actions:
255
+ # Append additional object permissions if specified.
256
+ if self.custom_view_additional_permissions:
257
+ model_permissions.append(*self.custom_view_additional_permissions)
258
+ # Append the model-level permissions for the action.
252
259
  model_permissions.append(f"{model._meta.app_label}.{action}_{model._meta.model_name}")
253
260
  return model_permissions
254
261
 
@@ -501,7 +508,13 @@ class NautobotViewSetMixin(GenericViewSet, AccessMixin, GetReturnURLMixin, FormV
501
508
 
502
509
  def get_action(self):
503
510
  """Helper method for retrieving action and if action not set defaulting to action name."""
504
- return PERMISSIONS_ACTION_MAP.get(self.action, self.action)
511
+ if self.custom_view_base_action:
512
+ return self.custom_view_base_action
513
+ if self.action in PERMISSIONS_ACTION_MAP:
514
+ # If the action is in the action_map, return the mapped permission
515
+ return PERMISSIONS_ACTION_MAP[self.action]
516
+
517
+ return self.action
505
518
 
506
519
  def get_extra_context(self, request, instance=None):
507
520
  """
@@ -1032,6 +1045,11 @@ class BulkEditAndBulkDeleteModelMixin:
1032
1045
 
1033
1046
  filter_query_params = new_filter_query_params
1034
1047
 
1048
+ if nullified_fields := request.POST.getlist("_nullify"):
1049
+ form_data["_nullify"] = nullified_fields
1050
+ else:
1051
+ form_data["_nullify"] = []
1052
+
1035
1053
  job_form = BulkEditObjects.as_form(
1036
1054
  data={
1037
1055
  "form_data": form_data,
@@ -28,6 +28,7 @@ from nautobot.core.views.utils import (
28
28
  check_filter_for_display,
29
29
  common_detail_view_context,
30
30
  get_csv_form_fields_from_serializer_class,
31
+ get_saved_views_for_user,
31
32
  view_changes_not_saved,
32
33
  )
33
34
  from nautobot.extras.models import SavedView
@@ -235,7 +236,8 @@ class NautobotHTMLRenderer(renderers.BrowsableAPIRenderer):
235
236
  if view.filterset_form_class is not None:
236
237
  filter_form = view.filterset_form_class(view.filter_params, label_suffix="")
237
238
  table = self.construct_table(view, request=request, permissions=permissions)
238
- search_form = SearchForm(data=view.filter_params)
239
+ q_placeholder = "Search " + bettertitle(model._meta.verbose_name_plural)
240
+ search_form = SearchForm(data=view.filter_params, q_placeholder=q_placeholder)
239
241
  elif view.action == "destroy":
240
242
  form = form_class(initial=request.GET)
241
243
  elif view.action in ["create", "update"]:
@@ -309,18 +311,7 @@ class NautobotHTMLRenderer(renderers.BrowsableAPIRenderer):
309
311
  list_url = f"{resolved_path.app_name}:{resolved_path.url_name}"
310
312
  saved_views = None
311
313
  if model.is_saved_view_model:
312
- # We are not using .restrict(request.user, "view") here
313
- # User should be able to see any saved view that he has the list view access to.
314
- if request.user.has_perms(["extras.view_savedview"]):
315
- saved_views = SavedView.objects.filter(view=list_url).order_by("name").only("pk", "name")
316
- else:
317
- shared_saved_views = (
318
- SavedView.objects.filter(view=list_url, is_shared=True).order_by("name").only("pk", "name")
319
- )
320
- user_owned_saved_views = (
321
- SavedView.objects.filter(view=list_url, owner=request.user).order_by("name").only("pk", "name")
322
- )
323
- saved_views = shared_saved_views | user_owned_saved_views
314
+ saved_views = get_saved_views_for_user(request.user, list_url)
324
315
 
325
316
  new_changes_not_applied = view_changes_not_saved(request, view, self.saved_view)
326
317
  context.update(
@@ -17,6 +17,7 @@ from nautobot.core.utils.data import is_uuid
17
17
  from nautobot.core.utils.filtering import get_filter_field_label
18
18
  from nautobot.core.utils.lookup import get_created_and_last_updated_usernames_for_model, get_form_for_model
19
19
  from nautobot.core.views.paginator import EnhancedPaginator, get_paginate_count
20
+ from nautobot.extras.models import SavedView
20
21
  from nautobot.extras.tables import AssociatedContactsTable, DynamicGroupTable, ObjectMetadataTable
21
22
 
22
23
 
@@ -382,3 +383,18 @@ def common_detail_view_context(request, instance):
382
383
  context["associated_object_metadata_table"] = None
383
384
 
384
385
  return context
386
+
387
+
388
+ def get_saved_views_for_user(user, list_url):
389
+ # We are not using .restrict(request.user, "view") here
390
+ # User should be able to see any saved view that he has the list view access to.
391
+ saved_views = SavedView.objects.filter(view=list_url).order_by("name").only("pk", "name")
392
+ if user.has_perms(["extras.view_savedview"]):
393
+ return saved_views
394
+
395
+ shared_saved_views = saved_views.filter(is_shared=True)
396
+ if user.is_authenticated:
397
+ user_owned_saved_views = SavedView.objects.filter(view=list_url, owner=user).order_by("name").only("pk", "name")
398
+ return shared_saved_views | user_owned_saved_views
399
+
400
+ return shared_saved_views
@@ -79,6 +79,7 @@ from nautobot.dcim.models import (
79
79
  Module,
80
80
  ModuleBay,
81
81
  ModuleBayTemplate,
82
+ ModuleFamily,
82
83
  ModuleType,
83
84
  PathEndpoint,
84
85
  Platform,
@@ -1124,3 +1125,15 @@ class InterfaceVDCAssignmentSerializer(ValidatedModelSerializer):
1124
1125
  class Meta:
1125
1126
  model = InterfaceVDCAssignment
1126
1127
  fields = "__all__"
1128
+
1129
+
1130
+ class ModuleFamilySerializer(NautobotModelSerializer):
1131
+ """API serializer for ModuleFamily objects."""
1132
+
1133
+ url = serializers.HyperlinkedIdentityField(view_name="dcim-api:modulefamily-detail")
1134
+ module_type_count = serializers.IntegerField(read_only=True)
1135
+ module_bay_count = serializers.IntegerField(read_only=True)
1136
+
1137
+ class Meta:
1138
+ model = ModuleFamily
1139
+ fields = "__all__"
nautobot/dcim/api/urls.py CHANGED
@@ -17,6 +17,7 @@ router.register("rack-reservations", views.RackReservationViewSet)
17
17
  router.register("manufacturers", views.ManufacturerViewSet)
18
18
  router.register("device-families", views.DeviceFamilyViewSet)
19
19
  router.register("device-types", views.DeviceTypeViewSet)
20
+ router.register("module-families", views.ModuleFamilyViewSet)
20
21
  router.register("module-types", views.ModuleTypeViewSet)
21
22
 
22
23
  # Device type and Module type components
@@ -54,6 +54,7 @@ from nautobot.dcim.models import (
54
54
  Module,
55
55
  ModuleBay,
56
56
  ModuleBayTemplate,
57
+ ModuleFamily,
57
58
  ModuleType,
58
59
  Platform,
59
60
  PowerFeed,
@@ -644,6 +645,14 @@ class CableViewSet(NautobotModelViewSet):
644
645
  serializer_class = serializers.CableSerializer
645
646
  filterset_class = filters.CableFilterSet
646
647
 
648
+ def get_queryset(self):
649
+ # 6933 fix: with prefetch related in queryset
650
+ # DeviceInterface is not properly cleared of _path_id
651
+ queryset = super().get_queryset()
652
+ if self.action == "destroy":
653
+ queryset = queryset.prefetch_related(None)
654
+ return queryset
655
+
647
656
 
648
657
  #
649
658
  # Virtual chassis
@@ -836,3 +845,14 @@ class InterfaceVDCAssignmentViewSet(ModelViewSet):
836
845
  queryset = InterfaceVDCAssignment.objects.all()
837
846
  serializer_class = serializers.InterfaceVDCAssignmentSerializer
838
847
  filterset_class = filters.InterfaceVDCAssignmentFilterSet
848
+
849
+
850
+ class ModuleFamilyViewSet(NautobotModelViewSet):
851
+ """API viewset for interacting with ModuleFamily objects."""
852
+
853
+ queryset = ModuleFamily.objects.annotate(
854
+ module_type_count=count_related(ModuleType, "module_family"),
855
+ module_bay_count=count_related(ModuleBay, "module_family"),
856
+ )
857
+ serializer_class = serializers.ModuleFamilySerializer
858
+ filterset_class = filters.ModuleFamilyFilterSet
nautobot/dcim/apps.py CHANGED
@@ -14,6 +14,7 @@ class DCIMConfig(NautobotConfig):
14
14
  "devicetype",
15
15
  "location",
16
16
  "module",
17
+ "modulefamily",
17
18
  "moduletype",
18
19
  "powerfeed",
19
20
  "rack",
nautobot/dcim/factory.py CHANGED
@@ -49,6 +49,7 @@ from nautobot.dcim.models import (
49
49
  Module,
50
50
  ModuleBay,
51
51
  ModuleBayTemplate,
52
+ ModuleFamily,
52
53
  ModuleType,
53
54
  Platform,
54
55
  PowerOutletTemplate,
@@ -787,6 +788,7 @@ class ModuleTypeFactory(PrimaryModelFactory):
787
788
  exclude = ("has_part_number", "has_comments")
788
789
 
789
790
  manufacturer = random_instance(Manufacturer, allow_null=False)
791
+ module_family = random_instance(ModuleFamily, allow_null=True)
790
792
 
791
793
  has_part_number = NautobotBoolIterator()
792
794
  part_number = factory.Maybe("has_part_number", factory.Faker("ean", length=8), "")
@@ -970,6 +972,15 @@ class ModuleBayTemplateFactory(ModularDeviceComponentTemplateFactory):
970
972
  factory.LazyAttribute(lambda o: o.module_type.module_bay_templates.count() + 1),
971
973
  )
972
974
 
975
+ class Params:
976
+ has_module_family = NautobotBoolIterator()
977
+
978
+ module_family = factory.Maybe(
979
+ "has_module_family",
980
+ random_instance(ModuleFamily),
981
+ None,
982
+ )
983
+
973
984
 
974
985
  class VirtualDeviceContextFactory(PrimaryModelFactory):
975
986
  class Meta:
@@ -74,6 +74,7 @@ from nautobot.dcim.models import (
74
74
  Module,
75
75
  ModuleBay,
76
76
  ModuleBayTemplate,
77
+ ModuleFamily,
77
78
  ModuleType,
78
79
  Platform,
79
80
  PowerFeed,
@@ -137,6 +138,7 @@ __all__ = (
137
138
  "ManufacturerFilterSet",
138
139
  "ModuleBayFilterSet",
139
140
  "ModuleBayTemplateFilterSet",
141
+ "ModuleFamilyFilterSet",
140
142
  "ModuleFilterSet",
141
143
  "ModuleTypeFilterSet",
142
144
  "PathEndpointFilterSet",
@@ -2072,6 +2074,12 @@ class ModuleFilterSet(
2072
2074
  to_field_name="model",
2073
2075
  label="Module type (model or ID)",
2074
2076
  )
2077
+ module_family = NaturalKeyOrPKMultipleChoiceFilter(
2078
+ field_name="module_type__module_family",
2079
+ queryset=ModuleFamily.objects.all(),
2080
+ to_field_name="name",
2081
+ label="Module family (name or ID)",
2082
+ )
2075
2083
  parent_module_bay = django_filters.ModelMultipleChoiceFilter(
2076
2084
  queryset=ModuleBay.objects.all(),
2077
2085
  label="Parent Module Bay",
@@ -2082,6 +2090,13 @@ class ModuleFilterSet(
2082
2090
  label="Device (name or ID)",
2083
2091
  method="filter_device",
2084
2092
  )
2093
+ compatible_with_module_bay = extend_schema_field({"type": "string", "format": "uuid"})(
2094
+ django_filters.ModelChoiceFilter(
2095
+ queryset=ModuleBay.objects.all(),
2096
+ method="filter_module_bay",
2097
+ label="Compatible with module bay (ID)",
2098
+ )
2099
+ )
2085
2100
 
2086
2101
  def _construct_device_filter_recursively(self, field_name, value):
2087
2102
  recursion_depth = MODULE_RECURSION_DEPTH_LIMIT
@@ -2107,6 +2122,12 @@ class ModuleFilterSet(
2107
2122
  params = self.generate_query_filter_device(value)
2108
2123
  return queryset.filter(params)
2109
2124
 
2125
+ def filter_module_bay(self, queryset, name, value):
2126
+ """Filter modules based on a module bay's module family."""
2127
+ if value and value.module_family:
2128
+ return queryset.filter(module_type__module_family=value.module_family)
2129
+ return queryset
2130
+
2110
2131
  class Meta:
2111
2132
  model = Module
2112
2133
  fields = "__all__"
@@ -2133,6 +2154,23 @@ class ModuleTypeFilterSet(DeviceTypeModuleTypeCommonFiltersMixin, NautobotFilter
2133
2154
  },
2134
2155
  },
2135
2156
  )
2157
+ manufacturer = NaturalKeyOrPKMultipleChoiceFilter(
2158
+ queryset=Manufacturer.objects.all(),
2159
+ to_field_name="name",
2160
+ label="Manufacturer (name or ID)",
2161
+ )
2162
+ module_family = NaturalKeyOrPKMultipleChoiceFilter(
2163
+ queryset=ModuleFamily.objects.all(),
2164
+ to_field_name="name",
2165
+ label="Module family (name or ID)",
2166
+ )
2167
+ compatible_with_module_bay = extend_schema_field({"type": "string", "format": "uuid"})(
2168
+ django_filters.ModelChoiceFilter(
2169
+ queryset=ModuleBay.objects.all(),
2170
+ method="filter_module_bay",
2171
+ label="Installable in module bay (ID)",
2172
+ )
2173
+ )
2136
2174
  has_modules = RelatedMembershipBooleanFilter(
2137
2175
  field_name="modules",
2138
2176
  label="Has module instances",
@@ -2142,6 +2180,12 @@ class ModuleTypeFilterSet(DeviceTypeModuleTypeCommonFiltersMixin, NautobotFilter
2142
2180
  model = ModuleType
2143
2181
  fields = "__all__"
2144
2182
 
2183
+ def filter_module_bay(self, queryset, name, value):
2184
+ """Filter module types based on a module bay's module family."""
2185
+ if value and value.module_family:
2186
+ return queryset.filter(module_family=value.module_family)
2187
+ return queryset
2188
+
2145
2189
 
2146
2190
  class ModuleBayTemplateFilterSet(ModularDeviceComponentTemplateModelFilterSetMixin, NautobotFilterSet):
2147
2191
  q = SearchFilter(
@@ -2172,6 +2216,14 @@ class ModuleBayTemplateFilterSet(ModularDeviceComponentTemplateModelFilterSetMix
2172
2216
  },
2173
2217
  }
2174
2218
  )
2219
+ module_family = NaturalKeyOrPKMultipleChoiceFilter(
2220
+ queryset=ModuleFamily.objects.all(),
2221
+ to_field_name="name",
2222
+ label="Module family (name or ID)",
2223
+ )
2224
+ requires_first_party_modules = django_filters.BooleanFilter(
2225
+ label="Requires first-party modules",
2226
+ )
2175
2227
 
2176
2228
  class Meta:
2177
2229
  model = ModuleBayTemplate
@@ -2228,6 +2280,15 @@ class ModuleBayFilterSet(NautobotFilterSet):
2228
2280
  field_name="installed_module",
2229
2281
  label="Has installed module",
2230
2282
  )
2283
+ module_family = NaturalKeyOrPKMultipleChoiceFilter(
2284
+ queryset=ModuleFamily.objects.all(),
2285
+ to_field_name="name",
2286
+ label="Module family (name or ID)",
2287
+ )
2288
+ requires_first_party_modules = django_filters.BooleanFilter(
2289
+ field_name="requires_first_party_modules",
2290
+ label="Requires first-party modules",
2291
+ )
2231
2292
 
2232
2293
  class Meta:
2233
2294
  model = ModuleBay
@@ -2362,3 +2423,52 @@ class InterfaceVDCAssignmentFilterSet(NautobotFilterSet):
2362
2423
  "interface",
2363
2424
  "virtual_device_context",
2364
2425
  ]
2426
+
2427
+
2428
+ class ModuleFamilyFilterSet(NautobotFilterSet):
2429
+ """FilterSet for ModuleFamily objects."""
2430
+
2431
+ q = SearchFilter(
2432
+ filter_predicates={
2433
+ "name": {
2434
+ "lookup_expr": "icontains",
2435
+ "preprocessor": str.strip,
2436
+ },
2437
+ "description": {
2438
+ "lookup_expr": "icontains",
2439
+ "preprocessor": str.strip,
2440
+ },
2441
+ "module_types__model": {
2442
+ "lookup_expr": "icontains",
2443
+ "preprocessor": str.strip,
2444
+ },
2445
+ "module_types__manufacturer__name": {
2446
+ "lookup_expr": "icontains",
2447
+ "preprocessor": str.strip,
2448
+ },
2449
+ }
2450
+ )
2451
+
2452
+ module_types = NaturalKeyOrPKMultipleChoiceFilter(
2453
+ queryset=ModuleType.objects.all(),
2454
+ to_field_name="model",
2455
+ label="Module types (model or ID)",
2456
+ )
2457
+
2458
+ module_bay_id = extend_schema_field({"type": "array", "items": {"type": "string", "format": "uuid"}})(
2459
+ django_filters.ModelMultipleChoiceFilter(
2460
+ queryset=ModuleBay.objects.all(),
2461
+ label="Module bay (ID)",
2462
+ )
2463
+ )
2464
+
2465
+ class Meta:
2466
+ model = ModuleFamily
2467
+ fields = [
2468
+ "id",
2469
+ "name",
2470
+ "description",
2471
+ "module_types",
2472
+ "module_bay_id",
2473
+ "tags",
2474
+ ]