nautobot 3.0.0a2__py3-none-any.whl → 3.0.0a3__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 (420) hide show
  1. nautobot/apps/choices.py +0 -2
  2. nautobot/apps/filters.py +7 -9
  3. nautobot/apps/models.py +2 -2
  4. nautobot/apps/ui.py +9 -1
  5. nautobot/circuits/filters.py +3 -2
  6. nautobot/circuits/navigation.py +3 -2
  7. nautobot/circuits/templates/circuits/circuit.html +1 -1
  8. nautobot/circuits/templates/circuits/circuit_create.html +3 -3
  9. nautobot/circuits/templates/circuits/circuittermination.html +1 -1
  10. nautobot/circuits/templates/circuits/circuittermination_create.html +9 -24
  11. nautobot/circuits/templates/circuits/circuittype.html +1 -1
  12. nautobot/circuits/templates/circuits/inc/circuit_termination_cable_fragment.html +6 -6
  13. nautobot/circuits/templates/circuits/inc/speed_widget.html +12 -12
  14. nautobot/circuits/templates/circuits/providernetwork.html +1 -1
  15. nautobot/circuits/tests/integration/test_circuit.py +10 -13
  16. nautobot/cloud/filters.py +1 -1
  17. nautobot/cloud/navigation.py +3 -2
  18. nautobot/core/api/schema.py +1 -1
  19. nautobot/core/api/serializers.py +6 -1
  20. nautobot/core/api/urls.py +1 -0
  21. nautobot/core/api/views.py +8 -0
  22. nautobot/core/apps/__init__.py +11 -10
  23. nautobot/core/celery/__init__.py +3 -5
  24. nautobot/core/checks.py +46 -0
  25. nautobot/core/cli/bootstrap_v3_to_v5.py +70 -1
  26. nautobot/core/cli/migrate_deprecated_templates.py +200 -0
  27. nautobot/core/constants.py +3 -0
  28. nautobot/core/context_processors.py +9 -1
  29. nautobot/core/forms/forms.py +1 -1
  30. nautobot/core/jobs/__init__.py +6 -3
  31. nautobot/core/jobs/groups.py +31 -1
  32. nautobot/core/management/commands/generate_test_data.py +28 -9
  33. nautobot/core/models/generics.py +9 -1
  34. nautobot/core/models/tree_queries.py +10 -5
  35. nautobot/core/settings.py +18 -12
  36. nautobot/core/settings.yaml +13 -7
  37. nautobot/core/signals.py +12 -1
  38. nautobot/core/tables.py +13 -6
  39. nautobot/core/templates/40x.html +1 -1
  40. nautobot/core/templates/500.html +2 -2
  41. nautobot/core/templates/admin/config/config.html +12 -12
  42. nautobot/core/templates/admin/index.html +3 -3
  43. nautobot/core/templates/buttons/export.html +1 -1
  44. nautobot/core/templates/components/button/dropdown.html +5 -3
  45. nautobot/core/templates/components/panel/body_wrapper_generic_table.html +1 -1
  46. nautobot/core/templates/components/panel/panel.html +3 -3
  47. nautobot/core/templates/components/tab/content_wrapper.html +2 -3
  48. nautobot/core/templates/components/tab/label_wrapper_distinct_view.html +1 -1
  49. nautobot/core/templates/echarts/echarts.html +1 -1
  50. nautobot/core/templates/generic/object_bulk_add_component.html +2 -1
  51. nautobot/core/templates/generic/object_bulk_create.html +4 -3
  52. nautobot/core/templates/generic/object_bulk_destroy.html +3 -3
  53. nautobot/core/templates/generic/object_bulk_remove.html +2 -2
  54. nautobot/core/templates/generic/object_bulk_update.html +5 -4
  55. nautobot/core/templates/generic/object_create.html +5 -4
  56. nautobot/core/templates/generic/object_import.html +2 -1
  57. nautobot/core/templates/generic/object_list.html +12 -4
  58. nautobot/core/templates/generic/object_notes.html +5 -3
  59. nautobot/core/templates/generic/object_retrieve.html +2 -3
  60. nautobot/core/templates/graphene/graphiql.html +7 -7
  61. nautobot/core/templates/home.html +1 -1
  62. nautobot/core/templates/import_success.html +2 -1
  63. nautobot/core/templates/inc/computed_fields/panel_data.html +1 -1
  64. nautobot/core/templates/inc/created_updated.html +7 -3
  65. nautobot/core/templates/inc/custom_fields/panel_data.html +1 -1
  66. nautobot/core/templates/inc/form_static_field.html +6 -0
  67. nautobot/core/templates/inc/header.html +1 -1
  68. nautobot/core/templates/inc/image_attachments.html +2 -1
  69. nautobot/core/templates/inc/nav_menu.html +2 -1
  70. nautobot/core/templates/inc/search_panel.html +4 -4
  71. nautobot/core/templates/login.html +4 -2
  72. nautobot/core/templates/nautobot_config.py.j2 +6 -5
  73. nautobot/core/templates/redoc_ui.html +7 -0
  74. nautobot/core/templates/search.html +1 -1
  75. nautobot/core/templates/swagger_ui.html +17 -3
  76. nautobot/core/templates/system_jobs/import_objects.html +1 -2
  77. nautobot/core/templates/utilities/confirmation_form.html +2 -2
  78. nautobot/core/templates/utilities/obj_table.html +10 -2
  79. nautobot/core/templates/utilities/render_field.html +7 -7
  80. nautobot/core/templates/utilities/render_jinja2.html +2 -2
  81. nautobot/core/templates/utilities/templatetags/filter_form_drawer.html +4 -4
  82. nautobot/core/templates/utilities/theme_preview.html +16 -3
  83. nautobot/core/templates/widgets/selectwithdisabled_option.html +3 -1
  84. nautobot/core/templatetags/helpers.py +52 -6
  85. nautobot/core/testing/api.py +68 -9
  86. nautobot/core/testing/filters.py +0 -23
  87. nautobot/core/testing/integration.py +23 -10
  88. nautobot/core/testing/mixins.py +2 -0
  89. nautobot/core/testing/views.py +4 -0
  90. nautobot/core/tests/integration/test_app_home.py +34 -30
  91. nautobot/core/tests/integration/test_app_navbar.py +3 -0
  92. nautobot/core/tests/nautobot_config_without_example_apps.py +4 -0
  93. nautobot/core/tests/runner.py +9 -1
  94. nautobot/core/tests/test_api.py +5 -3
  95. nautobot/core/tests/test_breadcrumbs.py +6 -7
  96. nautobot/core/tests/test_checks.py +28 -0
  97. nautobot/core/tests/test_cli.py +40 -0
  98. nautobot/core/tests/test_config.py +2 -1
  99. nautobot/core/tests/test_forms.py +55 -13
  100. nautobot/core/tests/test_jobs.py +75 -1
  101. nautobot/core/tests/test_nautobot_server.py +2 -0
  102. nautobot/core/tests/test_navigations.py +76 -1
  103. nautobot/core/tests/test_patch_social_django.py +42 -0
  104. nautobot/core/tests/test_tables.py +3 -1
  105. nautobot/core/tests/test_templatetags_helpers.py +53 -13
  106. nautobot/core/tests/test_templatetags_ui_framework.py +4 -4
  107. nautobot/core/tests/test_tree_queries.py +14 -1
  108. nautobot/core/tests/test_ui.py +1 -1
  109. nautobot/core/tests/test_utils.py +31 -4
  110. nautobot/core/tests/test_views.py +159 -31
  111. nautobot/core/ui/breadcrumbs.py +2 -12
  112. nautobot/core/ui/choices.py +142 -10
  113. nautobot/core/ui/constants.py +76 -12
  114. nautobot/core/ui/object_detail.py +92 -12
  115. nautobot/core/urls.py +12 -1
  116. nautobot/core/utils/cache.py +2 -1
  117. nautobot/core/utils/filtering.py +17 -17
  118. nautobot/core/utils/lookup.py +3 -8
  119. nautobot/core/utils/module_loading.py +21 -0
  120. nautobot/core/utils/patch_social_django.py +128 -0
  121. nautobot/core/views/__init__.py +38 -1
  122. nautobot/core/views/generic.py +3 -3
  123. nautobot/core/views/mixins.py +15 -3
  124. nautobot/core/views/renderers.py +2 -0
  125. nautobot/core/views/viewsets.py +2 -1
  126. nautobot/data_validation/apps.py +1 -5
  127. nautobot/data_validation/custom_validators.py +4 -4
  128. nautobot/data_validation/filters.py +1 -1
  129. nautobot/data_validation/forms.py +40 -0
  130. nautobot/data_validation/migrations/0001_initial.py +0 -7
  131. nautobot/data_validation/migrations/0002_data_migration_from_app.py +0 -12
  132. nautobot/data_validation/models.py +16 -7
  133. nautobot/data_validation/navigation.py +8 -1
  134. nautobot/data_validation/tables.py +12 -5
  135. nautobot/data_validation/templates/data_validation/datacompliance_tab.html +1 -0
  136. nautobot/data_validation/templates/data_validation/device_constraints.html +61 -0
  137. nautobot/data_validation/tests/__init__.py +2 -2
  138. nautobot/data_validation/tests/migrations/test_migrations.py +83 -3
  139. nautobot/data_validation/tests/test_data_compliance_rules.py +12 -7
  140. nautobot/data_validation/tests/test_filters.py +8 -6
  141. nautobot/data_validation/tests/test_models.py +15 -0
  142. nautobot/data_validation/tests/test_views.py +190 -32
  143. nautobot/data_validation/urls.py +2 -5
  144. nautobot/data_validation/views.py +73 -40
  145. nautobot/dcim/api/serializers.py +0 -13
  146. nautobot/dcim/apps.py +4 -0
  147. nautobot/dcim/choices.py +16 -0
  148. nautobot/dcim/custom_validators.py +84 -0
  149. nautobot/dcim/filter_mixins.py +353 -4
  150. nautobot/dcim/{filters/__init__.py → filters.py} +2 -35
  151. nautobot/dcim/forms.py +1 -1
  152. nautobot/dcim/migrations/0078_remove_device_location_tenant_name_uniqueness.py +16 -0
  153. nautobot/dcim/migrations/0079_device_name_data_migration.py +59 -0
  154. nautobot/dcim/models/device_components.py +81 -68
  155. nautobot/dcim/models/devices.py +13 -16
  156. nautobot/dcim/navigation.py +7 -6
  157. nautobot/dcim/tables/devices.py +3 -0
  158. nautobot/dcim/tables/template_code.py +14 -14
  159. nautobot/dcim/templates/dcim/cable.html +2 -61
  160. nautobot/dcim/templates/dcim/cable_connect.html +28 -112
  161. nautobot/dcim/templates/dcim/cable_edit.html +2 -5
  162. nautobot/dcim/templates/dcim/cable_retrieve.html +61 -0
  163. nautobot/dcim/templates/dcim/cable_trace.html +1 -3
  164. nautobot/dcim/templates/dcim/cable_update.html +5 -0
  165. nautobot/dcim/templates/dcim/consoleport.html +6 -5
  166. nautobot/dcim/templates/dcim/consoleserverport.html +6 -5
  167. nautobot/dcim/templates/dcim/device/config.html +2 -2
  168. nautobot/dcim/templates/dcim/device/consoleports.html +1 -1
  169. nautobot/dcim/templates/dcim/device/consoleserverports.html +1 -1
  170. nautobot/dcim/templates/dcim/device/devicebays.html +1 -1
  171. nautobot/dcim/templates/dcim/device/frontports.html +1 -1
  172. nautobot/dcim/templates/dcim/device/interfaces.html +1 -1
  173. nautobot/dcim/templates/dcim/device/inventory.html +1 -1
  174. nautobot/dcim/templates/dcim/device/lldp_neighbors.html +1 -1
  175. nautobot/dcim/templates/dcim/device/modulebays.html +1 -1
  176. nautobot/dcim/templates/dcim/device/poweroutlets.html +1 -1
  177. nautobot/dcim/templates/dcim/device/powerports.html +1 -1
  178. nautobot/dcim/templates/dcim/device/rearports.html +1 -1
  179. nautobot/dcim/templates/dcim/device/status.html +8 -8
  180. nautobot/dcim/templates/dcim/device/wireless.html +1 -1
  181. nautobot/dcim/templates/dcim/device.html +1 -1
  182. nautobot/dcim/templates/dcim/device_component_add.html +2 -2
  183. nautobot/dcim/templates/dcim/device_create.html +5 -3
  184. nautobot/dcim/templates/dcim/device_interface_delete.html +1 -1
  185. nautobot/dcim/templates/dcim/device_list.html +73 -10
  186. nautobot/dcim/templates/dcim/devicebay_populate.html +2 -2
  187. nautobot/dcim/templates/dcim/devicetype.html +1 -1
  188. nautobot/dcim/templates/dcim/devicetype_component_add.html +2 -2
  189. nautobot/dcim/templates/dcim/footer_convert_to_contact_or_team_record.html +14 -0
  190. nautobot/dcim/templates/dcim/frontport.html +9 -8
  191. nautobot/dcim/templates/dcim/inc/edit_form_softwareversion_js.html +2 -2
  192. nautobot/dcim/templates/dcim/interface.html +26 -6
  193. nautobot/dcim/templates/dcim/interface_bulk_delete.html +1 -1
  194. nautobot/dcim/templates/dcim/inventoryitem_add.html +3 -1
  195. nautobot/dcim/templates/dcim/inventoryitem_bulk_delete.html +1 -1
  196. nautobot/dcim/templates/dcim/inventoryitem_edit.html +3 -1
  197. nautobot/dcim/templates/dcim/location_retrieve.html +1 -242
  198. nautobot/dcim/templates/dcim/module/base.html +49 -9
  199. nautobot/dcim/templates/dcim/module_list.html +57 -8
  200. nautobot/dcim/templates/dcim/modulefamily_retrieve.html +1 -1
  201. nautobot/dcim/templates/dcim/moduletype_retrieve.html +49 -9
  202. nautobot/dcim/templates/dcim/platform_create.html +1 -1
  203. nautobot/dcim/templates/dcim/powerfeed.html +1 -1
  204. nautobot/dcim/templates/dcim/powerpanel.html +1 -1
  205. nautobot/dcim/templates/dcim/powerport.html +5 -4
  206. nautobot/dcim/templates/dcim/rack_elevation_list.html +16 -4
  207. nautobot/dcim/templates/dcim/rack_retrieve.html +33 -15
  208. nautobot/dcim/templates/dcim/rearport.html +7 -6
  209. nautobot/dcim/templates/dcim/virtualchassis.html +1 -1
  210. nautobot/dcim/templates/dcim/virtualchassis_add_member.html +16 -14
  211. nautobot/dcim/templates/dcim/virtualchassis_update.html +14 -6
  212. nautobot/dcim/tests/integration/test_controller.py +1 -0
  213. nautobot/dcim/tests/test_api.py +8 -0
  214. nautobot/dcim/tests/test_custom_validators.py +229 -0
  215. nautobot/dcim/tests/test_filters.py +12 -6
  216. nautobot/dcim/tests/test_models.py +63 -4
  217. nautobot/dcim/tests/test_views.py +63 -22
  218. nautobot/dcim/urls.py +64 -21
  219. nautobot/dcim/utils.py +3 -3
  220. nautobot/dcim/views.py +547 -273
  221. nautobot/extras/api/views.py +9 -1
  222. nautobot/extras/choices.py +2 -13
  223. nautobot/extras/{filters/mixins.py → filter_mixins.py} +1 -1
  224. nautobot/extras/{filters/customfields.py → filter_mixins_customfields.py} +42 -6
  225. nautobot/extras/{filters/__init__.py → filters.py} +14 -46
  226. nautobot/extras/forms/forms.py +5 -13
  227. nautobot/extras/forms/mixins.py +0 -41
  228. nautobot/extras/management/__init__.py +9 -0
  229. nautobot/extras/migrations/0127_approval_workflow_models.py +6 -6
  230. nautobot/extras/migrations/0129_jobresult_debug_log_count_jobresult_error_log_count_and_more.py +37 -0
  231. nautobot/extras/migrations/0130_jobresult_generate_log_entry_counts.py +42 -0
  232. nautobot/extras/models/__init__.py +1 -2
  233. nautobot/extras/models/approvals.py +22 -13
  234. nautobot/extras/models/contacts.py +2 -0
  235. nautobot/extras/models/groups.py +44 -5
  236. nautobot/extras/models/jobs.py +59 -1
  237. nautobot/extras/models/mixins.py +28 -0
  238. nautobot/extras/models/models.py +13 -0
  239. nautobot/extras/models/secrets.py +1 -0
  240. nautobot/extras/models/statuses.py +0 -15
  241. nautobot/extras/navigation.py +13 -9
  242. nautobot/extras/plugins/__init__.py +33 -55
  243. nautobot/extras/plugins/tables.py +3 -3
  244. nautobot/extras/plugins/urls.py +2 -21
  245. nautobot/extras/plugins/utils.py +1 -33
  246. nautobot/extras/plugins/views.py +0 -4
  247. nautobot/extras/signals.py +20 -19
  248. nautobot/extras/tables.py +52 -68
  249. nautobot/extras/templates/extras/approval_dashboard.html +7 -5
  250. nautobot/extras/templates/extras/approvalworkflowdefinition_update.html +4 -2
  251. nautobot/extras/templates/extras/approvalworkflowstage_retrieve.html +20 -12
  252. nautobot/extras/templates/extras/computedfield.html +1 -1
  253. nautobot/extras/templates/extras/configcontext.html +1 -1
  254. nautobot/extras/templates/extras/configcontextschema_validation.html +2 -2
  255. nautobot/extras/templates/extras/customfield.html +1 -1
  256. nautobot/extras/templates/extras/dynamicgroup_retrieve.html +11 -5
  257. nautobot/extras/templates/extras/dynamicgroup_update.html +1 -1
  258. nautobot/extras/templates/extras/gitrepository_result.html +0 -2
  259. nautobot/extras/templates/extras/graphqlquery_retrieve.html +1 -96
  260. nautobot/extras/templates/extras/inc/approval_buttons_column.html +20 -6
  261. nautobot/extras/templates/extras/inc/bulk_edit_overridable_field.html +8 -7
  262. nautobot/extras/templates/extras/inc/configcontext_format.html +10 -3
  263. nautobot/extras/templates/extras/inc/graphqlquery_execute.html +71 -0
  264. nautobot/extras/templates/extras/inc/job_tiles.html +15 -3
  265. nautobot/extras/templates/extras/inc/json_format.html +10 -3
  266. nautobot/extras/templates/extras/inc/overridable_field.html +13 -12
  267. nautobot/extras/templates/extras/job.html +29 -12
  268. nautobot/extras/templates/extras/job_bulk_edit.html +18 -0
  269. nautobot/extras/templates/extras/job_edit.html +52 -46
  270. nautobot/extras/templates/extras/job_list.html +29 -25
  271. nautobot/extras/templates/extras/marketplace.html +5 -9
  272. nautobot/extras/templates/extras/object_configcontext.html +1 -1
  273. nautobot/extras/templates/extras/object_dynamicgroups.html +2 -2
  274. nautobot/extras/templates/extras/objectchange_retrieve.html +19 -37
  275. nautobot/extras/templates/extras/plugin_detail.html +26 -21
  276. nautobot/extras/templates/extras/plugins_list.html +16 -26
  277. nautobot/extras/templates/extras/role_retrieve.html +64 -0
  278. nautobot/extras/templates/extras/scheduledjob.html +4 -2
  279. nautobot/extras/templates/extras/secretsgroup.html +1 -1
  280. nautobot/extras/templates/extras/tag.html +1 -1
  281. nautobot/extras/templatetags/custom_links.py +12 -12
  282. nautobot/extras/templatetags/job_buttons.py +14 -12
  283. nautobot/extras/test_jobs/invalid_import.py +9 -0
  284. nautobot/extras/test_jobs/log_counts_by_level.py +23 -0
  285. nautobot/extras/test_jobs/missing_import.py +11 -0
  286. nautobot/extras/tests/integration/test_configcontextschema.py +27 -26
  287. nautobot/extras/tests/integration/test_customfields.py +8 -7
  288. nautobot/extras/tests/integration/test_dynamicgroups.py +5 -1
  289. nautobot/extras/tests/integration/test_plugin_banner.py +3 -0
  290. nautobot/extras/tests/integration/test_plugins.py +18 -6
  291. nautobot/extras/tests/test_api.py +27 -18
  292. nautobot/extras/tests/test_approvals.py +38 -38
  293. nautobot/extras/tests/test_changelog.py +35 -3
  294. nautobot/extras/tests/test_customfields.py +22 -13
  295. nautobot/extras/tests/test_customfields_filters.py +479 -0
  296. nautobot/extras/tests/test_dynamicgroups.py +39 -1
  297. nautobot/extras/tests/test_filters.py +21 -19
  298. nautobot/extras/tests/test_forms.py +18 -21
  299. nautobot/extras/tests/test_jobs.py +25 -4
  300. nautobot/extras/tests/test_migrations.py +1 -0
  301. nautobot/extras/tests/test_models.py +13 -31
  302. nautobot/extras/tests/test_plugins.py +36 -10
  303. nautobot/extras/tests/test_views.py +31 -30
  304. nautobot/extras/views.py +81 -19
  305. nautobot/ipam/factory.py +7 -0
  306. nautobot/ipam/filter_mixins.py +38 -0
  307. nautobot/ipam/filters.py +27 -38
  308. nautobot/ipam/formfields.py +1 -1
  309. nautobot/ipam/forms.py +6 -3
  310. nautobot/ipam/migrations/0030_ipam__namespaces.py +13 -0
  311. nautobot/ipam/migrations/0031_ipam___data_migrations.py +4 -1
  312. nautobot/ipam/migrations/0054_namespace_tenant.py +25 -0
  313. nautobot/ipam/models.py +29 -2
  314. nautobot/ipam/navigation.py +3 -2
  315. nautobot/ipam/signals.py +71 -0
  316. nautobot/ipam/tables.py +13 -6
  317. nautobot/ipam/templates/ipam/inc/toggle_available.html +10 -10
  318. nautobot/ipam/templates/ipam/inc/vlangroup_header.html +1 -0
  319. nautobot/ipam/templates/ipam/ipaddress.html +14 -0
  320. nautobot/ipam/templates/ipam/ipaddress_merge.html +3 -3
  321. nautobot/ipam/templates/ipam/ipaddresstointerface_retrieve.html +1 -0
  322. nautobot/ipam/templates/ipam/namespace_update.html +15 -0
  323. nautobot/ipam/templates/ipam/prefix_delete.html +1 -1
  324. nautobot/ipam/templates/ipam/prefix_list.html +14 -13
  325. nautobot/ipam/templates/ipam/service.html +1 -1
  326. nautobot/ipam/templates/ipam/vlan.html +1 -1
  327. nautobot/ipam/templates/ipam/vlan_interfaces.html +1 -1
  328. nautobot/ipam/templates/ipam/vlan_vminterfaces.html +1 -1
  329. nautobot/ipam/tests/migration/test_migrations.py +89 -0
  330. nautobot/ipam/tests/test_api.py +13 -6
  331. nautobot/ipam/tests/test_filters.py +10 -0
  332. nautobot/ipam/tests/test_forms.py +1 -1
  333. nautobot/ipam/tests/test_models.py +43 -1
  334. nautobot/ipam/tests/test_tables.py +1 -2
  335. nautobot/ipam/tests/test_utils.py +1 -1
  336. nautobot/ipam/tests/test_views.py +13 -14
  337. nautobot/ipam/ui.py +0 -17
  338. nautobot/ipam/utils/migrations.py +16 -2
  339. nautobot/ipam/utils/testing.py +9 -3
  340. nautobot/ipam/views.py +46 -6
  341. nautobot/project-static/dist/css/nautobot.css +1 -1
  342. nautobot/project-static/dist/css/nautobot.css.map +1 -1
  343. nautobot/project-static/dist/js/nautobot.js +1 -1
  344. nautobot/project-static/dist/js/nautobot.js.map +1 -1
  345. nautobot/project-static/js/cabletrace.js +1 -1
  346. nautobot/project-static/js/interface_filtering.js +20 -16
  347. nautobot/project-static/nautobot-icons/battery-3.svg +3 -0
  348. nautobot/project-static/nautobot-icons/cloud.svg +1 -1
  349. nautobot/project-static/nautobot-icons/control-panel.svg +1 -1
  350. nautobot/project-static/nautobot-icons/device-lifecycle.svg +1 -1
  351. nautobot/project-static/nautobot-icons/elements.svg +1 -1
  352. nautobot/project-static/nautobot-icons/extensibility.svg +3 -0
  353. nautobot/project-static/nautobot-icons/hammer.svg +1 -1
  354. nautobot/project-static/nautobot-icons/organization.svg +3 -0
  355. nautobot/project-static/nautobot-icons/secrets.svg +1 -1
  356. nautobot/project-static/nautobot-icons/security.svg +3 -0
  357. nautobot/project-static/nautobot-icons/server.svg +1 -1
  358. nautobot/project-static/nautobot-icons/star-filled.svg +1 -1
  359. nautobot/project-static/nautobot-icons/star.svg +1 -1
  360. nautobot/tenancy/api/serializers.py +1 -0
  361. nautobot/tenancy/api/views.py +2 -1
  362. nautobot/tenancy/{filters/__init__.py → filters.py} +2 -10
  363. nautobot/tenancy/navigation.py +3 -1
  364. nautobot/tenancy/tests/test_filters.py +0 -2
  365. nautobot/tenancy/views.py +2 -1
  366. nautobot/ui/src/js/collapse.js +3 -3
  367. nautobot/ui/src/js/nautobot.js +16 -0
  368. nautobot/ui/src/scss/colors.scss +1 -1
  369. nautobot/ui/src/scss/nautobot.scss +61 -28
  370. nautobot/users/templates/users/profile.html +45 -12
  371. nautobot/users/templates/users/sessionkey_delete.html +1 -1
  372. nautobot/users/tests/test_api.py +4 -0
  373. nautobot/users/views.py +4 -2
  374. nautobot/virtualization/models.py +1 -68
  375. nautobot/virtualization/navigation.py +3 -2
  376. nautobot/virtualization/templates/virtualization/virtual_machine_vminterface_delete.html +1 -1
  377. nautobot/virtualization/templates/virtualization/virtualmachine.html +1 -1
  378. nautobot/virtualization/templates/virtualization/virtualmachine_list.html +2 -2
  379. nautobot/virtualization/templates/virtualization/virtualmachine_update.html +3 -1
  380. nautobot/virtualization/tests/test_api.py +3 -0
  381. nautobot/virtualization/tests/test_models.py +44 -4
  382. nautobot/vpn/__init__.py +0 -0
  383. nautobot/vpn/api/serializers.py +113 -0
  384. nautobot/vpn/api/urls.py +19 -0
  385. nautobot/vpn/api/views.py +70 -0
  386. nautobot/vpn/apps.py +8 -0
  387. nautobot/vpn/choices.py +171 -0
  388. nautobot/vpn/factory.py +209 -0
  389. nautobot/vpn/filters.py +233 -0
  390. nautobot/vpn/forms.py +486 -0
  391. nautobot/vpn/homepage.py +19 -0
  392. nautobot/vpn/migrations/0001_initial.py +541 -0
  393. nautobot/vpn/migrations/0002_populate_defaults.py +199 -0
  394. nautobot/vpn/migrations/__init__.py +0 -0
  395. nautobot/vpn/models.py +527 -0
  396. nautobot/vpn/navigation.py +98 -0
  397. nautobot/vpn/tables.py +380 -0
  398. nautobot/vpn/templates/vpn/vpnprofile.html +2 -0
  399. nautobot/vpn/templates/vpn/vpnprofile_create.html +150 -0
  400. nautobot/vpn/tests/__init__.py +0 -0
  401. nautobot/vpn/tests/test_api.py +341 -0
  402. nautobot/vpn/tests/test_filters.py +139 -0
  403. nautobot/vpn/tests/test_forms.py +294 -0
  404. nautobot/vpn/tests/test_models.py +97 -0
  405. nautobot/vpn/tests/test_views.py +281 -0
  406. nautobot/vpn/urls.py +16 -0
  407. nautobot/vpn/views.py +437 -0
  408. nautobot/wireless/navigation.py +3 -2
  409. nautobot/wireless/tests/integration/test_radio_profile.py +1 -5
  410. nautobot/wireless/tests/test_api.py +1 -1
  411. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/METADATA +14 -14
  412. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/RECORD +417 -366
  413. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/entry_points.txt +1 -0
  414. nautobot/data_validation/template_content.py +0 -42
  415. nautobot/dcim/filters/mixins.py +0 -354
  416. nautobot/ipam/templates/ipam/inc/prefix_header_extra_content_table.html +0 -4
  417. /nautobot/tenancy/{filters/mixins.py → filter_mixins.py} +0 -0
  418. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/LICENSE.txt +0 -0
  419. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/NOTICE +0 -0
  420. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/WHEEL +0 -0
@@ -5,9 +5,10 @@ import inspect
5
5
  from logging import getLogger
6
6
 
7
7
  from django.conf import settings
8
+ from django.conf.urls import include
8
9
  from django.core.exceptions import ValidationError
9
10
  from django.template.loader import get_template
10
- from django.urls import get_resolver, URLPattern
11
+ from django.urls import clear_url_caches, get_resolver, path, URLPattern
11
12
  from packaging import version
12
13
 
13
14
  from nautobot.core.apps import (
@@ -17,10 +18,9 @@ from nautobot.core.apps import (
17
18
  register_menu_items,
18
19
  )
19
20
  from nautobot.core.signals import nautobot_database_ready
20
- from nautobot.core.utils.deprecation import class_deprecated_in_favor_of
21
+ from nautobot.core.utils.module_loading import import_string_optional
21
22
  from nautobot.extras.choices import BannerClassChoices
22
23
  from nautobot.extras.plugins.exceptions import PluginImproperlyConfigured
23
- from nautobot.extras.plugins.utils import import_object
24
24
  from nautobot.extras.registry import register_datasource_contents, registry
25
25
  from nautobot.extras.secrets import register_secrets_provider
26
26
 
@@ -105,9 +105,24 @@ class NautobotAppConfig(NautobotConfig):
105
105
  """Callback after plugin app is loaded."""
106
106
  # We don't call super().ready here because we don't need or use the on-ready behavior of a core Nautobot app
107
107
 
108
+ from nautobot.extras.plugins.urls import BASE_URL_TO_APP_LABEL, plugin_api_patterns, plugin_patterns
109
+
108
110
  # Introspect URL patterns and models to make available to the installed-plugins detail UI view.
109
- urlpatterns = import_object(f"{self.__module__}.urls.urlpatterns")
110
- api_urlpatterns = import_object(f"{self.__module__}.api.urls.urlpatterns")
111
+ urlpatterns = import_string_optional(f"{self.__module__}.urls.urlpatterns")
112
+ api_urlpatterns = import_string_optional(f"{self.__module__}.api.urls.urlpatterns")
113
+
114
+ base_url = self.base_url or self.label
115
+
116
+ if urlpatterns is not None:
117
+ plugin_patterns.append(path(f"{base_url}/", include((urlpatterns, self.label))))
118
+
119
+ if api_urlpatterns is not None:
120
+ plugin_api_patterns.append(path(f"{base_url}/", include((api_urlpatterns, f"{self.label}-api"))))
121
+
122
+ if any([urlpatterns, api_urlpatterns]):
123
+ clear_url_caches()
124
+
125
+ BASE_URL_TO_APP_LABEL[base_url] = self.label
111
126
 
112
127
  self.features = {
113
128
  "api_urlpatterns": sorted(
@@ -123,36 +138,31 @@ class NautobotAppConfig(NautobotConfig):
123
138
  }
124
139
 
125
140
  # Register banner function (if defined)
126
- banner_function = import_object(f"{self.__module__}.{self.banner_function}")
127
- if banner_function is not None:
141
+ if banner_function := import_string_optional(f"{self.__module__}.{self.banner_function}"):
128
142
  register_banner_function(banner_function)
129
143
  self.features["banner"] = True
130
144
 
131
145
  # Register model validators (if defined)
132
- validators = import_object(f"{self.__module__}.{self.custom_validators}")
133
- if validators is not None:
146
+ if validators := import_string_optional(f"{self.__module__}.{self.custom_validators}"):
134
147
  register_custom_validators(validators)
135
148
  self.features["custom_validators"] = sorted(set(validator.model for validator in validators))
136
149
 
137
150
  # Register datasource contents (if defined)
138
- datasource_contents = import_object(f"{self.__module__}.{self.datasource_contents}")
139
- if datasource_contents is not None:
151
+ if datasource_contents := import_string_optional(f"{self.__module__}.{self.datasource_contents}"):
140
152
  register_datasource_contents(datasource_contents)
141
153
  self.features["datasource_contents"] = datasource_contents
142
154
 
143
155
  # Register GraphQL types (if defined)
144
- graphql_types = import_object(f"{self.__module__}.{self.graphql_types}")
145
- if graphql_types is not None:
156
+ if graphql_types := import_string_optional(f"{self.__module__}.{self.graphql_types}"):
146
157
  register_graphql_types(graphql_types)
147
158
 
148
159
  # Import jobs (if present)
149
160
  # Note that we do *not* auto-call `register_jobs()` - the App is responsible for doing so when imported.
150
- jobs = import_object(f"{self.__module__}.{self.jobs}")
151
- if jobs is not None:
161
+ if jobs := import_string_optional(f"{self.__module__}.{self.jobs}"):
152
162
  self.features["jobs"] = jobs
153
163
 
154
164
  # Import metrics (if present)
155
- metrics = import_object(f"{self.__module__}.{self.metrics}")
165
+ metrics = import_string_optional(f"{self.__module__}.{self.metrics}")
156
166
  if metrics is not None and self.name not in settings.METRICS_DISABLED_APPS:
157
167
  register_metrics(metrics)
158
168
  self.features["metrics"] = [] # Initialize as empty, to be filled by the signal handler
@@ -161,19 +171,16 @@ class NautobotAppConfig(NautobotConfig):
161
171
  nautobot_database_ready.connect(signal_callback, sender=self)
162
172
 
163
173
  # Register plugin navigation menu items (if defined)
164
- menu_items = import_object(f"{self.__module__}.{self.menu_items}")
165
- if menu_items is not None:
174
+ if menu_items := import_string_optional(f"{self.__module__}.{self.menu_items}"):
166
175
  register_plugin_menu_items(self.verbose_name, menu_items)
167
176
  self.features["nav_menu"] = menu_items
168
177
 
169
- homepage_layout = import_object(f"{self.__module__}.{self.homepage_layout}")
170
- if homepage_layout is not None:
178
+ if homepage_layout := import_string_optional(f"{self.__module__}.{self.homepage_layout}"):
171
179
  register_homepage_panels(self.path, self.label, homepage_layout)
172
180
  self.features["home_page"] = homepage_layout
173
181
 
174
182
  # Register template content (if defined)
175
- template_extensions = import_object(f"{self.__module__}.{self.template_extensions}")
176
- if template_extensions is not None:
183
+ if template_extensions := import_string_optional(f"{self.__module__}.{self.template_extensions}"):
177
184
  register_template_extensions(template_extensions)
178
185
  self.features["template_extensions"] = sorted(set(extension.model for extension in template_extensions))
179
186
 
@@ -185,15 +192,13 @@ class NautobotAppConfig(NautobotConfig):
185
192
  pass
186
193
 
187
194
  # Register secrets providers (if any)
188
- secrets_providers = import_object(f"{self.__module__}.{self.secrets_providers}")
189
- if secrets_providers is not None:
195
+ if secrets_providers := import_string_optional(f"{self.__module__}.{self.secrets_providers}"):
190
196
  for secrets_provider in secrets_providers:
191
197
  register_secrets_provider(secrets_provider)
192
198
  self.features["secrets_providers"] = secrets_providers
193
199
 
194
200
  # Register custom filters (if any)
195
- filter_extensions = import_object(f"{self.__module__}.{self.filter_extensions}")
196
- if filter_extensions is not None:
201
+ if filter_extensions := import_string_optional(f"{self.__module__}.{self.filter_extensions}"):
197
202
  register_filter_extensions(filter_extensions, self.name)
198
203
  self.features["filter_extensions"] = {"filterset_fields": [], "filterform_fields": []}
199
204
  for filter_extension in filter_extensions:
@@ -207,8 +212,7 @@ class NautobotAppConfig(NautobotConfig):
207
212
  )
208
213
 
209
214
  # Register override view (if any)
210
- override_views = import_object(f"{self.__module__}.{self.override_views}")
211
- if override_views is not None:
215
+ if override_views := import_string_optional(f"{self.__module__}.{self.override_views}"):
212
216
  for qualified_view_name, view in override_views.items():
213
217
  view_class_name = view.view_class.__name__ if hasattr(view, "view_class") else view.cls.__name__
214
218
  self.features.setdefault("overridden_views", []).append(
@@ -272,17 +276,11 @@ class NautobotAppConfig(NautobotConfig):
272
276
 
273
277
  def _register_table_extensions(self):
274
278
  """Register tables extensions (if any)."""
275
- table_extensions = import_object(f"{self.__module__}.{self.table_extensions}")
276
- if table_extensions is not None:
279
+ if table_extensions := import_string_optional(f"{self.__module__}.{self.table_extensions}"):
277
280
  register_table_extensions(table_extensions, self.name)
278
281
  self.features["table_extensions"] = get_table_extension_features(table_extensions)
279
282
 
280
283
 
281
- @class_deprecated_in_favor_of(NautobotAppConfig)
282
- class PluginConfig(NautobotAppConfig):
283
- pass
284
-
285
-
286
284
  #
287
285
  # Template content injection
288
286
  #
@@ -413,11 +411,6 @@ class TemplateExtension:
413
411
  raise NotImplementedError
414
412
 
415
413
 
416
- @class_deprecated_in_favor_of(TemplateExtension)
417
- class PluginTemplateExtension(TemplateExtension):
418
- pass
419
-
420
-
421
414
  def register_template_extensions(class_list):
422
415
  """
423
416
  Register a list of TemplateExtension classes
@@ -444,11 +437,6 @@ class Banner:
444
437
  self.banner_class = banner_class
445
438
 
446
439
 
447
- @class_deprecated_in_favor_of(Banner)
448
- class PluginBanner(Banner):
449
- pass
450
-
451
-
452
440
  def register_banner_function(function):
453
441
  """
454
442
  Register a function that may return a Banner object.
@@ -494,11 +482,6 @@ class FilterExtension:
494
482
  filterform_fields = {}
495
483
 
496
484
 
497
- @class_deprecated_in_favor_of(FilterExtension)
498
- class PluginFilterExtension(FilterExtension):
499
- pass
500
-
501
-
502
485
  def register_filter_extensions(filter_extensions, plugin_name):
503
486
  """
504
487
  Register a list of FilterExtension classes
@@ -789,11 +772,6 @@ class CustomValidator:
789
772
  raise NotImplementedError
790
773
 
791
774
 
792
- @class_deprecated_in_favor_of(CustomValidator)
793
- class PluginCustomValidator(CustomValidator):
794
- pass
795
-
796
-
797
775
  def register_custom_validators(class_list):
798
776
  """
799
777
  Register a list of CustomValidator classes
@@ -20,21 +20,21 @@ class InstalledAppsTable(tables.Table):
20
20
  {% if record.home_url %}
21
21
  <a href="{% url record.home_url %}" class="btn btn-primary btn-xs" title="Home">
22
22
  {% else %}
23
- <a href="" class="btn btn-primary btn-xs disabled" title="No home link provided">
23
+ <a class="btn btn-primary btn-xs disabled" aria-disabled="true" title="No home link provided">
24
24
  {% endif %}
25
25
  <i class="mdi mdi-home"></i>
26
26
  </a>
27
27
  {% if record.config_url %}
28
28
  <a href="{% url record.config_url %}" class="btn btn-warning btn-xs" title="Configure">
29
29
  {% else %}
30
- <a href="" class="btn btn-warning btn-xs disabled" title="No configuration link provided">
30
+ <a class="btn btn-warning btn-xs disabled" aria-disabled="true" title="No configuration link provided">
31
31
  {% endif %}
32
32
  <i class="mdi mdi-cog"></i>
33
33
  </a>
34
34
  {% if record.docs_url %}
35
35
  <a href="{% url record.docs_url %}" class="btn btn-info btn-xs" title="Docs">
36
36
  {% else %}
37
- <a href="" class="btn btn-info btn-xs disabled" title="No docs provided">
37
+ <a class="btn btn-info btn-xs disabled" aria-disabled="true" title="No docs provided">
38
38
  {% endif %}
39
39
  <i class="mdi mdi-book-open-page-variant"></i>
40
40
  </a>
@@ -1,12 +1,9 @@
1
- from django.apps import apps
2
- from django.conf import settings
3
- from django.conf.urls import include
4
1
  from django.urls import path
5
2
 
6
- from nautobot.extras.plugins.utils import import_object
7
-
8
3
  from . import views
9
4
 
5
+ BASE_URL_TO_APP_LABEL = {}
6
+
10
7
  # Initialize URL base, API, and admin URL patterns for plugins
11
8
  apps_patterns = [
12
9
  path("installed-apps/", views.InstalledAppsView.as_view(), name="apps_list"),
@@ -30,19 +27,3 @@ plugin_api_patterns = [
30
27
  ),
31
28
  ]
32
29
  plugin_admin_patterns = []
33
-
34
- # Register base/API URL patterns for each plugin
35
- for plugin_path in settings.PLUGINS:
36
- plugin_name = plugin_path.split(".")[-1]
37
- app = apps.get_app_config(plugin_name)
38
- base_url = app.base_url or app.label
39
-
40
- # Check if the plugin specifies any base URLs
41
- urlpatterns = import_object(f"{plugin_path}.urls.urlpatterns")
42
- if urlpatterns is not None:
43
- plugin_patterns.append(path(f"{base_url}/", include((urlpatterns, app.label))))
44
-
45
- # Check if the plugin specifies any API URLs
46
- urlpatterns = import_object(f"{plugin_path}.api.urls.urlpatterns")
47
- if urlpatterns is not None:
48
- plugin_api_patterns.append(path(f"{base_url}/", include((urlpatterns, f"{app.label}-api"))))
@@ -4,7 +4,6 @@ Plugin utilities.
4
4
 
5
5
  import importlib.util
6
6
  import logging
7
- import sys
8
7
 
9
8
  from django.core.exceptions import ImproperlyConfigured
10
9
  from django.utils.module_loading import import_string
@@ -17,37 +16,6 @@ from .exceptions import PluginImproperlyConfigured, PluginNotFound
17
16
  logger = logging.getLogger(__name__)
18
17
 
19
18
 
20
- def import_object(module_and_object):
21
- """
22
- Import a specific object from a specific module by name, such as "nautobot.extras.plugins.utils.import_object".
23
-
24
- Returns the imported object, or None if it doesn't exist.
25
- """
26
- target_module_name, object_name = module_and_object.rsplit(".", 1)
27
- module_hierarchy = target_module_name.split(".")
28
-
29
- # Iterate through the module hierarchy, checking for the existence of each successive submodule.
30
- # We have to do this rather than jumping directly to calling find_spec(target_module_name)
31
- # because find_spec will raise a ModuleNotFoundError if any parent module of target_module_name does not exist.
32
- module_name = ""
33
- for module_component in module_hierarchy:
34
- module_name = f"{module_name}.{module_component}" if module_name else module_component
35
- spec = importlib.util.find_spec(module_name)
36
- if spec is None:
37
- # No such module
38
- return None
39
-
40
- # Okay, target_module_name exists. Load it if not already loaded
41
- if target_module_name in sys.modules:
42
- module = sys.modules[target_module_name]
43
- else:
44
- module = importlib.util.module_from_spec(spec)
45
- sys.modules[target_module_name] = module
46
- spec.loader.exec_module(module)
47
-
48
- return getattr(module, object_name, None)
49
-
50
-
51
19
  def load_plugins(settings):
52
20
  """Process plugins and log errors if they can't be loaded."""
53
21
  for plugin_name in settings.PLUGINS:
@@ -77,7 +45,7 @@ def load_plugin(plugin_name, settings):
77
45
  except AttributeError as err:
78
46
  raise PluginImproperlyConfigured(
79
47
  f"Plugin {plugin_name} does not provide a 'config' variable. This should be defined in the plugin's "
80
- f"__init__.py file and point to the PluginConfig subclass."
48
+ f"__init__.py file and point to the NautobotAppConfig subclass."
81
49
  ) from err
82
50
 
83
51
  # Validate user-provided configuration settings and assign defaults. Plugin
@@ -86,7 +86,6 @@ class InstalledAppsView(GenericView):
86
86
  """
87
87
 
88
88
  table = InstalledAppsTable
89
- breadcrumbs = Breadcrumbs(items={"*": [ViewNameBreadcrumbItem(view_name="apps:apps_list", label="Installed Apps")]})
90
89
  view_titles = Titles(titles={"*": "Installed Apps"})
91
90
 
92
91
  def get(self, request):
@@ -250,9 +249,6 @@ class MarketplaceView(GenericView):
250
249
  View for listing all available Apps.
251
250
  """
252
251
 
253
- breadcrumbs = Breadcrumbs(
254
- items={"generic": [ViewNameBreadcrumbItem(view_name="apps:apps_marketplace", label="Apps Marketplace")]}
255
- )
256
252
  view_titles = Titles(titles={"generic": "Apps Marketplace"})
257
253
 
258
254
  def get(self, request):
@@ -24,7 +24,7 @@ from nautobot.core.celery import app, import_jobs
24
24
  from nautobot.core.models import BaseModel
25
25
  from nautobot.core.utils.cache import construct_cache_key
26
26
  from nautobot.core.utils.logging import sanitize
27
- from nautobot.extras.choices import JobResultStatusChoices, ObjectChangeActionChoices
27
+ from nautobot.extras.choices import ButtonClassChoices, JobResultStatusChoices, ObjectChangeActionChoices
28
28
  from nautobot.extras.constants import CHANGELOG_MAX_CHANGE_CONTEXT_DETAIL
29
29
  from nautobot.extras.models import (
30
30
  ComputedField,
@@ -557,24 +557,6 @@ m2m_changed.connect(dynamic_group_children_changed, sender=DynamicGroup.children
557
557
  pre_save.connect(dynamic_group_membership_created, sender=DynamicGroupMembership)
558
558
 
559
559
 
560
- def dynamic_group_update_cached_members(sender, instance, **kwargs):
561
- """
562
- When a DynamicGroup or DynamicGroupMembership is updated, update the cache of members for it and any parent groups.
563
- """
564
- if isinstance(instance, DynamicGroupMembership):
565
- group = instance.parent_group
566
- else:
567
- group = instance
568
-
569
- group.update_cached_members()
570
- for ancestor in group.get_ancestors():
571
- ancestor.update_cached_members()
572
-
573
-
574
- post_save.connect(dynamic_group_update_cached_members, sender=DynamicGroup)
575
- post_save.connect(dynamic_group_update_cached_members, sender=DynamicGroupMembership)
576
-
577
-
578
560
  #
579
561
  # Jobs
580
562
  #
@@ -659,6 +641,25 @@ def refresh_job_models(sender, *, apps, **kwargs):
659
641
  job_model.installed = False
660
642
  job_model.save()
661
643
 
644
+ # Wire up the JobButton for Dynamic Group member refresh
645
+ JobButton = apps.get_model("extras", "JobButton")
646
+ ContentType = apps.get_model("contenttypes", "ContentType") # pylint: disable=redefined-outer-name
647
+ DynamicGroup = apps.get_model("extras", "DynamicGroup") # pylint: disable=redefined-outer-name
648
+
649
+ dg_job_button, _ = JobButton.objects.get_or_create(
650
+ name="Refresh Dynamic Group Members Cache",
651
+ job=Job.objects.get(
652
+ module_name="nautobot.core.jobs.groups", job_class_name="RefreshDynamicGroupCacheJobButtonReceiver"
653
+ ),
654
+ defaults={
655
+ "enabled": True,
656
+ "text": "Refresh Members",
657
+ "button_class": ButtonClassChoices.CLASS_WARNING,
658
+ "confirmation": True,
659
+ },
660
+ )
661
+ dg_job_button.content_types.add(ContentType.objects.get_for_model(DynamicGroup))
662
+
662
663
 
663
664
  #
664
665
  # Metadata
nautobot/extras/tables.py CHANGED
@@ -1,13 +1,13 @@
1
+ import logging
2
+ from textwrap import dedent
3
+
1
4
  from django.conf import settings
2
- from django.db.models import QuerySet
5
+ from django.contrib.contenttypes.models import ContentType
3
6
  from django.utils.html import format_html, format_html_join
4
7
  import django_tables2 as tables
5
- from django_tables2.data import TableData
6
- from django_tables2.rows import BoundRows
7
8
  from django_tables2.utils import Accessor
8
9
  from jsonschema.exceptions import ValidationError as JSONSchemaValidationError
9
10
 
10
- from nautobot.core.models.querysets import count_related
11
11
  from nautobot.core.tables import (
12
12
  ApprovalButtonsColumn,
13
13
  BaseTable,
@@ -24,7 +24,7 @@ from nautobot.core.tables import (
24
24
  from nautobot.core.templatetags.helpers import HTML_NONE, render_boolean, render_json, render_markdown
25
25
  from nautobot.tenancy.tables import TenantColumn
26
26
 
27
- from .choices import LogLevelChoices, MetadataTypeDataTypeChoices
27
+ from .choices import JobResultStatusChoices, MetadataTypeDataTypeChoices
28
28
  from .models import (
29
29
  ApprovalWorkflow,
30
30
  ApprovalWorkflowDefinition,
@@ -73,6 +73,8 @@ from .models import (
73
73
  )
74
74
  from .registry import registry
75
75
 
76
+ logger = logging.getLogger(__name__)
77
+
76
78
  APPROVAL_WORKFLOW_OBJECT = """
77
79
  {% if record.object_under_review and record.object_under_review.get_absolute_url %}
78
80
  <a href="{{ record.object_under_review.get_absolute_url }}">{{ record.object_under_review }}</a>
@@ -129,7 +131,7 @@ GITREPOSITORY_BUTTONS = """
129
131
  <button
130
132
  data-url="{% url 'extras:gitrepository_sync' pk=record.pk %}"
131
133
  type="submit"
132
- class="dropdown-item sync-repository{% if perms.extras.change_gitrepository %} text-primary"{% else %}" disabled="disabled"{% endif %}
134
+ class="dropdown-item sync-repository{% if perms.extras.change_gitrepository %} text-primary"{% else %}" disabled{% endif %}
133
135
  >
134
136
  <span class="mdi mdi-source-branch-sync" aria-hidden="true"></span>
135
137
  Sync
@@ -168,7 +170,7 @@ JOB_RESULT_BUTTONS = """
168
170
  </li>
169
171
  {% else %}
170
172
  <li>
171
- <a href="#" class="dropdown-item disabled">
173
+ <a class="dropdown-item disabled" aria-disabled="true">
172
174
  <span class="mdi mdi-repeat-off" aria-hidden="true"></span>
173
175
  Job is not available, cannot be re-run
174
176
  </a>
@@ -220,7 +222,7 @@ SCHEDULED_JOB_APPROVAL_QUEUE_BUTTONS = """
220
222
  <button
221
223
  type="button"
222
224
  onClick="handleDetailPostAction('{% url 'extras:scheduledjob_approval_request_view' pk=record.pk %}', '_dry_run')"
223
- class="dropdown-item{% if perms.extras.run_job and record.job_model.supports_dryrun %} text-primary"{% else %}" disabled="disabled"{% endif %}
225
+ class="dropdown-item{% if perms.extras.run_job and record.job_model.supports_dryrun %} text-primary"{% else %}" disabled{% endif %}
224
226
  >
225
227
  <span class="mdi mdi-play" aria-hidden="true"></span>
226
228
  Dry Run
@@ -230,7 +232,7 @@ SCHEDULED_JOB_APPROVAL_QUEUE_BUTTONS = """
230
232
  <button
231
233
  type="button"
232
234
  onClick="handleDetailPostAction('{% url 'extras:scheduledjob_approval_request_view' pk=record.pk %}', '_approve')"
233
- class="dropdown-item{% if perms.extras.run_job %} text-success"{% else %}" disabled="disabled"{% endif %}
235
+ class="dropdown-item{% if perms.extras.run_job %} text-success"{% else %}" disabled{% endif %}
234
236
  >
235
237
  <span class="mdi mdi-check" aria-hidden="true"></span>
236
238
  Approve
@@ -240,7 +242,7 @@ SCHEDULED_JOB_APPROVAL_QUEUE_BUTTONS = """
240
242
  <button
241
243
  type="button"
242
244
  onClick="handleDetailPostAction('{% url 'extras:scheduledjob_approval_request_view' pk=record.pk %}', '_deny')"
243
- class="dropdown-item{% if perms.extras.run_job %} text-danger"{% else %}" disabled="disabled"{% endif %}
245
+ class="dropdown-item{% if perms.extras.run_job %} text-danger"{% else %}" disabled{% endif %}
244
246
  >
245
247
  <span class="mdi mdi-close" aria-hidden="true"></span>
246
248
  Deny
@@ -294,7 +296,7 @@ class ApprovalWorkflowStageDefinitionTable(BaseTable):
294
296
  fields = (
295
297
  "pk",
296
298
  "approval_workflow_definition",
297
- "weight",
299
+ "sequence",
298
300
  "name",
299
301
  "min_approvers",
300
302
  "denial_message",
@@ -303,7 +305,7 @@ class ApprovalWorkflowStageDefinitionTable(BaseTable):
303
305
  default_columns = (
304
306
  "pk",
305
307
  "approval_workflow_definition",
306
- "weight",
308
+ "sequence",
307
309
  "name",
308
310
  "min_approvers",
309
311
  "denial_message",
@@ -1065,8 +1067,8 @@ def log_object_link(value, record):
1065
1067
 
1066
1068
  def log_entry_color_css(record):
1067
1069
  if record.log_level.lower() in ("failure", "error", "critical"):
1068
- return "danger"
1069
- return record.log_level.lower()
1070
+ return "table-danger"
1071
+ return "table-" + record.log_level.lower()
1070
1072
 
1071
1073
 
1072
1074
  class JobTable(BaseTable):
@@ -1270,62 +1272,20 @@ class JobResultTable(BaseTable):
1270
1272
  duration = tables.Column(orderable=False)
1271
1273
  actions = ButtonsColumn(JobResult, buttons=("delete",), prepend_template=JOB_RESULT_BUTTONS)
1272
1274
 
1273
- def __init__(self, *args, **kwargs):
1274
- super().__init__(*args, **kwargs)
1275
- # Only calculate log counts for "summary" column if it's actually visible.
1276
- if "summary" in self.columns and self.columns["summary"].visible and isinstance(self.data.data, QuerySet):
1277
- self.data = TableData.from_data(
1278
- self.data.data.annotate(
1279
- debug_log_count=count_related(
1280
- JobLogEntry, "job_result", filter_dict={"log_level": LogLevelChoices.LOG_DEBUG}
1281
- ),
1282
- success_log_count=count_related(
1283
- JobLogEntry, "job_result", filter_dict={"log_level": LogLevelChoices.LOG_SUCCESS}
1284
- ),
1285
- info_log_count=count_related(
1286
- JobLogEntry, "job_result", filter_dict={"log_level": LogLevelChoices.LOG_INFO}
1287
- ),
1288
- warning_log_count=count_related(
1289
- JobLogEntry, "job_result", filter_dict={"log_level": LogLevelChoices.LOG_WARNING}
1290
- ),
1291
- error_log_count=count_related(
1292
- JobLogEntry,
1293
- "job_result",
1294
- filter_dict={
1295
- "log_level__in": [
1296
- LogLevelChoices.LOG_FAILURE,
1297
- LogLevelChoices.LOG_ERROR,
1298
- LogLevelChoices.LOG_CRITICAL,
1299
- ],
1300
- },
1301
- ),
1302
- )
1303
- )
1304
- self.data.set_table(self)
1305
- self.rows = BoundRows(data=self.data, table=self, pinned_data=self.pinned_data)
1306
-
1307
1275
  def render_summary(self, record):
1308
1276
  """
1309
1277
  Define custom rendering for the summary column.
1310
1278
  """
1311
- # Normally the *_log_count attributes will be generated efficiently via queryset annotation in the view,
1312
- # however, we cannot assume that will always be the case. Calculate them inefficiently as a fallback.
1313
- if not hasattr(record, "debug_log_count"):
1314
- record.debug_log_count = record.job_log_entries.filter(log_level=LogLevelChoices.LOG_DEBUG).count()
1315
- if not hasattr(record, "success_log_count"):
1316
- record.success_log_count = record.job_log_entries.filter(log_level=LogLevelChoices.LOG_SUCCESS).count()
1317
- if not hasattr(record, "info_log_count"):
1318
- record.info_log_count = record.job_log_entries.filter(log_level=LogLevelChoices.LOG_INFO).count()
1319
- if not hasattr(record, "warning_log_count"):
1320
- record.warning_log_count = record.job_log_entries.filter(log_level=LogLevelChoices.LOG_WARNING).count()
1321
- if not hasattr(record, "error_log_count"):
1322
- record.error_log_count = record.job_log_entries.filter(
1323
- log_level__in=[
1324
- LogLevelChoices.LOG_FAILURE,
1325
- LogLevelChoices.LOG_ERROR,
1326
- LogLevelChoices.LOG_CRITICAL,
1327
- ]
1328
- ).count()
1279
+ # The *_log_count attributes will be calculated and updated at the end of a Job run when JobResult is saved.
1280
+ # If the values are not present due to a running Job or are missing in any field, skip display.
1281
+ if record.status not in JobResultStatusChoices.READY_STATES or None in [
1282
+ record.debug_log_count,
1283
+ record.success_log_count,
1284
+ record.info_log_count,
1285
+ record.warning_log_count,
1286
+ record.error_log_count,
1287
+ ]:
1288
+ return ""
1329
1289
 
1330
1290
  return format_html(
1331
1291
  """<label class="badge bg-secondary">{}</label>
@@ -1364,6 +1324,7 @@ class JobResultTable(BaseTable):
1364
1324
  "job_model",
1365
1325
  "user",
1366
1326
  "status",
1327
+ "summary",
1367
1328
  "actions",
1368
1329
  )
1369
1330
 
@@ -1588,8 +1549,31 @@ class ObjectChangeTable(BaseTable):
1588
1549
 
1589
1550
  def __init__(self, *args, **kwargs):
1590
1551
  super().__init__(*args, **kwargs)
1591
- # The `object_repr` column also uses the `changed_object` generic-foreign-key value
1592
- self.add_conditional_prefetch("object_repr", "changed_object")
1552
+ # Only prefetch if all content types are valid
1553
+ if all(ct.model_class() is not None for ct in ContentType.objects.all()):
1554
+ self.add_conditional_prefetch("object_repr", "changed_object")
1555
+ else:
1556
+ error_message = dedent("""\
1557
+ One or more ContentType entries in the database are invalid.
1558
+ This will likely cause performance degradation when viewing the Object Change log.
1559
+ An administrator can follow these steps to resolve common issues:
1560
+ - Run `nautobot-server remove_stale_contenttypes`
1561
+ - Run `nautobot-server migrate <app_label> zero` for any app labels which no longer exist
1562
+ - Manually dropping tables for any models which have been removed from Nautobot or its plugins from your database
1563
+ - Run ```
1564
+ from django.contrib.contenttypes.models import ContentType
1565
+ qs = ContentType.objects.filter(
1566
+ app_label__in=[
1567
+ "<app_label_of_removed_plugin_1>",
1568
+ "<app_label_of_removed_plugin_2>",
1569
+ ]
1570
+ ) | ContentType.objects.filter(model__icontains="<name_of_removed_model_1>")
1571
+ # Review the queryset before running delete
1572
+ qs.delete()
1573
+ ```
1574
+ Please ensure you fully understand the implications of these actions before proceeding.
1575
+ """)
1576
+ logger.warning(error_message)
1593
1577
 
1594
1578
 
1595
1579
  #
@@ -2,12 +2,14 @@
2
2
  {% load helpers %}
3
3
 
4
4
  {% block buttons %}
5
- <div class="btn-group">
6
- <a class="btn {% if approval_view %}btn-primary{% else %}btn-secondary{% endif %}" href="{% url 'extras:approver_dashboard' %}">
7
- My Approvals
5
+ <div class="btn-group" role="group">
6
+ <a class="btn btn-primary{% if approval_view %} bg-primary nb-text-body-bg{% endif %}"
7
+ href="{% url 'extras:approver_dashboard' %}">
8
+ <span class="mdi mdi-checkbox-multiple-outline" aria-hidden="true"></span> My Approvals
8
9
  </a>
9
- <a class="btn {% if not approval_view %}btn-primary{% else %}btn-secondary{% endif %}" href="{% url 'extras:approvee_dashboard' %}">
10
- My Requests
10
+ <a class="btn btn-primary{% if not approval_view %} bg-primary nb-text-body-bg{% endif %}"
11
+ href="{% url 'extras:approvee_dashboard' %}">
12
+ <span class="mdi mdi-help-box-multiple-outline" aria-hidden="true"></span> My Requests
11
13
  </a>
12
14
  </div>
13
15
  {% endblock %}