nautobot 2.2.7__py3-none-any.whl → 2.2.9__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 (306) hide show
  1. nautobot/core/filters.py +15 -1
  2. nautobot/core/graphql/generators.py +4 -4
  3. nautobot/core/graphql/schema.py +9 -7
  4. nautobot/core/jobs/__init__.py +4 -1
  5. nautobot/core/settings.py +13 -2
  6. nautobot/core/settings.yaml +14 -0
  7. nautobot/core/templates/nautobot_config.py.j2 +15 -0
  8. nautobot/core/tests/integration/test_general_functionality.py +36 -0
  9. nautobot/core/tests/test_graphql.py +51 -5
  10. nautobot/core/tests/test_jobs.py +82 -2
  11. nautobot/core/tests/test_templatetags_netutils.py +3 -3
  12. nautobot/dcim/models/device_components.py +7 -0
  13. nautobot/dcim/models/devices.py +14 -3
  14. nautobot/dcim/tables/devices.py +19 -4
  15. nautobot/dcim/templates/dcim/deviceredundancygroup_retrieve.html +6 -0
  16. nautobot/dcim/tests/test_models.py +106 -0
  17. nautobot/dcim/tests/test_views.py +13 -0
  18. nautobot/dcim/views.py +8 -2
  19. nautobot/extras/api/views.py +7 -59
  20. nautobot/extras/homepage.py +12 -2
  21. nautobot/extras/jobs.py +2 -2
  22. nautobot/extras/models/jobs.py +81 -0
  23. nautobot/extras/models/relationships.py +12 -0
  24. nautobot/extras/signals.py +14 -1
  25. nautobot/extras/tables.py +36 -5
  26. nautobot/extras/templates/extras/job_detail.html +11 -0
  27. nautobot/extras/tests/integration/test_dynamicgroups.py +1 -1
  28. nautobot/extras/tests/test_relationships.py +221 -1
  29. nautobot/extras/tests/test_views.py +21 -0
  30. nautobot/extras/utils.py +34 -5
  31. nautobot/extras/views.py +20 -46
  32. nautobot/ipam/models.py +9 -12
  33. nautobot/ipam/tests/test_models.py +3 -2
  34. nautobot/ipam/views.py +2 -8
  35. nautobot/project-static/css/base.css +1 -0
  36. nautobot/project-static/docs/404.html +4 -4
  37. nautobot/project-static/docs/apps/index.html +4 -4
  38. nautobot/project-static/docs/apps/nautobot-apps.html +4 -4
  39. nautobot/project-static/docs/assets/stylesheets/{main.6543a935.min.css → main.76a95c52.min.css} +1 -1
  40. nautobot/project-static/docs/assets/stylesheets/{main.6543a935.min.css.map → main.76a95c52.min.css.map} +1 -1
  41. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +4 -4
  42. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +4 -4
  43. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +4 -4
  44. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +4 -4
  45. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +4 -4
  46. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +4 -4
  47. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +4 -4
  48. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +4 -4
  49. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +4 -4
  50. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +4 -4
  51. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +4 -4
  52. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +4 -4
  53. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +4 -4
  54. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +4 -4
  55. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +4 -4
  56. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +4 -4
  57. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +4 -4
  58. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +4 -4
  59. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +4 -4
  60. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +4 -4
  61. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +4 -4
  62. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +6 -4
  63. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +4 -4
  64. nautobot/project-static/docs/development/apps/api/configuration-view.html +4 -4
  65. nautobot/project-static/docs/development/apps/api/database-backend-config.html +4 -4
  66. nautobot/project-static/docs/development/apps/api/models/django-admin.html +4 -4
  67. nautobot/project-static/docs/development/apps/api/models/global-search.html +4 -4
  68. nautobot/project-static/docs/development/apps/api/models/graphql.html +4 -4
  69. nautobot/project-static/docs/development/apps/api/models/index.html +4 -4
  70. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +4 -4
  71. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +4 -4
  72. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +4 -4
  73. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +4 -4
  74. nautobot/project-static/docs/development/apps/api/platform-features/index.html +4 -4
  75. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +4 -4
  76. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +4 -4
  77. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +4 -4
  78. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +4 -4
  79. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +4 -4
  80. nautobot/project-static/docs/development/apps/api/prometheus.html +4 -4
  81. nautobot/project-static/docs/development/apps/api/setup.html +4 -4
  82. nautobot/project-static/docs/development/apps/api/testing.html +4 -4
  83. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +4 -4
  84. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +4 -4
  85. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +4 -4
  86. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +4 -4
  87. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +4 -4
  88. nautobot/project-static/docs/development/apps/api/views/base-template.html +4 -4
  89. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +4 -4
  90. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +4 -4
  91. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +4 -4
  92. nautobot/project-static/docs/development/apps/api/views/index.html +4 -4
  93. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +4 -4
  94. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +4 -4
  95. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +4 -4
  96. nautobot/project-static/docs/development/apps/api/views/notes.html +4 -4
  97. nautobot/project-static/docs/development/apps/api/views/rest-api.html +4 -4
  98. nautobot/project-static/docs/development/apps/api/views/urls.html +4 -4
  99. nautobot/project-static/docs/development/apps/index.html +4 -4
  100. nautobot/project-static/docs/development/apps/migration/code-updates.html +4 -4
  101. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +4 -4
  102. nautobot/project-static/docs/development/apps/migration/from-v1.html +4 -4
  103. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +4 -4
  104. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +4 -4
  105. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +4 -4
  106. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +4 -4
  107. nautobot/project-static/docs/development/apps/porting-from-netbox.html +4 -4
  108. nautobot/project-static/docs/development/core/application-registry.html +4 -4
  109. nautobot/project-static/docs/development/core/best-practices.html +4 -4
  110. nautobot/project-static/docs/development/core/bootstrap-ui.html +4 -4
  111. nautobot/project-static/docs/development/core/caching.html +4 -4
  112. nautobot/project-static/docs/development/core/controllers.html +4 -4
  113. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +4 -4
  114. nautobot/project-static/docs/development/core/generic-views.html +4 -4
  115. nautobot/project-static/docs/development/core/getting-started.html +4 -4
  116. nautobot/project-static/docs/development/core/homepage.html +4 -4
  117. nautobot/project-static/docs/development/core/index.html +15 -4
  118. nautobot/project-static/docs/development/core/model-checklist.html +4 -4
  119. nautobot/project-static/docs/development/core/model-features.html +4 -4
  120. nautobot/project-static/docs/development/core/natural-keys.html +4 -4
  121. nautobot/project-static/docs/development/core/navigation-menu.html +4 -4
  122. nautobot/project-static/docs/development/core/release-checklist.html +4 -4
  123. nautobot/project-static/docs/development/core/role-internals.html +4 -4
  124. nautobot/project-static/docs/development/core/settings.html +4 -4
  125. nautobot/project-static/docs/development/core/style-guide.html +4 -4
  126. nautobot/project-static/docs/development/core/templates.html +4 -4
  127. nautobot/project-static/docs/development/core/testing.html +4 -4
  128. nautobot/project-static/docs/development/core/user-preferences.html +4 -4
  129. nautobot/project-static/docs/development/index.html +4 -4
  130. nautobot/project-static/docs/development/jobs/index.html +379 -365
  131. nautobot/project-static/docs/development/jobs/migration/from-v1.html +4 -4
  132. nautobot/project-static/docs/index.html +8228 -13
  133. nautobot/project-static/docs/overview/application_stack.html +4 -4
  134. nautobot/project-static/docs/overview/design_philosophy.html +6 -6
  135. nautobot/project-static/docs/overview/index.html +13 -8228
  136. nautobot/project-static/docs/release-notes/index.html +4 -4
  137. nautobot/project-static/docs/release-notes/version-1.0.html +4 -4
  138. nautobot/project-static/docs/release-notes/version-1.1.html +4 -4
  139. nautobot/project-static/docs/release-notes/version-1.2.html +4 -4
  140. nautobot/project-static/docs/release-notes/version-1.3.html +4 -4
  141. nautobot/project-static/docs/release-notes/version-1.4.html +4 -4
  142. nautobot/project-static/docs/release-notes/version-1.5.html +4 -4
  143. nautobot/project-static/docs/release-notes/version-1.6.html +4 -4
  144. nautobot/project-static/docs/release-notes/version-2.0.html +4 -4
  145. nautobot/project-static/docs/release-notes/version-2.1.html +4 -4
  146. nautobot/project-static/docs/release-notes/version-2.2.html +419 -136
  147. nautobot/project-static/docs/requirements.txt +2 -1
  148. nautobot/project-static/docs/search/search_index.json +1 -1
  149. nautobot/project-static/docs/sitemap.xml +260 -260
  150. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  151. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +4 -4
  152. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +4 -4
  153. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +4 -4
  154. nautobot/project-static/docs/user-guide/administration/configuration/index.html +4 -4
  155. nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +36 -4
  156. nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +4 -4
  157. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +4 -4
  158. nautobot/project-static/docs/user-guide/administration/guides/caching.html +4 -4
  159. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +8 -4
  160. nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +4 -4
  161. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +4 -4
  162. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +4 -4
  163. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +4 -4
  164. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +4 -4
  165. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +4 -4
  166. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +4 -4
  167. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +4 -4
  168. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +8 -8
  169. nautobot/project-static/docs/user-guide/administration/installation/index.html +4 -4
  170. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +9 -5
  171. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +4 -4
  172. nautobot/project-static/docs/user-guide/administration/installation/services.html +4 -4
  173. nautobot/project-static/docs/user-guide/administration/installation-extras/docker.html +4 -4
  174. nautobot/project-static/docs/user-guide/administration/installation-extras/health-checks.html +4 -4
  175. nautobot/project-static/docs/user-guide/administration/installation-extras/selinux-troubleshooting.html +4 -4
  176. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +4 -4
  177. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +4 -4
  178. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +62 -10
  179. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +4 -4
  180. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +4 -4
  181. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +4 -4
  182. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +4 -4
  183. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +4 -4
  184. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +4 -4
  185. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +4 -4
  186. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +4 -4
  187. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +4 -4
  188. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +4 -4
  189. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +4 -4
  190. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +4 -4
  191. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +4 -4
  192. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +4 -4
  193. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +4 -4
  194. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +4 -4
  195. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +4 -4
  196. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +4 -4
  197. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +4 -4
  198. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +4 -4
  199. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +4 -4
  200. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +4 -4
  201. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +4 -4
  202. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +4 -4
  203. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +4 -4
  204. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +4 -4
  205. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +4 -4
  206. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +4 -4
  207. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +4 -4
  208. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +4 -4
  209. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +4 -4
  210. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +4 -4
  211. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +4 -4
  212. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +4 -4
  213. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +4 -4
  214. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +4 -4
  215. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +4 -4
  216. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +4 -4
  217. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +4 -4
  218. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +4 -4
  219. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +4 -4
  220. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +4 -4
  221. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +4 -4
  222. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +4 -4
  223. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +4 -4
  224. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +4 -4
  225. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +4 -4
  226. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +4 -4
  227. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +4 -4
  228. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +4 -4
  229. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +4 -4
  230. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +4 -4
  231. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +4 -4
  232. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +4 -4
  233. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +4 -4
  234. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +4 -4
  235. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +4 -4
  236. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +4 -4
  237. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +4 -4
  238. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +4 -4
  239. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +4 -4
  240. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +4 -4
  241. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +4 -4
  242. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +4 -4
  243. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +4 -4
  244. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +4 -4
  245. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +4 -4
  246. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +4 -4
  247. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +4 -4
  248. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +4 -4
  249. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +4 -4
  250. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +4 -4
  251. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +4 -4
  252. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +4 -4
  253. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +4 -4
  254. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +4 -4
  255. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +4 -4
  256. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +4 -4
  257. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +4 -4
  258. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +4 -4
  259. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +4 -4
  260. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +4 -4
  261. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +4 -4
  262. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +4 -4
  263. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +4 -4
  264. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +4 -4
  265. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +4 -4
  266. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +4 -4
  267. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +4 -4
  268. nautobot/project-static/docs/user-guide/index.html +4 -4
  269. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +4 -4
  270. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +4 -4
  271. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +4 -4
  272. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +4 -4
  273. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +4 -4
  274. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +4 -4
  275. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +4 -4
  276. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +4 -4
  277. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +4 -4
  278. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +4 -4
  279. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +4 -4
  280. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +4 -4
  281. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +4 -4
  282. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +4 -4
  283. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +4 -4
  284. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +4 -4
  285. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +4 -4
  286. nautobot/project-static/docs/user-guide/platform-functionality/note.html +4 -4
  287. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +4 -4
  288. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +4 -4
  289. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +4 -4
  290. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +4 -4
  291. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +4 -4
  292. nautobot/project-static/docs/user-guide/platform-functionality/role.html +4 -4
  293. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +4 -4
  294. nautobot/project-static/docs/user-guide/platform-functionality/status.html +4 -4
  295. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +4 -4
  296. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +4 -4
  297. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +4 -4
  298. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +4 -4
  299. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +4 -4
  300. nautobot/virtualization/tables.py +2 -5
  301. {nautobot-2.2.7.dist-info → nautobot-2.2.9.dist-info}/METADATA +3 -3
  302. {nautobot-2.2.7.dist-info → nautobot-2.2.9.dist-info}/RECORD +306 -305
  303. {nautobot-2.2.7.dist-info → nautobot-2.2.9.dist-info}/LICENSE.txt +0 -0
  304. {nautobot-2.2.7.dist-info → nautobot-2.2.9.dist-info}/NOTICE +0 -0
  305. {nautobot-2.2.7.dist-info → nautobot-2.2.9.dist-info}/WHEEL +0 -0
  306. {nautobot-2.2.7.dist-info → nautobot-2.2.9.dist-info}/entry_points.txt +0 -0
@@ -15,9 +15,11 @@ from nautobot.core.tables import RelationshipColumn
15
15
  from nautobot.core.testing import TestCase
16
16
  from nautobot.core.testing.models import ModelTestCases
17
17
  from nautobot.core.utils.lookup import get_route_for_model
18
+ from nautobot.dcim.forms import DeviceForm
18
19
  from nautobot.dcim.models import (
19
20
  Controller,
20
21
  Device,
22
+ DeviceType,
21
23
  DeviceTypeToSoftwareImageFile,
22
24
  Location,
23
25
  LocationType,
@@ -27,7 +29,7 @@ from nautobot.dcim.models import (
27
29
  from nautobot.dcim.tables import LocationTable
28
30
  from nautobot.dcim.tests.test_views import create_test_device
29
31
  from nautobot.extras.choices import RelationshipRequiredSideChoices, RelationshipSideChoices, RelationshipTypeChoices
30
- from nautobot.extras.models import Relationship, RelationshipAssociation, Status
32
+ from nautobot.extras.models import Relationship, RelationshipAssociation, Role, Status
31
33
  from nautobot.ipam.models import VLAN, VLANGroup
32
34
 
33
35
 
@@ -486,6 +488,224 @@ class RelationshipTest(RelationshipBaseTest, ModelTestCases.BaseModelTestCase):
486
488
  with self.assertNumQueries(0):
487
489
  manager_method(Location)
488
490
 
491
+ def test_required_related_object_errors(self):
492
+ """
493
+ Confirm that the fix in https://github.com/nautobot/nautobot/pull/5570 is working as expected
494
+ """
495
+ device_ct = ContentType.objects.get_for_model(Device)
496
+ status = Status.objects.get_for_model(Device).first()
497
+ device_type = DeviceType.objects.exclude(manufacturer__isnull=True).first()
498
+ # Create a Device with role Role 1
499
+ role_1 = Role.objects.create(name="Role 1")
500
+ role_1.content_types.add(ContentType.objects.get_for_model(Device))
501
+ device_1 = Device.objects.create(
502
+ device_type=device_type, role=role_1, name="Device 1", location=self.locations[0], status=status
503
+ )
504
+ # Create a Device with role Role 2
505
+ role_2 = Role.objects.create(name="Role 2")
506
+ role_2.content_types.add(ContentType.objects.get_for_model(Device))
507
+ device_2 = Device.objects.create(
508
+ device_type=device_type, role=role_2, name="Device 2", location=self.locations[0], status=status
509
+ )
510
+ # Create a Device with role Role 3
511
+ role_3 = Role.objects.create(name="Role 3")
512
+ role_3.content_types.add(ContentType.objects.get_for_model(Device))
513
+ device_3 = Device.objects.create(
514
+ device_type=device_type, role=role_3, name="Device 3", location=self.locations[0], status=status
515
+ )
516
+ # Create a one-to-many relationship with destination required, source filter: {"role": ["Role 1"]}
517
+ # and destination filter {"role": ["Role 2"]}
518
+ relationship = Relationship.objects.create(
519
+ label="Device to Devices",
520
+ key="device_to_devices",
521
+ source_type=device_ct,
522
+ source_filter={"role": ["Role 1"]},
523
+ destination_type=device_ct,
524
+ destination_filter={"role": ["Role 2"]},
525
+ type=RelationshipTypeChoices.TYPE_ONE_TO_MANY,
526
+ required_on="destination",
527
+ )
528
+ # Attempt to update device_3 which will not be in the queryset filtered by the destination filter
529
+ # Assert that the form is valid and no ValueError is raised.
530
+ update_status = Status.objects.get_for_model(Device).last()
531
+ update_data_for_device_3 = {
532
+ "location": device_3.location.pk,
533
+ "device_type": device_3.device_type.pk,
534
+ "role": device_3.role.pk,
535
+ "name": device_3.name,
536
+ "status": update_status.pk,
537
+ }
538
+ form = DeviceForm(instance=device_3, data=update_data_for_device_3)
539
+ self.assertTrue(form.is_valid())
540
+ # Attempt to update device_1 which will not be in the destination filter,
541
+ # but is in the source filter.
542
+ update_data_for_device_1 = {
543
+ "location": device_1.location.pk,
544
+ "device_type": device_1.device_type.pk,
545
+ "role": device_1.role.pk,
546
+ "name": device_1.name,
547
+ "status": update_status.pk,
548
+ }
549
+ form2 = DeviceForm(instance=device_1, data=update_data_for_device_1)
550
+ self.assertTrue(form2.is_valid())
551
+ # Attempt to update device_2 which will be in the destination filter, so it should
552
+ # require the relationship.
553
+ update_data_for_device_2 = {
554
+ "location": device_2.location.pk,
555
+ "device_type": device_2.device_type.pk,
556
+ "role": device_2.role.pk,
557
+ "name": "Device 2",
558
+ "status": update_status.pk,
559
+ }
560
+ form3 = DeviceForm(instance=device_2, data=update_data_for_device_2)
561
+ self.assertFalse(form3.is_valid())
562
+ # Device 1 has a relationship to Device 2
563
+ update_data_for_device_1 = {
564
+ "location": device_1.location.pk,
565
+ "device_type": device_1.device_type.pk,
566
+ "role": device_1.role.pk,
567
+ "name": device_1.name,
568
+ "status": update_status.pk,
569
+ "cr_device_to_devices__destination": [device_2.pk],
570
+ }
571
+ form4 = DeviceForm(instance=device_1, data=update_data_for_device_1)
572
+ self.assertTrue(form4.is_valid())
573
+ form4.save()
574
+ # Device 2 has a relationship to Device 1, form should validate and save.
575
+ update_data_for_device_2 = {
576
+ "location": device_2.location.pk,
577
+ "device_type": device_2.device_type.pk,
578
+ "role": device_2.role.pk,
579
+ "name": "Device 2",
580
+ "status": update_status.pk,
581
+ "cr_device_to_devices__source": device_1.pk,
582
+ }
583
+ form5 = DeviceForm(instance=device_2, data=update_data_for_device_2)
584
+ self.assertTrue(form5.is_valid())
585
+ form5.save()
586
+ # Device 2 has a relationship to Device 3, save should fail as Device 3 doesn't match filter.
587
+ update_data_for_device_2 = {
588
+ "location": device_2.location.pk,
589
+ "device_type": device_2.device_type.pk,
590
+ "role": device_2.role.pk,
591
+ "name": "Device 2",
592
+ "status": update_status.pk,
593
+ "cr_device_to_devices__source": device_3.pk,
594
+ }
595
+ form6 = DeviceForm(instance=device_2, data=update_data_for_device_2)
596
+ with self.assertRaises(ValidationError):
597
+ form6.save()
598
+ # Device 1 has a relationship to Device 3, save should fail as Device 3 doesn't match filter.
599
+ update_data_for_device_1 = {
600
+ "location": device_1.location.pk,
601
+ "device_type": device_1.device_type.pk,
602
+ "role": device_1.role.pk,
603
+ "name": "Device 1",
604
+ "status": update_status.pk,
605
+ "cr_device_to_devices__destination": [
606
+ device_3.pk,
607
+ ],
608
+ }
609
+ form6 = DeviceForm(instance=device_1, data=update_data_for_device_1)
610
+ with self.assertRaises(ValidationError):
611
+ form6.save()
612
+
613
+ relationship.required_on = "source"
614
+ relationship.save()
615
+ # Attempt to update device_3 which will not be in the queryset filtered by the destination filter
616
+ # Assert that the form is valid and no ValueError is raised. This ensures that an object that
617
+ # does not take part in any relationships can still be updated, addressing issue #5569.
618
+ update_status = Status.objects.get_for_model(Device).last()
619
+ update_data_for_device_3 = {
620
+ "location": device_3.location.pk,
621
+ "device_type": device_3.device_type.pk,
622
+ "role": device_3.role.pk,
623
+ "name": device_3.name,
624
+ "status": update_status.pk,
625
+ }
626
+ form = DeviceForm(instance=device_3, data=update_data_for_device_3)
627
+ self.assertTrue(form.is_valid())
628
+ # Attempt to update device_1 which will not be in the destination filter,
629
+ # but is in the source filter. Should fail as device_to_devices is required.
630
+ device_1.delete()
631
+ device_1 = Device.objects.create(
632
+ device_type=device_type, role=role_1, name="Device 1", location=self.locations[0], status=status
633
+ )
634
+ update_data_for_device_1 = {
635
+ "location": device_1.location.pk,
636
+ "device_type": device_1.device_type.pk,
637
+ "role": device_1.role.pk,
638
+ "name": device_1.name,
639
+ "status": update_status.pk,
640
+ }
641
+ form2 = DeviceForm(instance=device_1, data=update_data_for_device_1)
642
+ self.assertFalse(form2.is_valid())
643
+ # Attempt to update device_2 which will be in the destination filter, which should not require the
644
+ # relationship anymore.
645
+ device_2.delete()
646
+ device_2 = Device.objects.create(
647
+ device_type=device_type, role=role_2, name="Device 2", location=self.locations[0], status=status
648
+ )
649
+ update_data_for_device_2 = {
650
+ "location": device_2.location.pk,
651
+ "device_type": device_2.device_type.pk,
652
+ "role": device_2.role.pk,
653
+ "name": "Device 2",
654
+ "status": update_status.pk,
655
+ }
656
+ form3 = DeviceForm(instance=device_2, data=update_data_for_device_2)
657
+ self.assertTrue(form3.is_valid())
658
+ # Device 1 has a relationship to Device 2
659
+ update_data_for_device_1 = {
660
+ "location": device_1.location.pk,
661
+ "device_type": device_1.device_type.pk,
662
+ "role": device_1.role.pk,
663
+ "name": device_1.name,
664
+ "status": update_status.pk,
665
+ "cr_device_to_devices__destination": [device_2.pk],
666
+ }
667
+ form4 = DeviceForm(instance=device_1, data=update_data_for_device_1)
668
+ self.assertTrue(form4.is_valid())
669
+ form4.save()
670
+ # Device 2 has a relationship to Device 1, form should validate and save.
671
+ update_data_for_device_2 = {
672
+ "location": device_2.location.pk,
673
+ "device_type": device_2.device_type.pk,
674
+ "role": device_2.role.pk,
675
+ "name": "Device 2",
676
+ "status": update_status.pk,
677
+ "cr_device_to_devices__source": device_1.pk,
678
+ }
679
+ form5 = DeviceForm(instance=device_2, data=update_data_for_device_2)
680
+ self.assertTrue(form5.is_valid())
681
+ form5.save()
682
+ # Device 2 has a relationship to Device 3, save should fail as Device 3 doesn't match filter.
683
+ update_data_for_device_2 = {
684
+ "location": device_2.location.pk,
685
+ "device_type": device_2.device_type.pk,
686
+ "role": device_2.role.pk,
687
+ "name": "Device 2",
688
+ "status": update_status.pk,
689
+ "cr_device_to_devices__source": device_3.pk,
690
+ }
691
+ form6 = DeviceForm(instance=device_2, data=update_data_for_device_2)
692
+ with self.assertRaises(ValidationError):
693
+ form6.save()
694
+ # Device 1 has a relationship to Device 3, save should fail as Device 3 doesn't match filter.
695
+ update_data_for_device_1 = {
696
+ "location": device_1.location.pk,
697
+ "device_type": device_1.device_type.pk,
698
+ "role": device_1.role.pk,
699
+ "name": "Device 1",
700
+ "status": update_status.pk,
701
+ "cr_device_to_devices__destination": [
702
+ device_3.pk,
703
+ ],
704
+ }
705
+ form6 = DeviceForm(instance=device_1, data=update_data_for_device_1)
706
+ with self.assertRaises(ValidationError):
707
+ form6.save()
708
+
489
709
 
490
710
  class RelationshipAssociationTest(RelationshipBaseTest, ModelTestCases.BaseModelTestCase):
491
711
  model = RelationshipAssociation
@@ -2532,6 +2532,27 @@ class JobButtonRenderingTestCase(TestCase):
2532
2532
  )
2533
2533
 
2534
2534
 
2535
+ class JobCustomTemplateTestCase(TestCase):
2536
+ @classmethod
2537
+ def setUpTestData(cls):
2538
+ # Job model objects are automatically created during database migrations
2539
+
2540
+ # But we do need to make sure the ones we're testing are flagged appropriately
2541
+ cls.example_job = Job.objects.get(job_class_name="ExampleCustomFormJob")
2542
+ cls.example_job.enabled = True
2543
+ cls.example_job.save()
2544
+
2545
+ cls.run_url = reverse("extras:job_run", kwargs={"pk": cls.example_job.pk})
2546
+
2547
+ def test_rendering_custom_template(self):
2548
+ obj_perm = ObjectPermission(name="Test permission", actions=["view", "run"])
2549
+ obj_perm.save()
2550
+ obj_perm.users.add(self.user)
2551
+ obj_perm.object_types.add(ContentType.objects.get_for_model(Job))
2552
+ with self.assertTemplateUsed("example_app/custom_job_form.html"):
2553
+ self.client.get(self.run_url)
2554
+
2555
+
2535
2556
  # TODO: Convert to StandardTestCases.Views
2536
2557
  class ObjectChangeTestCase(TestCase):
2537
2558
  user_permissions = ("extras.view_objectchange",)
nautobot/extras/utils.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import collections
2
+ import contextlib
2
3
  import hashlib
3
4
  import hmac
4
5
  import logging
@@ -14,6 +15,7 @@ from django.db import transaction
14
15
  from django.db.models import Q
15
16
  from django.template.loader import get_template, TemplateDoesNotExist
16
17
  from django.utils.deconstruct import deconstructible
18
+ import redis.exceptions
17
19
 
18
20
  from nautobot.core.choices import ColorChoices
19
21
  from nautobot.core.constants import CHARFIELD_MAX_LENGTH
@@ -109,12 +111,17 @@ class ChangeLoggedModelsQuery(FeaturedQueryMixin):
109
111
  def change_logged_models_queryset():
110
112
  """
111
113
  Cacheable function for cases where we need this queryset many times, such as when saving multiple objects.
114
+
115
+ Cache is cleared by post_migrate signal (nautobot.extras.signals.post_migrate_clear_content_type_caches).
112
116
  """
117
+ queryset = None
113
118
  cache_key = "nautobot.extras.utils.change_logged_models_queryset"
114
- queryset = cache.get(cache_key)
119
+ with contextlib.suppress(redis.exceptions.ConnectionError):
120
+ queryset = cache.get(cache_key)
115
121
  if queryset is None:
116
122
  queryset = ChangeLoggedModelsQuery().as_queryset()
117
- cache.set(cache_key, queryset)
123
+ with contextlib.suppress(redis.exceptions.ConnectionError):
124
+ cache.set(cache_key, queryset)
118
125
  return queryset
119
126
 
120
127
 
@@ -163,12 +170,34 @@ class FeatureQuery:
163
170
 
164
171
  >>> FeatureQuery('statuses').get_choices()
165
172
  [('dcim.device', 13), ('dcim.rack', 34)]
173
+
174
+ Cache is cleared by post_migrate signal (nautobot.extras.signals.post_migrate_clear_content_type_caches).
166
175
  """
167
- return [(f"{ct.app_label}.{ct.model}", ct.pk) for ct in ContentType.objects.filter(self.get_query())]
176
+ choices = None
177
+ cache_key = f"nautobot.extras.utils.FeatureQuery.choices.{self.feature}"
178
+ with contextlib.suppress(redis.exceptions.ConnectionError):
179
+ choices = cache.get(cache_key)
180
+ if choices is None:
181
+ choices = [(f"{ct.app_label}.{ct.model}", ct.pk) for ct in ContentType.objects.filter(self.get_query())]
182
+ with contextlib.suppress(redis.exceptions.ConnectionError):
183
+ cache.set(cache_key, choices)
184
+ return choices
168
185
 
169
186
  def list_subclasses(self):
170
- """Return a list of model classes that declare this feature."""
171
- return [ct.model_class() for ct in ContentType.objects.filter(self.get_query())]
187
+ """
188
+ Return a list of model classes that declare this feature.
189
+
190
+ Cache is cleared by post_migrate signal (nautobot.extras.signals.post_migrate_clear_content_type_caches).
191
+ """
192
+ subclasses = None
193
+ cache_key = f"nautobot.extras.utils.FeatureQuery.subclasses.{self.feature}"
194
+ with contextlib.suppress(redis.exceptions.ConnectionError):
195
+ subclasses = cache.get(cache_key)
196
+ if subclasses is None:
197
+ subclasses = [ct.model_class() for ct in ContentType.objects.filter(self.get_query())]
198
+ with contextlib.suppress(redis.exceptions.ConnectionError):
199
+ cache.set(cache_key, subclasses)
200
+ return subclasses
172
201
 
173
202
 
174
203
  @deconstructible
nautobot/extras/views.py CHANGED
@@ -1,4 +1,3 @@
1
- from datetime import timedelta
2
1
  import logging
3
2
 
4
3
  from celery import chain
@@ -1345,55 +1344,25 @@ class JobRunView(ObjectPermissionRequiredMixin, View):
1345
1344
  schedule_type = schedule_form.cleaned_data["_schedule_type"]
1346
1345
 
1347
1346
  if (not dryrun and job_model.approval_required) or schedule_type in JobExecutionType.SCHEDULE_CHOICES:
1348
- crontab = ""
1349
-
1350
- if schedule_type == JobExecutionType.TYPE_IMMEDIATELY:
1351
- # The job must be approved.
1352
- # If the schedule_type is immediate, we still create the task, but mark it for approval
1353
- # as a once in the future task with the due date set to the current time. This means
1354
- # when approval is granted, the task is immediately due for execution.
1355
- schedule_type = JobExecutionType.TYPE_FUTURE
1356
- schedule_datetime = timezone.now()
1357
- schedule_name = f"{job_model} - {schedule_datetime}"
1358
-
1359
- else:
1360
- schedule_name = schedule_form.cleaned_data["_schedule_name"]
1361
-
1362
- if schedule_type == JobExecutionType.TYPE_CUSTOM:
1363
- crontab = schedule_form.cleaned_data["_recurrence_custom_time"]
1364
- # doing .get("key", "default") returns None instead of "default" here for some reason
1365
- schedule_datetime = schedule_form.cleaned_data.get("_schedule_start_time")
1366
- if schedule_datetime is None:
1367
- # "_schedule_start_time" is checked against ScheduledJob.earliest_possible_time()
1368
- # which returns timezone.now() + timedelta(seconds=15)
1369
- schedule_datetime = timezone.now() + timedelta(seconds=20)
1370
- else:
1371
- schedule_datetime = schedule_form.cleaned_data["_schedule_start_time"]
1372
-
1373
- celery_kwargs = {"nautobot_job_profile": profile, "queue": task_queue}
1374
- scheduled_job = ScheduledJob(
1375
- name=schedule_name,
1376
- task=job_model.class_path,
1377
- job_model=job_model,
1378
- start_time=schedule_datetime,
1379
- description=f"Nautobot job {schedule_name} scheduled by {request.user} for {schedule_datetime}",
1380
- kwargs=job_class.serialize_data(job_form.cleaned_data),
1381
- celery_kwargs=celery_kwargs,
1347
+ scheduled_job = ScheduledJob.create_schedule(
1348
+ job_model,
1349
+ request.user,
1350
+ name=schedule_form.cleaned_data.get("_schedule_name"),
1351
+ start_time=schedule_form.cleaned_data.get("_schedule_start_time"),
1382
1352
  interval=schedule_type,
1383
- one_off=schedule_type == JobExecutionType.TYPE_FUTURE,
1384
- queue=task_queue,
1385
- user=request.user,
1353
+ crontab=schedule_form.cleaned_data.get("_recurrence_custom_time"),
1386
1354
  approval_required=job_model.approval_required,
1387
- crontab=crontab,
1355
+ task_queue=task_queue,
1356
+ profile=profile,
1357
+ **job_class.serialize_data(job_form.cleaned_data),
1388
1358
  )
1389
- scheduled_job.validated_save()
1390
1359
 
1391
1360
  if job_model.approval_required:
1392
- messages.success(request, f"Job {schedule_name} successfully submitted for approval")
1393
- return redirect(return_url if return_url else "extras:scheduledjob_approval_queue_list")
1361
+ messages.success(request, f"Job {scheduled_job.name} successfully submitted for approval")
1362
+ return redirect(return_url or "extras:scheduledjob_approval_queue_list")
1394
1363
  else:
1395
- messages.success(request, f"Job {schedule_name} successfully scheduled")
1396
- return redirect(return_url if return_url else "extras:scheduledjob_list")
1364
+ messages.success(request, f"Job {scheduled_job.name} successfully scheduled")
1365
+ return redirect(return_url or "extras:scheduledjob_list")
1397
1366
 
1398
1367
  else:
1399
1368
  # Enqueue job for immediate execution
@@ -1787,8 +1756,13 @@ class JobLogEntryTableView(generic.GenericView):
1787
1756
  else:
1788
1757
  queryset = instance.job_log_entries.all()
1789
1758
  log_table = tables.JobLogEntryTable(data=queryset, user=request.user)
1790
- RequestConfig(request).configure(log_table)
1791
- return HttpResponse(log_table.as_html(request))
1759
+ paginate = {
1760
+ "paginator_class": EnhancedPaginator,
1761
+ "per_page": get_paginate_count(request),
1762
+ }
1763
+ RequestConfig(request, paginate).configure(log_table)
1764
+ table = log_table.as_html(request)
1765
+ return HttpResponse(table)
1792
1766
 
1793
1767
 
1794
1768
  #
nautobot/ipam/models.py CHANGED
@@ -1011,7 +1011,7 @@ class IPAddress(PrimaryModel):
1011
1011
  parent = models.ForeignKey(
1012
1012
  "ipam.Prefix",
1013
1013
  blank=True,
1014
- null=True,
1014
+ null=True, # TODO remove this, it shouldn't be permitted for the database!
1015
1015
  related_name="ip_addresses", # `IPAddress` to use `related_name="ip_addresses"`
1016
1016
  on_delete=models.PROTECT,
1017
1017
  help_text="The parent Prefix of this IPAddress.",
@@ -1108,7 +1108,7 @@ class IPAddress(PrimaryModel):
1108
1108
  raise ValidationError({"namespace": "No suitable parent Prefix exists in this Namespace"}) from e
1109
1109
 
1110
1110
  def clean(self):
1111
- super().clean()
1111
+ self.address = self.address # not a no-op - forces re-calling of self._deconstruct_address()
1112
1112
 
1113
1113
  # Validate that host is not being modified
1114
1114
  if self.present_in_database:
@@ -1122,8 +1122,8 @@ class IPAddress(PrimaryModel):
1122
1122
 
1123
1123
  closest_parent = self._get_closest_parent()
1124
1124
  # Validate `parent` can be used as the parent for this ipaddress
1125
- if self.parent and closest_parent:
1126
- if self.parent != closest_parent:
1125
+ if closest_parent is not None:
1126
+ if self.parent is not None and self.parent != closest_parent:
1127
1127
  raise ValidationError(
1128
1128
  {
1129
1129
  "parent": (
@@ -1135,23 +1135,20 @@ class IPAddress(PrimaryModel):
1135
1135
  self.parent = closest_parent
1136
1136
  self._namespace = None
1137
1137
 
1138
- def save(self, *args, **kwargs):
1139
1138
  # 3.0 TODO: uncomment the below to enforce this constraint
1140
1139
  # if self.parent.type != choices.PrefixTypeChoices.TYPE_NETWORK:
1141
1140
  # err_msg = f"IP addresses cannot be created in {self.parent.type} prefixes. You must create a network prefix first."
1142
1141
  # raise ValidationError({"address": err_msg})
1143
1142
 
1144
- self.address = self.address # not a no-op - forces re-calling of self._deconstruct_address()
1145
-
1146
1143
  # Force dns_name to lowercase
1147
1144
  if not self.dns_name.islower:
1148
1145
  self.dns_name = self.dns_name.lower()
1149
1146
 
1150
- # Host and mask_length are required to get closest parent
1151
- closest_parent = self._get_closest_parent()
1152
- if closest_parent is not None:
1153
- self.parent = closest_parent
1154
- self._namespace = None
1147
+ super().clean()
1148
+
1149
+ def save(self, *args, **kwargs):
1150
+ self.clean() # MUST do data fixup as above
1151
+
1155
1152
  super().save(*args, **kwargs)
1156
1153
 
1157
1154
  @property
@@ -997,8 +997,9 @@ class TestIPAddress(ModelTestCases.BaseModelTestCase):
997
997
  def test_duplicate_global_unique(self):
998
998
  """Test that duplicate IPs in the same Namespace raises an error."""
999
999
  IPAddress.objects.create(address="192.0.2.1/24", status=self.status, namespace=self.namespace)
1000
- with self.assertRaises(IntegrityError):
1001
- IPAddress.objects.create(address="192.0.2.1/24", status=self.status, namespace=self.namespace)
1000
+ duplicate_ip = IPAddress(address="192.0.2.1/24", status=self.status, namespace=self.namespace)
1001
+ with self.assertRaises(ValidationError):
1002
+ duplicate_ip.full_clean()
1002
1003
 
1003
1004
  def test_multiple_nat_outside_list(self):
1004
1005
  """
nautobot/ipam/views.py CHANGED
@@ -943,14 +943,8 @@ class IPAddressAssignView(view_mixins.GetReturnURLMixin, generic.ObjectView):
943
943
  ip_addresses = IPAddress.objects.restrict(request.user, "view").filter(pk__in=pks)
944
944
  interface.ip_addresses.add(*ip_addresses)
945
945
  return redirect(self.get_return_url(request))
946
-
947
- return render(
948
- request,
949
- "ipam/ipaddress_assign.html",
950
- {
951
- "return_url": self.get_return_url(request),
952
- },
953
- )
946
+ messages.error(request, "Please select at least one IP Address from the table.")
947
+ return redirect(request.get_full_path())
954
948
 
955
949
 
956
950
  class IPAddressMergeView(view_mixins.GetReturnURLMixin, view_mixins.ObjectPermissionRequiredMixin, View):
@@ -299,6 +299,7 @@ table.report th a {
299
299
  }
300
300
  .panel table {
301
301
  margin-bottom: 0;
302
+ overflow: hidden;
302
303
  }
303
304
  .panel .table th {
304
305
  border-bottom-width: 1px;
@@ -12,7 +12,7 @@
12
12
 
13
13
 
14
14
  <link rel="icon" href="/projects/core/en/stable/assets/favicon.ico">
15
- <meta name="generator" content="mkdocs-1.6.0, mkdocs-material-9.5.28">
15
+ <meta name="generator" content="mkdocs-1.6.0, mkdocs-material-9.5.29">
16
16
 
17
17
 
18
18
 
@@ -20,7 +20,7 @@
20
20
 
21
21
 
22
22
 
23
- <link rel="stylesheet" href="/projects/core/en/stable/assets/stylesheets/main.6543a935.min.css">
23
+ <link rel="stylesheet" href="/projects/core/en/stable/assets/stylesheets/main.76a95c52.min.css">
24
24
 
25
25
 
26
26
  <link rel="stylesheet" href="/projects/core/en/stable/assets/stylesheets/palette.06af60db.min.css">
@@ -218,7 +218,7 @@
218
218
 
219
219
 
220
220
  <li class="md-tabs__item">
221
- <a href="/projects/core/en/stable/overview/index.html" class="md-tabs__link">
221
+ <a href="/projects/core/en/stable/index.html" class="md-tabs__link">
222
222
 
223
223
 
224
224
 
@@ -387,7 +387,7 @@
387
387
 
388
388
 
389
389
  <div class="md-nav__link md-nav__container">
390
- <a href="/projects/core/en/stable/overview/index.html" class="md-nav__link ">
390
+ <a href="/projects/core/en/stable/index.html" class="md-nav__link ">
391
391
 
392
392
 
393
393
  <span class="md-ellipsis">
@@ -18,7 +18,7 @@
18
18
 
19
19
 
20
20
  <link rel="icon" href="../assets/favicon.ico">
21
- <meta name="generator" content="mkdocs-1.6.0, mkdocs-material-9.5.28">
21
+ <meta name="generator" content="mkdocs-1.6.0, mkdocs-material-9.5.29">
22
22
 
23
23
 
24
24
 
@@ -26,7 +26,7 @@
26
26
 
27
27
 
28
28
 
29
- <link rel="stylesheet" href="../assets/stylesheets/main.6543a935.min.css">
29
+ <link rel="stylesheet" href="../assets/stylesheets/main.76a95c52.min.css">
30
30
 
31
31
 
32
32
  <link rel="stylesheet" href="../assets/stylesheets/palette.06af60db.min.css">
@@ -238,7 +238,7 @@
238
238
 
239
239
 
240
240
  <li class="md-tabs__item">
241
- <a href="../overview/index.html" class="md-tabs__link">
241
+ <a href="../index.html" class="md-tabs__link">
242
242
 
243
243
 
244
244
 
@@ -409,7 +409,7 @@
409
409
 
410
410
 
411
411
  <div class="md-nav__link md-nav__container">
412
- <a href="../overview/index.html" class="md-nav__link ">
412
+ <a href="../index.html" class="md-nav__link ">
413
413
 
414
414
 
415
415
  <span class="md-ellipsis">
@@ -16,7 +16,7 @@
16
16
 
17
17
 
18
18
  <link rel="icon" href="../assets/favicon.ico">
19
- <meta name="generator" content="mkdocs-1.6.0, mkdocs-material-9.5.28">
19
+ <meta name="generator" content="mkdocs-1.6.0, mkdocs-material-9.5.29">
20
20
 
21
21
 
22
22
 
@@ -24,7 +24,7 @@
24
24
 
25
25
 
26
26
 
27
- <link rel="stylesheet" href="../assets/stylesheets/main.6543a935.min.css">
27
+ <link rel="stylesheet" href="../assets/stylesheets/main.76a95c52.min.css">
28
28
 
29
29
 
30
30
  <link rel="stylesheet" href="../assets/stylesheets/palette.06af60db.min.css">
@@ -236,7 +236,7 @@
236
236
 
237
237
 
238
238
  <li class="md-tabs__item">
239
- <a href="../overview/index.html" class="md-tabs__link">
239
+ <a href="../index.html" class="md-tabs__link">
240
240
 
241
241
 
242
242
 
@@ -407,7 +407,7 @@
407
407
 
408
408
 
409
409
  <div class="md-nav__link md-nav__container">
410
- <a href="../overview/index.html" class="md-nav__link ">
410
+ <a href="../index.html" class="md-nav__link ">
411
411
 
412
412
 
413
413
  <span class="md-ellipsis">