nautobot 2.1.7__py3-none-any.whl → 2.1.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 (338) hide show
  1. nautobot/apps/api.py +1 -2
  2. nautobot/apps/utils.py +4 -0
  3. nautobot/apps/views.py +2 -0
  4. nautobot/circuits/api/urls.py +1 -2
  5. nautobot/circuits/api/views.py +0 -12
  6. nautobot/circuits/tests/integration/test_relationships.py +0 -4
  7. nautobot/core/api/routers.py +25 -3
  8. nautobot/core/api/utils.py +4 -0
  9. nautobot/core/api/views.py +21 -15
  10. nautobot/core/celery/schedulers.py +13 -0
  11. nautobot/core/choices.py +0 -21
  12. nautobot/core/models/__init__.py +1 -1
  13. nautobot/core/models/tree_queries.py +29 -7
  14. nautobot/core/releases.py +1 -1
  15. nautobot/core/settings.py +9 -0
  16. nautobot/core/settings_funcs.py +0 -18
  17. nautobot/core/signals.py +5 -5
  18. nautobot/core/tasks.py +7 -3
  19. nautobot/core/templates/admin/base.html +23 -94
  20. nautobot/core/templates/generic/object_list.html +2 -0
  21. nautobot/core/templates/graphene/graphiql.html +18 -47
  22. nautobot/core/templates/inc/footer.html +5 -5
  23. nautobot/core/templates/inc/nav_menu.html +0 -7
  24. nautobot/core/templates/nautobot_config.py.j2 +6 -0
  25. nautobot/core/templates/rest_framework/api.html +12 -5
  26. nautobot/core/testing/mixins.py +13 -5
  27. nautobot/core/tests/integration/test_plugin_navbar.py +7 -21
  28. nautobot/core/tests/integration/test_view_authentication.py +67 -0
  29. nautobot/core/tests/runner.py +25 -2
  30. nautobot/core/tests/test_graphql.py +2 -14
  31. nautobot/core/tests/test_models.py +3 -3
  32. nautobot/core/tests/test_navigations.py +67 -10
  33. nautobot/core/tests/test_releases.py +9 -3
  34. nautobot/core/tests/test_views.py +23 -16
  35. nautobot/core/utils/lookup.py +124 -0
  36. nautobot/core/views/__init__.py +3 -7
  37. nautobot/core/views/generic.py +9 -0
  38. nautobot/dcim/api/urls.py +1 -2
  39. nautobot/dcim/api/views.py +1 -12
  40. nautobot/dcim/choices.py +56 -0
  41. nautobot/dcim/models/racks.py +1 -3
  42. nautobot/dcim/navigation.py +1 -1
  43. nautobot/dcim/templates/dcim/device/lldp_neighbors.html +67 -43
  44. nautobot/dcim/tests/test_api.py +3 -0
  45. nautobot/dcim/tests/test_filters.py +0 -28
  46. nautobot/dcim/views.py +5 -2
  47. nautobot/extras/api/urls.py +1 -2
  48. nautobot/extras/api/views.py +0 -10
  49. nautobot/extras/choices.py +14 -0
  50. nautobot/extras/models/customfields.py +93 -34
  51. nautobot/extras/models/groups.py +1 -1
  52. nautobot/extras/models/relationships.py +32 -19
  53. nautobot/extras/navigation.py +3 -2
  54. nautobot/extras/plugins/__init__.py +8 -0
  55. nautobot/extras/plugins/views.py +6 -9
  56. nautobot/extras/querysets.py +1 -1
  57. nautobot/extras/signals.py +12 -6
  58. nautobot/extras/templates/extras/customfield.html +22 -14
  59. nautobot/extras/templatetags/job_buttons.py +7 -0
  60. nautobot/extras/templatetags/plugins.py +5 -1
  61. nautobot/extras/tests/test_customfields.py +323 -287
  62. nautobot/extras/tests/test_dynamicgroups.py +1 -1
  63. nautobot/extras/tests/test_jobs.py +2 -2
  64. nautobot/extras/tests/test_plugins.py +41 -0
  65. nautobot/extras/tests/test_relationships.py +31 -14
  66. nautobot/extras/tests/test_views.py +124 -1
  67. nautobot/extras/utils.py +7 -3
  68. nautobot/extras/views.py +10 -10
  69. nautobot/ipam/api/urls.py +1 -2
  70. nautobot/ipam/api/views.py +6 -13
  71. nautobot/ipam/tables.py +0 -1
  72. nautobot/ipam/tests/test_graphql.py +2 -3
  73. nautobot/ipam/views.py +12 -10
  74. nautobot/project-static/css/base.css +1 -0
  75. nautobot/project-static/docs/404.html +30 -2
  76. nautobot/project-static/docs/apps/index.html +30 -2
  77. nautobot/project-static/docs/apps/nautobot-apps.html +30 -2
  78. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +30 -2
  79. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +30 -2
  80. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +410 -410
  81. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +30 -2
  82. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +386 -358
  83. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +30 -2
  84. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +30 -2
  85. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +30 -2
  86. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +30 -2
  87. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +45 -17
  88. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +30 -2
  89. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +30 -2
  90. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +30 -2
  91. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +759 -602
  92. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +30 -2
  93. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +30 -2
  94. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +30 -2
  95. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +528 -467
  96. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +205 -109
  97. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +30 -2
  98. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +1265 -785
  99. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +1827 -1746
  100. nautobot/project-static/docs/development/apps/api/configuration-view.html +30 -2
  101. nautobot/project-static/docs/development/apps/api/database-backend-config.html +30 -2
  102. nautobot/project-static/docs/development/apps/api/models/django-admin.html +30 -2
  103. nautobot/project-static/docs/development/apps/api/models/global-search.html +30 -2
  104. nautobot/project-static/docs/development/apps/api/models/graphql.html +30 -2
  105. nautobot/project-static/docs/development/apps/api/models/index.html +30 -2
  106. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +31 -3
  107. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +30 -2
  108. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +30 -2
  109. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +30 -2
  110. nautobot/project-static/docs/development/apps/api/platform-features/index.html +30 -2
  111. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +30 -2
  112. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +30 -2
  113. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +30 -2
  114. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +30 -2
  115. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +30 -2
  116. nautobot/project-static/docs/development/apps/api/prometheus.html +30 -2
  117. nautobot/project-static/docs/development/apps/api/setup.html +30 -2
  118. nautobot/project-static/docs/development/apps/api/testing.html +33 -5
  119. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +30 -2
  120. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +30 -2
  121. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +30 -2
  122. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +33 -5
  123. nautobot/project-static/docs/development/apps/api/ui-extensions/object-detail-views.html +13 -5559
  124. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +5594 -0
  125. nautobot/project-static/docs/development/apps/api/ui-extensions/tabs.html +3 -3
  126. nautobot/project-static/docs/development/apps/api/views/base-template.html +30 -2
  127. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +44 -11
  128. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +47 -14
  129. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +30 -2
  130. nautobot/project-static/docs/development/apps/api/views/index.html +30 -2
  131. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +30 -2
  132. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +30 -2
  133. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +30 -2
  134. nautobot/project-static/docs/development/apps/api/views/notes.html +30 -2
  135. nautobot/project-static/docs/development/apps/api/views/rest-api.html +30 -2
  136. nautobot/project-static/docs/development/apps/api/views/urls.html +30 -2
  137. nautobot/project-static/docs/development/apps/index.html +30 -2
  138. nautobot/project-static/docs/development/apps/migration/code-updates.html +30 -2
  139. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +30 -2
  140. nautobot/project-static/docs/development/apps/migration/from-v1.html +30 -2
  141. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +30 -2
  142. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +30 -2
  143. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +30 -2
  144. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +30 -2
  145. nautobot/project-static/docs/development/apps/porting-from-netbox.html +30 -2
  146. nautobot/project-static/docs/development/core/application-registry.html +30 -2
  147. nautobot/project-static/docs/development/core/best-practices.html +33 -5
  148. nautobot/project-static/docs/development/core/bootstrap-ui.html +30 -2
  149. nautobot/project-static/docs/development/core/caching.html +5481 -0
  150. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +30 -2
  151. nautobot/project-static/docs/development/core/extending-models.html +33 -5
  152. nautobot/project-static/docs/development/core/generic-views.html +30 -2
  153. nautobot/project-static/docs/development/core/getting-started.html +49 -12
  154. nautobot/project-static/docs/development/core/homepage.html +30 -2
  155. nautobot/project-static/docs/development/core/index.html +30 -2
  156. nautobot/project-static/docs/development/core/model-features.html +30 -2
  157. nautobot/project-static/docs/development/core/natural-keys.html +30 -2
  158. nautobot/project-static/docs/development/core/navigation-menu.html +30 -2
  159. nautobot/project-static/docs/development/core/release-checklist.html +30 -2
  160. nautobot/project-static/docs/development/core/role-internals.html +30 -2
  161. nautobot/project-static/docs/development/core/style-guide.html +30 -2
  162. nautobot/project-static/docs/development/core/templates.html +30 -2
  163. nautobot/project-static/docs/development/core/testing.html +30 -2
  164. nautobot/project-static/docs/development/core/user-preferences.html +30 -2
  165. nautobot/project-static/docs/development/index.html +30 -2
  166. nautobot/project-static/docs/development/jobs/index.html +30 -2
  167. nautobot/project-static/docs/development/jobs/migration/from-v1.html +30 -2
  168. nautobot/project-static/docs/index.html +30 -2
  169. nautobot/project-static/docs/objects.inv +0 -0
  170. nautobot/project-static/docs/release-notes/index.html +30 -2
  171. nautobot/project-static/docs/release-notes/version-1.0.html +30 -2
  172. nautobot/project-static/docs/release-notes/version-1.1.html +30 -2
  173. nautobot/project-static/docs/release-notes/version-1.2.html +30 -2
  174. nautobot/project-static/docs/release-notes/version-1.3.html +30 -2
  175. nautobot/project-static/docs/release-notes/version-1.4.html +31 -3
  176. nautobot/project-static/docs/release-notes/version-1.5.html +30 -2
  177. nautobot/project-static/docs/release-notes/version-1.6.html +573 -134
  178. nautobot/project-static/docs/release-notes/version-2.0.html +30 -2
  179. nautobot/project-static/docs/release-notes/version-2.1.html +539 -170
  180. nautobot/project-static/docs/search/search_index.json +1 -1
  181. nautobot/project-static/docs/sitemap.xml +250 -240
  182. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  183. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +30 -2
  184. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +30 -2
  185. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +30 -2
  186. nautobot/project-static/docs/user-guide/administration/configuration/index.html +30 -2
  187. nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +49 -2
  188. nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +30 -2
  189. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +30 -2
  190. nautobot/project-static/docs/user-guide/administration/guides/caching.html +30 -2
  191. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +30 -2
  192. nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +30 -2
  193. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +30 -2
  194. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +30 -2
  195. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +30 -2
  196. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +30 -2
  197. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +30 -2
  198. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +30 -2
  199. nautobot/project-static/docs/user-guide/administration/installation/docker.html +37 -5
  200. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +30 -2
  201. nautobot/project-static/docs/user-guide/administration/installation/health-checks.html +6019 -0
  202. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +30 -2
  203. nautobot/project-static/docs/user-guide/administration/installation/index.html +30 -2
  204. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +30 -2
  205. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +30 -2
  206. nautobot/project-static/docs/user-guide/administration/installation/selinux-troubleshooting.html +33 -5
  207. nautobot/project-static/docs/user-guide/administration/installation/services.html +30 -2
  208. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +30 -2
  209. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +30 -2
  210. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +30 -2
  211. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +30 -2
  212. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +30 -2
  213. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +30 -2
  214. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +30 -2
  215. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +30 -2
  216. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +30 -2
  217. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +30 -2
  218. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +30 -2
  219. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +30 -2
  220. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +30 -2
  221. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +30 -2
  222. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +30 -2
  223. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +30 -2
  224. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +30 -2
  225. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +30 -2
  226. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +30 -2
  227. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +30 -2
  228. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +30 -2
  229. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +30 -2
  230. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +30 -2
  231. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +30 -2
  232. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +30 -2
  233. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +30 -2
  234. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +30 -2
  235. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +30 -2
  236. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +30 -2
  237. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +30 -2
  238. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +30 -2
  239. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +30 -2
  240. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +30 -2
  241. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +30 -2
  242. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +30 -2
  243. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +30 -2
  244. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +30 -2
  245. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +30 -2
  246. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +30 -2
  247. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +30 -2
  248. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +30 -2
  249. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +30 -2
  250. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +30 -2
  251. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +30 -2
  252. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +30 -2
  253. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +30 -2
  254. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +30 -2
  255. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +30 -2
  256. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +30 -2
  257. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +30 -2
  258. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +30 -2
  259. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +30 -2
  260. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +30 -2
  261. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +30 -2
  262. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +30 -2
  263. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +30 -2
  264. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +30 -2
  265. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +30 -2
  266. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +30 -2
  267. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +30 -2
  268. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +30 -2
  269. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +30 -2
  270. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +30 -2
  271. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +30 -2
  272. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +30 -2
  273. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +30 -2
  274. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +30 -2
  275. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +30 -2
  276. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +30 -2
  277. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +33 -5
  278. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +30 -2
  279. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +30 -2
  280. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +30 -2
  281. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +30 -2
  282. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +30 -2
  283. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +30 -2
  284. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +30 -2
  285. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +30 -2
  286. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +30 -2
  287. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +30 -2
  288. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +30 -2
  289. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +30 -2
  290. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +30 -2
  291. nautobot/project-static/docs/user-guide/index.html +30 -2
  292. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +30 -2
  293. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +30 -2
  294. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +111 -15
  295. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +30 -2
  296. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +30 -2
  297. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +30 -2
  298. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +30 -2
  299. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +30 -2
  300. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +30 -2
  301. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +30 -2
  302. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +30 -2
  303. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +30 -2
  304. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +30 -2
  305. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +30 -2
  306. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +30 -2
  307. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +30 -2
  308. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +30 -2
  309. nautobot/project-static/docs/user-guide/platform-functionality/note.html +30 -2
  310. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +30 -2
  311. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +30 -2
  312. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +30 -2
  313. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +30 -2
  314. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +30 -2
  315. nautobot/project-static/docs/user-guide/platform-functionality/role.html +30 -2
  316. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +30 -2
  317. nautobot/project-static/docs/user-guide/platform-functionality/status.html +30 -2
  318. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +30 -2
  319. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +30 -2
  320. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +30 -2
  321. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +30 -2
  322. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +30 -2
  323. nautobot/tenancy/api/urls.py +1 -2
  324. nautobot/tenancy/api/views.py +0 -12
  325. nautobot/tenancy/navigation.py +1 -1
  326. nautobot/tenancy/tests/test_filters.py +0 -168
  327. nautobot/users/api/urls.py +1 -2
  328. nautobot/users/api/views.py +2 -65
  329. nautobot/users/views.py +8 -8
  330. nautobot/virtualization/api/urls.py +1 -2
  331. nautobot/virtualization/api/views.py +0 -12
  332. nautobot/virtualization/tests/test_filters.py +0 -28
  333. {nautobot-2.1.7.dist-info → nautobot-2.1.9.dist-info}/METADATA +2 -2
  334. {nautobot-2.1.7.dist-info → nautobot-2.1.9.dist-info}/RECORD +338 -334
  335. {nautobot-2.1.7.dist-info → nautobot-2.1.9.dist-info}/LICENSE.txt +0 -0
  336. {nautobot-2.1.7.dist-info → nautobot-2.1.9.dist-info}/NOTICE +0 -0
  337. {nautobot-2.1.7.dist-info → nautobot-2.1.9.dist-info}/WHEEL +0 -0
  338. {nautobot-2.1.7.dist-info → nautobot-2.1.9.dist-info}/entry_points.txt +0 -0
@@ -1268,39 +1268,11 @@ class PlatformTestCase(FilterTestCases.NameOnlyFilterTestCase):
1268
1268
  params = {"devices": [devices[0].pk, devices[1].pk]}
1269
1269
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), len(devices))
1270
1270
 
1271
- def test_has_devices(self):
1272
- with self.subTest():
1273
- params = {"has_devices": True}
1274
- self.assertQuerysetEqual(
1275
- self.filterset(params, self.queryset).qs,
1276
- self.queryset.exclude(devices__isnull=True),
1277
- )
1278
- with self.subTest():
1279
- params = {"has_devices": False}
1280
- self.assertQuerysetEqual(
1281
- self.filterset(params, self.queryset).qs,
1282
- self.queryset.exclude(devices__isnull=False),
1283
- )
1284
-
1285
1271
  def test_virtual_machines(self):
1286
1272
  virtual_machines = [VirtualMachine.objects.first(), VirtualMachine.objects.last()]
1287
1273
  params = {"virtual_machines": [virtual_machines[0].pk, virtual_machines[1].pk]}
1288
1274
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), len(virtual_machines))
1289
1275
 
1290
- def test_has_virtual_machines(self):
1291
- with self.subTest():
1292
- params = {"has_virtual_machines": True}
1293
- self.assertQuerysetEqual(
1294
- self.filterset(params, self.queryset).qs,
1295
- self.queryset.exclude(virtual_machines__isnull=True),
1296
- )
1297
- with self.subTest():
1298
- params = {"has_virtual_machines": False}
1299
- self.assertQuerysetEqual(
1300
- self.filterset(params, self.queryset).qs,
1301
- self.queryset.exclude(virtual_machines__isnull=False),
1302
- )
1303
-
1304
1276
 
1305
1277
  class DeviceTestCase(FilterTestCases.FilterTestCase, FilterTestCases.TenancyFilterTestCaseMixin):
1306
1278
  queryset = Device.objects.all()
nautobot/dcim/views.py CHANGED
@@ -11,7 +11,7 @@ from django.forms import (
11
11
  ModelMultipleChoiceField,
12
12
  MultipleHiddenInput,
13
13
  )
14
- from django.shortcuts import get_object_or_404, redirect, render
14
+ from django.shortcuts import get_object_or_404, HttpResponse, redirect, render
15
15
  from django.utils.functional import cached_property
16
16
  from django.utils.html import format_html
17
17
  from django.views.generic import View
@@ -2316,7 +2316,7 @@ class CableCreateView(generic.ObjectEditView):
2316
2316
  "rear-port": forms.ConnectCableToRearPortForm,
2317
2317
  "power-feed": forms.ConnectCableToPowerFeedForm,
2318
2318
  "circuit-termination": forms.ConnectCableToCircuitTerminationForm,
2319
- }[kwargs.get("termination_b_type")]
2319
+ }.get(kwargs.get("termination_b_type"), None)
2320
2320
 
2321
2321
  return super().dispatch(request, *args, **kwargs)
2322
2322
 
@@ -2333,6 +2333,9 @@ class CableCreateView(generic.ObjectEditView):
2333
2333
  return obj
2334
2334
 
2335
2335
  def get(self, request, *args, **kwargs):
2336
+ if self.model_form is None:
2337
+ return HttpResponse(status_code=400)
2338
+
2336
2339
  obj = self.alter_obj(self.get_object(kwargs), request, args, kwargs)
2337
2340
 
2338
2341
  # Parse initial data manually to avoid setting field values as lists
@@ -2,8 +2,7 @@ from nautobot.core.api.routers import OrderedDefaultRouter
2
2
 
3
3
  from . import views
4
4
 
5
- router = OrderedDefaultRouter()
6
- router.APIRootView = views.ExtrasRootView
5
+ router = OrderedDefaultRouter(view_name="Extras")
7
6
 
8
7
  # Computed Fields
9
8
  router.register("computed-fields", views.ComputedFieldViewSet)
@@ -16,7 +16,6 @@ from rest_framework.exceptions import MethodNotAllowed, PermissionDenied, Valida
16
16
  from rest_framework.parsers import JSONParser, MultiPartParser
17
17
  from rest_framework.permissions import IsAuthenticated
18
18
  from rest_framework.response import Response
19
- from rest_framework.routers import APIRootView
20
19
 
21
20
  from nautobot.core.api.authentication import TokenPermissions
22
21
  from nautobot.core.api.utils import get_serializer_for_model
@@ -74,15 +73,6 @@ from nautobot.extras.utils import get_worker_count
74
73
  from . import serializers
75
74
 
76
75
 
77
- class ExtrasRootView(APIRootView):
78
- """
79
- Extras API root view
80
- """
81
-
82
- def get_view_name(self):
83
- return "Extras"
84
-
85
-
86
76
  class NotesViewSetMixin:
87
77
  def restrict_queryset(self, request, *args, **kwargs):
88
78
  """
@@ -64,11 +64,25 @@ class CustomFieldTypeChoices(ChoiceSet):
64
64
  (TYPE_MARKDOWN, "Markdown"),
65
65
  )
66
66
 
67
+ # Types that support validation_minimum/validation_maximum
68
+ MIN_MAX_TYPES = (
69
+ TYPE_TEXT,
70
+ TYPE_INTEGER,
71
+ TYPE_URL,
72
+ TYPE_SELECT,
73
+ TYPE_MULTISELECT,
74
+ TYPE_JSON,
75
+ TYPE_MARKDOWN,
76
+ )
77
+
78
+ # Types that support validation_regex
67
79
  REGEX_TYPES = (
68
80
  TYPE_TEXT,
69
81
  TYPE_URL,
70
82
  TYPE_SELECT,
71
83
  TYPE_MULTISELECT,
84
+ TYPE_JSON,
85
+ TYPE_MARKDOWN,
72
86
  )
73
87
 
74
88
 
@@ -1,11 +1,12 @@
1
1
  from collections import OrderedDict
2
2
  from datetime import date, datetime
3
- from functools import lru_cache
3
+ import json
4
4
  import logging
5
5
  import re
6
6
 
7
7
  from django import forms
8
8
  from django.contrib.contenttypes.models import ContentType
9
+ from django.core.cache import cache
9
10
  from django.core.exceptions import ObjectDoesNotExist
10
11
  from django.core.serializers.json import DjangoJSONEncoder
11
12
  from django.core.validators import RegexValidator, ValidationError
@@ -46,13 +47,20 @@ logger = logging.getLogger(__name__)
46
47
  class ComputedFieldManager(BaseManager.from_queryset(RestrictedQuerySet)):
47
48
  use_in_migrations = True
48
49
 
49
- @lru_cache(maxsize=128)
50
50
  def get_for_model(self, model):
51
51
  """
52
52
  Return all ComputedFields assigned to the given model.
53
53
  """
54
- content_type = ContentType.objects.get_for_model(model._meta.concrete_model)
55
- return self.get_queryset().filter(content_type=content_type)
54
+ concrete_model = model._meta.concrete_model
55
+ cache_key = f"{self.get_for_model.cache_key_prefix}.{concrete_model._meta.label_lower}"
56
+ queryset = cache.get(cache_key)
57
+ if queryset is None:
58
+ content_type = ContentType.objects.get_for_model(concrete_model)
59
+ queryset = self.get_queryset().filter(content_type=content_type)
60
+ cache.set(cache_key, queryset)
61
+ return queryset
62
+
63
+ get_for_model.cache_key_prefix = "nautobot.extras.computedfield.get_for_model"
56
64
 
57
65
 
58
66
  @extras_features("graphql")
@@ -298,7 +306,6 @@ class CustomFieldModel(models.Model):
298
306
  class CustomFieldManager(BaseManager.from_queryset(RestrictedQuerySet)):
299
307
  use_in_migrations = True
300
308
 
301
- @lru_cache(maxsize=128)
302
309
  def get_for_model(self, model, exclude_filter_disabled=False):
303
310
  """
304
311
  Return all CustomFields assigned to the given model.
@@ -307,11 +314,20 @@ class CustomFieldManager(BaseManager.from_queryset(RestrictedQuerySet)):
307
314
  model: The django model to which custom fields are registered
308
315
  exclude_filter_disabled: Exclude any custom fields which have filter logic disabled
309
316
  """
310
- content_type = ContentType.objects.get_for_model(model._meta.concrete_model)
311
- qs = self.get_queryset().filter(content_types=content_type)
312
- if exclude_filter_disabled:
313
- qs = qs.exclude(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED)
314
- return qs
317
+ concrete_model = model._meta.concrete_model
318
+ cache_key = (
319
+ f"{self.get_for_model.cache_key_prefix}.{concrete_model._meta.label_lower}.{exclude_filter_disabled}"
320
+ )
321
+ queryset = cache.get(cache_key)
322
+ if queryset is None:
323
+ content_type = ContentType.objects.get_for_model(concrete_model)
324
+ queryset = self.get_queryset().filter(content_types=content_type)
325
+ if exclude_filter_disabled:
326
+ queryset = queryset.exclude(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED)
327
+ cache.set(cache_key, queryset)
328
+ return queryset
329
+
330
+ get_for_model.cache_key_prefix = "nautobot.extras.customfield.get_for_model"
315
331
 
316
332
 
317
333
  @extras_features("webhooks")
@@ -375,13 +391,13 @@ class CustomField(BaseModel, ChangeLoggedModel, NotesMixin):
375
391
  blank=True,
376
392
  null=True,
377
393
  verbose_name="Minimum value",
378
- help_text="Minimum allowed value (for numeric fields).",
394
+ help_text="Minimum allowed value (for numeric fields) or length (for text fields).",
379
395
  )
380
396
  validation_maximum = models.BigIntegerField(
381
397
  blank=True,
382
398
  null=True,
383
399
  verbose_name="Maximum value",
384
- help_text="Maximum allowed value (for numeric fields).",
400
+ help_text="Maximum allowed value (for numeric fields) or length (for text fields).",
385
401
  )
386
402
  validation_regex = models.CharField(
387
403
  blank=True,
@@ -449,16 +465,16 @@ class CustomField(BaseModel, ChangeLoggedModel, NotesMixin):
449
465
  except ValidationError as err:
450
466
  raise ValidationError({"default": f'Invalid default value "{self.default}": {err.message}'})
451
467
 
452
- # Minimum/maximum values can be set only for numeric fields
453
- if self.validation_minimum is not None and self.type != CustomFieldTypeChoices.TYPE_INTEGER:
454
- raise ValidationError({"validation_minimum": "A minimum value may be set only for numeric fields"})
455
- if self.validation_maximum is not None and self.type != CustomFieldTypeChoices.TYPE_INTEGER:
456
- raise ValidationError({"validation_maximum": "A maximum value may be set only for numeric fields"})
468
+ # Minimum/maximum values can be set only for fields that support them
469
+ if self.validation_minimum is not None and self.type not in CustomFieldTypeChoices.MIN_MAX_TYPES:
470
+ raise ValidationError({"validation_minimum": "A minimum value may not be set for fields of this type"})
471
+ if self.validation_maximum is not None and self.type not in CustomFieldTypeChoices.MIN_MAX_TYPES:
472
+ raise ValidationError({"validation_maximum": "A maximum value may not be set for fields of this type"})
457
473
 
458
474
  # Regex validation can be set only for text, url, select and multi-select fields
459
475
  if self.validation_regex and self.type not in CustomFieldTypeChoices.REGEX_TYPES:
460
476
  raise ValidationError(
461
- {"validation_regex": "Regular expression validation is supported only for text, URL and select fields"}
477
+ {"validation_regex": "Regular expression validation is not supported for fields of this type"}
462
478
  )
463
479
 
464
480
  # Choices can be set only on selection fields
@@ -525,13 +541,35 @@ class CustomField(BaseModel, ChangeLoggedModel, NotesMixin):
525
541
  widget=DatePicker(),
526
542
  )
527
543
 
528
- # Text and URL
529
- elif self.type in (CustomFieldTypeChoices.TYPE_URL, CustomFieldTypeChoices.TYPE_TEXT):
544
+ # Text-like fields
545
+ elif self.type in (
546
+ CustomFieldTypeChoices.TYPE_URL,
547
+ CustomFieldTypeChoices.TYPE_TEXT,
548
+ CustomFieldTypeChoices.TYPE_MARKDOWN,
549
+ ):
530
550
  if self.type == CustomFieldTypeChoices.TYPE_URL:
531
- field = LaxURLField(required=required, initial=initial)
551
+ field = LaxURLField(
552
+ required=required,
553
+ initial=initial,
554
+ min_length=self.validation_minimum,
555
+ max_length=self.validation_maximum,
556
+ )
532
557
  elif self.type == CustomFieldTypeChoices.TYPE_TEXT:
533
- field = forms.CharField(max_length=255, required=required, initial=initial)
534
-
558
+ field = forms.CharField(
559
+ required=required,
560
+ initial=initial,
561
+ min_length=self.validation_minimum,
562
+ max_length=self.validation_maximum,
563
+ )
564
+ elif self.type == CustomFieldTypeChoices.TYPE_MARKDOWN:
565
+ field = CommentField(
566
+ required=required,
567
+ initial=initial,
568
+ widget=SmallTextarea,
569
+ label=None,
570
+ min_length=self.validation_minimum,
571
+ max_length=self.validation_maximum,
572
+ )
535
573
  if self.validation_regex:
536
574
  field.validators = [
537
575
  RegexValidator(
@@ -540,12 +578,10 @@ class CustomField(BaseModel, ChangeLoggedModel, NotesMixin):
540
578
  )
541
579
  ]
542
580
 
543
- # Markdown
544
- elif self.type == CustomFieldTypeChoices.TYPE_MARKDOWN:
545
- field = CommentField(widget=SmallTextarea, label=None)
546
-
547
581
  # JSON
548
582
  elif self.type == CustomFieldTypeChoices.TYPE_JSON:
583
+ # Unlike the above cases, we don't apply min_length/max_length to the field,
584
+ # nor do we add a RegexValidator to the field, as these all apply after parsing and validating the JSON
549
585
  if simple_json_filter:
550
586
  field = JSONField(encoder=DjangoJSONEncoder, required=required, initial=None, widget=TextInput)
551
587
  else:
@@ -606,15 +642,33 @@ class CustomField(BaseModel, ChangeLoggedModel, NotesMixin):
606
642
  """
607
643
  if value not in [None, "", []]:
608
644
  # Validate text field
609
- if self.type in (CustomFieldTypeChoices.TYPE_TEXT, CustomFieldTypeChoices.TYPE_URL):
645
+ if self.type in (
646
+ CustomFieldTypeChoices.TYPE_TEXT,
647
+ CustomFieldTypeChoices.TYPE_URL,
648
+ CustomFieldTypeChoices.TYPE_MARKDOWN,
649
+ ):
610
650
  if not isinstance(value, str):
611
651
  raise ValidationError("Value must be a string")
612
-
652
+ if self.validation_minimum is not None and len(value) < self.validation_minimum:
653
+ raise ValidationError(f"Value must be at least {self.validation_minimum} characters in length")
654
+ if self.validation_maximum is not None and len(value) > self.validation_maximum:
655
+ raise ValidationError(f"Value must not exceed {self.validation_maximum} characters in length")
613
656
  if self.validation_regex and not re.search(self.validation_regex, value):
614
657
  raise ValidationError(f"Value must match regex '{self.validation_regex}'")
615
658
 
659
+ # Validate JSON
660
+ elif self.type == CustomFieldTypeChoices.TYPE_JSON:
661
+ if self.validation_regex or self.validation_minimum is not None or self.validation_maximum is not None:
662
+ json_value = json.dumps(value)
663
+ if self.validation_minimum is not None and len(json_value) < self.validation_minimum:
664
+ raise ValidationError(f"Value must be at least {self.validation_minimum} characters in length")
665
+ if self.validation_maximum is not None and len(json_value) > self.validation_maximum:
666
+ raise ValidationError(f"Value must not exceed {self.validation_maximum} characters in length")
667
+ if self.validation_regex and not re.search(self.validation_regex, json_value):
668
+ raise ValidationError(f"Value must match regex '{self.validation_regex}'")
669
+
616
670
  # Validate integer
617
- if self.type == CustomFieldTypeChoices.TYPE_INTEGER:
671
+ elif self.type == CustomFieldTypeChoices.TYPE_INTEGER:
618
672
  try:
619
673
  value = int(value)
620
674
  except ValueError:
@@ -625,14 +679,14 @@ class CustomField(BaseModel, ChangeLoggedModel, NotesMixin):
625
679
  raise ValidationError(f"Value must not exceed {self.validation_maximum}")
626
680
 
627
681
  # Validate boolean
628
- if self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
682
+ elif self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
629
683
  try:
630
684
  value = is_truthy(value)
631
685
  except ValueError as exc:
632
686
  raise ValidationError("Value must be true or false.") from exc
633
687
 
634
688
  # Validate date
635
- if self.type == CustomFieldTypeChoices.TYPE_DATE:
689
+ elif self.type == CustomFieldTypeChoices.TYPE_DATE:
636
690
  if not isinstance(value, date):
637
691
  try:
638
692
  datetime.strptime(value, "%Y-%m-%d")
@@ -640,13 +694,13 @@ class CustomField(BaseModel, ChangeLoggedModel, NotesMixin):
640
694
  raise ValidationError("Date values must be in the format YYYY-MM-DD.")
641
695
 
642
696
  # Validate selected choice
643
- if self.type == CustomFieldTypeChoices.TYPE_SELECT:
697
+ elif self.type == CustomFieldTypeChoices.TYPE_SELECT:
644
698
  if value not in self.custom_field_choices.values_list("value", flat=True):
645
699
  raise ValidationError(
646
700
  f"Invalid choice ({value}). Available choices are: {', '.join(self.custom_field_choices.values_list('value', flat=True))}"
647
701
  )
648
702
 
649
- if self.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
703
+ elif self.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
650
704
  if isinstance(value, str):
651
705
  value = value.split(",")
652
706
  if not set(value).issubset(self.custom_field_choices.values_list("value", flat=True)):
@@ -706,6 +760,11 @@ class CustomFieldChoice(BaseModel, ChangeLoggedModel):
706
760
  if self.custom_field.type not in (CustomFieldTypeChoices.TYPE_SELECT, CustomFieldTypeChoices.TYPE_MULTISELECT):
707
761
  raise ValidationError("Custom field choices can only be assigned to selection fields.")
708
762
 
763
+ if self.custom_field.validation_minimum is not None and len(self.value) < self.custom_field.validation_minimum:
764
+ raise ValidationError(f"Value must be at least {self.custom_field.validation_minimum} characters long.")
765
+ if self.custom_field.validation_maximum is not None and len(self.value) > self.custom_field.validation_maximum:
766
+ raise ValidationError(f"Value must not exceed {self.custom_field.validation_maximum} characters long.")
767
+
709
768
  if not re.search(self.custom_field.validation_regex, self.value):
710
769
  raise ValidationError(f"Value must match regex {self.custom_field.validation_regex} got {self.value}.")
711
770
 
@@ -325,7 +325,7 @@ class DynamicGroup(OrganizationalModel):
325
325
  @property
326
326
  def members_cache_key(self):
327
327
  """Return the cache key for this group's members."""
328
- return f"{self.__class__.__name__}.{self.id}.members_cached"
328
+ return f"nautobot.extras.dynamicgroup.{self.id}.members_cached"
329
329
 
330
330
  @property
331
331
  def members_cached(self):
@@ -1,9 +1,9 @@
1
- from functools import lru_cache
2
1
  import logging
3
2
 
4
3
  from django import forms
5
4
  from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
6
5
  from django.contrib.contenttypes.models import ContentType
6
+ from django.core.cache import cache
7
7
  from django.core.exceptions import ValidationError
8
8
  from django.core.serializers.json import DjangoJSONEncoder
9
9
  from django.db import models
@@ -329,7 +329,6 @@ class RelationshipModel(models.Model):
329
329
  class RelationshipManager(BaseManager.from_queryset(RestrictedQuerySet)):
330
330
  use_in_migrations = True
331
331
 
332
- @lru_cache(maxsize=128)
333
332
  def get_for_model(self, model, hidden=None):
334
333
  """
335
334
  Return all Relationships assigned to the given model.
@@ -345,7 +344,6 @@ class RelationshipManager(BaseManager.from_queryset(RestrictedQuerySet)):
345
344
  self.get_for_model_destination(model, hidden=hidden),
346
345
  )
347
346
 
348
- @lru_cache(maxsize=128)
349
347
  def get_for_model_source(self, model, hidden=None):
350
348
  """
351
349
  Return all Relationships assigned to the given model for the source side only.
@@ -354,15 +352,21 @@ class RelationshipManager(BaseManager.from_queryset(RestrictedQuerySet)):
354
352
  model (Model): The django model to which relationships are registered
355
353
  hidden (bool): Filter based on the value of the hidden flag, or None to not apply this filter
356
354
  """
357
- content_type = ContentType.objects.get_for_model(model._meta.concrete_model)
358
- result = (
359
- self.get_queryset().filter(source_type=content_type).select_related("source_type", "destination_type")
360
- ) # You almost always will want access to the source_type/destination_type
361
- if hidden is not None:
362
- result = result.filter(source_hidden=hidden)
363
- return result
364
-
365
- @lru_cache(maxsize=128)
355
+ concrete_model = model._meta.concrete_model
356
+ cache_key = f"{self.get_for_model_source.cache_key_prefix}.{concrete_model._meta.label_lower}.{hidden}"
357
+ queryset = cache.get(cache_key)
358
+ if queryset is None:
359
+ content_type = ContentType.objects.get_for_model(concrete_model)
360
+ queryset = (
361
+ self.get_queryset().filter(source_type=content_type).select_related("source_type", "destination_type")
362
+ ) # You almost always will want access to the source_type/destination_type
363
+ if hidden is not None:
364
+ queryset = queryset.filter(source_hidden=hidden)
365
+ cache.set(cache_key, queryset)
366
+ return queryset
367
+
368
+ get_for_model_source.cache_key_prefix = "nautobot.extras.relationship.get_for_model_source"
369
+
366
370
  def get_for_model_destination(self, model, hidden=None):
367
371
  """
368
372
  Return all Relationships assigned to the given model for the destination side only.
@@ -371,13 +375,22 @@ class RelationshipManager(BaseManager.from_queryset(RestrictedQuerySet)):
371
375
  model (Model): The django model to which relationships are registered
372
376
  hidden (bool): Filter based on the value of the hidden flag, or None to not apply this filter
373
377
  """
374
- content_type = ContentType.objects.get_for_model(model._meta.concrete_model)
375
- result = (
376
- self.get_queryset().filter(destination_type=content_type).select_related("source_type", "destination_type")
377
- ) # You almost always will want access to the source_type/destination_type
378
- if hidden is not None:
379
- result = result.filter(destination_hidden=hidden)
380
- return result
378
+ concrete_model = model._meta.concrete_model
379
+ cache_key = f"{self.get_for_model_destination.cache_key_prefix}.{concrete_model._meta.label_lower}.{hidden}"
380
+ queryset = cache.get(cache_key)
381
+ if queryset is None:
382
+ content_type = ContentType.objects.get_for_model(concrete_model)
383
+ queryset = (
384
+ self.get_queryset()
385
+ .filter(destination_type=content_type)
386
+ .select_related("source_type", "destination_type")
387
+ ) # You almost always will want access to the source_type/destination_type
388
+ if hidden is not None:
389
+ queryset = queryset.filter(destination_hidden=hidden)
390
+ cache.set(cache_key, queryset)
391
+ return queryset
392
+
393
+ get_for_model_destination.cache_key_prefix = "nautobot.extras.relationship.get_for_model_destination"
381
394
 
382
395
  def get_required_for_model(self, model):
383
396
  """
@@ -66,7 +66,7 @@ menu_items = (
66
66
  name="Roles",
67
67
  weight=100,
68
68
  permissions=[
69
- "extras.view_status",
69
+ "extras.view_role",
70
70
  ],
71
71
  buttons=(
72
72
  NavMenuAddButton(
@@ -120,7 +120,7 @@ menu_items = (
120
120
  ),
121
121
  NavMenuItem(
122
122
  link="extras:secretsgroup_list",
123
- name="Secret Groups",
123
+ name="Secrets Groups",
124
124
  weight=200,
125
125
  permissions=["extras.view_secretsgroup"],
126
126
  buttons=(
@@ -154,6 +154,7 @@ menu_items = (
154
154
  weight=200,
155
155
  permissions=[
156
156
  "extras.view_job",
157
+ "extras.view_scheduledjob",
157
158
  ],
158
159
  buttons=(),
159
160
  ),
@@ -331,6 +331,14 @@ class TemplateExtension:
331
331
  """
332
332
  raise NotImplementedError
333
333
 
334
+ def list_buttons(self):
335
+ """
336
+ Buttons that will be rendered and added to the existing list of buttons on the list page view. Content
337
+ should be returned as an HTML string. Note that content does not need to be marked as safe because this is
338
+ automatically handled.
339
+ """
340
+ raise NotImplementedError
341
+
334
342
  def detail_tabs(self):
335
343
  """
336
344
  Tabs that will be rendered and added to the existing list of tabs on the detail page view.
@@ -2,7 +2,6 @@ from collections import OrderedDict
2
2
 
3
3
  from django.apps import apps
4
4
  from django.conf import settings
5
- from django.contrib.auth.mixins import LoginRequiredMixin
6
5
  from django.http import Http404
7
6
  from django.shortcuts import render
8
7
  from django.urls.exceptions import NoReverseMatch
@@ -14,8 +13,9 @@ from rest_framework.response import Response
14
13
  from rest_framework.reverse import reverse
15
14
  from rest_framework.views import APIView
16
15
 
17
- from nautobot.core.api.views import NautobotAPIVersionMixin
16
+ from nautobot.core.api.views import AuthenticatedAPIRootView, NautobotAPIVersionMixin
18
17
  from nautobot.core.forms import TableConfigForm
18
+ from nautobot.core.views.generic import GenericView
19
19
  from nautobot.core.views.mixins import AdminRequiredMixin
20
20
  from nautobot.core.views.paginator import EnhancedPaginator, get_paginate_count
21
21
  from nautobot.extras.plugins.tables import InstalledPluginsTable
@@ -67,7 +67,7 @@ class InstalledPluginsView(AdminRequiredMixin, View):
67
67
  )
68
68
 
69
69
 
70
- class InstalledPluginDetailView(LoginRequiredMixin, View):
70
+ class InstalledPluginDetailView(GenericView):
71
71
  """
72
72
  View for showing details of an installed plugin.
73
73
  """
@@ -92,7 +92,6 @@ class InstalledPluginsAPIView(NautobotAPIVersionMixin, APIView):
92
92
  """
93
93
 
94
94
  permission_classes = [permissions.IsAdminUser]
95
- _ignore_model_permissions = True
96
95
 
97
96
  def get_view_name(self):
98
97
  return "Installed Plugins"
@@ -128,11 +127,9 @@ class InstalledPluginsAPIView(NautobotAPIVersionMixin, APIView):
128
127
  return Response([self._get_plugin_data(apps.get_app_config(plugin)) for plugin in settings.PLUGINS])
129
128
 
130
129
 
131
- class PluginsAPIRootView(NautobotAPIVersionMixin, APIView):
132
- _ignore_model_permissions = True
133
-
134
- def get_view_name(self):
135
- return "Plugins"
130
+ class PluginsAPIRootView(AuthenticatedAPIRootView):
131
+ name = "Apps"
132
+ description = "API extension point for installed Nautobot Apps"
136
133
 
137
134
  @staticmethod
138
135
  def _get_plugin_entry(plugin, app_config, request, format_):
@@ -210,7 +210,7 @@ class DynamicGroupQuerySet(RestrictedQuerySet):
210
210
  Return the cache key for the queryset of `DynamicGroup` objects that are eligible to potentially contain the
211
211
  given object.
212
212
  """
213
- return f"{obj._meta.label_lower}._get_eligible_dynamic_groups"
213
+ return f"nautobot.{obj._meta.label_lower}._get_eligible_dynamic_groups"
214
214
 
215
215
  def _get_eligible_dynamic_groups(self, obj, use_cache=False):
216
216
  """
@@ -1,3 +1,4 @@
1
+ import contextlib
1
2
  import contextvars
2
3
  from datetime import timedelta
3
4
  import logging
@@ -18,6 +19,7 @@ from django.db.models.signals import m2m_changed, post_delete, post_save, pre_de
18
19
  from django.dispatch import receiver
19
20
  from django.utils import timezone
20
21
  from django_prometheus.models import model_deletes, model_inserts, model_updates
22
+ import redis.exceptions
21
23
 
22
24
  from nautobot.core.celery import app, import_jobs_as_celery_tasks
23
25
  from nautobot.core.utils.config import get_settings_or_config
@@ -65,8 +67,8 @@ def _get_user_if_authenticated(user, instance):
65
67
  @receiver(post_save)
66
68
  @receiver(m2m_changed)
67
69
  @receiver(post_delete)
68
- def invalidate_lru_cache(sender, **kwargs):
69
- """Invalidate the LRU cache for ComputedFields, CustomFields and Relationships."""
70
+ def invalidate_models_cache(sender, **kwargs):
71
+ """Invalidate the related-models cache for ComputedFields, CustomFields and Relationships."""
70
72
  if sender is CustomField.content_types.through:
71
73
  manager = CustomField.objects
72
74
  elif sender in (ComputedField, CustomField, Relationship):
@@ -80,9 +82,13 @@ def invalidate_lru_cache(sender, **kwargs):
80
82
  "get_for_model_destination",
81
83
  )
82
84
 
83
- for method in cached_methods:
84
- if hasattr(manager, method):
85
- getattr(manager, method).cache_clear()
85
+ for method_name in cached_methods:
86
+ if hasattr(manager, method_name):
87
+ method = getattr(manager, method_name)
88
+ if hasattr(method, "cache_key_prefix"):
89
+ with contextlib.suppress(redis.exceptions.ConnectionError):
90
+ # TODO: *maybe* target more narrowly, e.g. only clear the cache for specific related content-types?
91
+ cache.delete_pattern(f"{method.cache_key_prefix}.*")
86
92
 
87
93
 
88
94
  @receiver(post_save)
@@ -343,7 +349,7 @@ def dynamic_group_eligible_groups_changed(sender, instance, **kwargs):
343
349
  return
344
350
 
345
351
  content_type = instance.content_type
346
- cache_key = f"{content_type.app_label}.{content_type.model}._get_eligible_dynamic_groups"
352
+ cache_key = f"nautobot.{content_type.app_label}.{content_type.model}._get_eligible_dynamic_groups"
347
353
  cache.set(
348
354
  cache_key,
349
355
  DynamicGroup.objects.filter(content_type_id=instance.content_type_id),