nautobot 3.0.0a2__py3-none-any.whl → 3.0.0rc1__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.
Files changed (659) hide show
  1. nautobot/apps/choices.py +4 -2
  2. nautobot/apps/filters.py +7 -9
  3. nautobot/apps/models.py +2 -2
  4. nautobot/apps/ui.py +13 -1
  5. nautobot/apps/utils.py +8 -0
  6. nautobot/circuits/filters.py +3 -2
  7. nautobot/circuits/navigation.py +3 -2
  8. nautobot/circuits/templates/circuits/circuit_create.html +3 -3
  9. nautobot/circuits/templates/circuits/circuittermination_create.html +9 -24
  10. nautobot/circuits/templates/circuits/inc/circuit_termination_cable_fragment.html +6 -6
  11. nautobot/circuits/templates/circuits/inc/speed_widget.html +12 -12
  12. nautobot/circuits/tests/integration/test_circuit.py +10 -13
  13. nautobot/circuits/tests/integration/test_circuits_bulk_operations.py +0 -3
  14. nautobot/circuits/views.py +6 -2
  15. nautobot/cloud/filters.py +1 -1
  16. nautobot/cloud/navigation.py +3 -2
  17. nautobot/core/api/schema.py +1 -1
  18. nautobot/core/api/serializers.py +6 -1
  19. nautobot/core/api/urls.py +2 -0
  20. nautobot/core/api/views.py +12 -0
  21. nautobot/core/apps/__init__.py +11 -10
  22. nautobot/core/celery/__init__.py +3 -5
  23. nautobot/core/checks.py +46 -0
  24. nautobot/core/choices.py +1 -1
  25. nautobot/core/cli/bootstrap_v3_to_v5.py +105 -13
  26. nautobot/core/cli/migrate_deprecated_templates.py +227 -0
  27. nautobot/core/constants.py +3 -0
  28. nautobot/core/context_processors.py +9 -1
  29. nautobot/core/filters.py +4 -0
  30. nautobot/core/forms/__init__.py +2 -0
  31. nautobot/core/forms/forms.py +1 -1
  32. nautobot/core/forms/widgets.py +21 -2
  33. nautobot/core/jobs/__init__.py +62 -3
  34. nautobot/core/jobs/groups.py +31 -1
  35. nautobot/core/management/commands/generate_test_data.py +28 -9
  36. nautobot/core/models/__init__.py +11 -0
  37. nautobot/core/models/generics.py +9 -1
  38. nautobot/core/models/tree_queries.py +10 -5
  39. nautobot/core/models/utils.py +1 -1
  40. nautobot/core/settings.py +35 -19
  41. nautobot/core/settings.yaml +17 -33
  42. nautobot/core/signals.py +12 -1
  43. nautobot/core/tables.py +13 -6
  44. nautobot/core/templates/40x.html +1 -1
  45. nautobot/core/templates/500.html +2 -2
  46. nautobot/core/templates/admin/base.html +1 -2
  47. nautobot/core/templates/admin/change_list.html +9 -12
  48. nautobot/core/templates/admin/config/config.html +12 -12
  49. nautobot/core/templates/admin/index.html +3 -3
  50. nautobot/core/templates/base_django.html +1 -2
  51. nautobot/core/templates/buttons/export.html +1 -1
  52. nautobot/core/templates/components/button/dropdown.html +5 -3
  53. nautobot/core/templates/components/panel/body_wrapper_generic_table.html +1 -1
  54. nautobot/core/templates/components/panel/header_extra_content_table.html +1 -1
  55. nautobot/core/templates/components/panel/panel.html +3 -3
  56. nautobot/core/templates/components/tab/content_wrapper.html +6 -7
  57. nautobot/core/templates/components/tab/label_wrapper_distinct_view.html +1 -1
  58. nautobot/core/templates/echarts/echarts.html +22 -9
  59. nautobot/core/templates/generic/object_bulk_add_component.html +2 -1
  60. nautobot/core/templates/generic/object_bulk_create.html +6 -5
  61. nautobot/core/templates/generic/object_bulk_delete.html +1 -1
  62. nautobot/core/templates/generic/object_bulk_destroy.html +3 -3
  63. nautobot/core/templates/generic/object_bulk_edit.html +1 -1
  64. nautobot/core/templates/generic/object_bulk_import.html +1 -1
  65. nautobot/core/templates/generic/object_bulk_remove.html +2 -2
  66. nautobot/core/templates/generic/object_bulk_update.html +5 -4
  67. nautobot/core/templates/generic/object_create.html +5 -4
  68. nautobot/core/templates/generic/object_delete.html +1 -1
  69. nautobot/core/templates/generic/object_detail.html +1 -1
  70. nautobot/core/templates/generic/object_edit.html +1 -1
  71. nautobot/core/templates/generic/object_import.html +2 -1
  72. nautobot/core/templates/generic/object_list.html +12 -4
  73. nautobot/core/templates/generic/object_notes.html +5 -3
  74. nautobot/core/templates/generic/object_retrieve.html +4 -5
  75. nautobot/core/templates/graphene/graphiql.html +7 -8
  76. nautobot/core/templates/home.html +1 -1
  77. nautobot/core/templates/import_success.html +2 -1
  78. nautobot/core/templates/inc/computed_fields/panel_data.html +1 -1
  79. nautobot/core/templates/inc/created_updated.html +7 -3
  80. nautobot/core/templates/inc/custom_fields/panel_data.html +1 -1
  81. nautobot/core/templates/inc/footer.html +3 -1
  82. nautobot/core/templates/inc/form_static_field.html +6 -0
  83. nautobot/core/templates/inc/header.html +11 -1
  84. nautobot/core/templates/inc/image_attachments.html +2 -1
  85. nautobot/core/templates/inc/media.html +14 -0
  86. nautobot/core/templates/inc/nav_menu.html +3 -9
  87. nautobot/core/templates/inc/object_details_advanced_panel.html +2 -2
  88. nautobot/core/templates/inc/search_panel.html +4 -4
  89. nautobot/core/templates/login.html +4 -2
  90. nautobot/core/templates/nautobot_config.py.j2 +6 -11
  91. nautobot/core/templates/redoc_ui.html +7 -0
  92. nautobot/core/templates/rest_framework/api.html +103 -2
  93. nautobot/core/templates/search.html +1 -1
  94. nautobot/core/templates/swagger_ui.html +17 -3
  95. nautobot/core/templates/system_jobs/import_objects.html +1 -2
  96. nautobot/core/templates/utilities/confirmation_form.html +2 -2
  97. nautobot/core/templates/utilities/obj_table.html +10 -2
  98. nautobot/core/templates/utilities/render_field.html +7 -7
  99. nautobot/core/templates/utilities/render_jinja2.html +2 -2
  100. nautobot/core/templates/utilities/templatetags/filter_form_drawer.html +37 -4
  101. nautobot/core/templates/utilities/theme_preview.html +19 -3
  102. nautobot/core/templates/widgets/number_input_with_choices.html +44 -0
  103. nautobot/core/templates/widgets/selectwithdisabled_option.html +3 -1
  104. nautobot/core/templatetags/helpers.py +76 -18
  105. nautobot/core/testing/api.py +68 -9
  106. nautobot/core/testing/filters.py +0 -23
  107. nautobot/core/testing/integration.py +41 -17
  108. nautobot/core/testing/mixins.py +2 -0
  109. nautobot/core/testing/utils.py +18 -4
  110. nautobot/core/testing/views.py +104 -13
  111. nautobot/core/tests/integration/test_app_home.py +34 -30
  112. nautobot/core/tests/integration/test_app_navbar.py +3 -0
  113. nautobot/core/tests/integration/test_filters.py +48 -11
  114. nautobot/core/tests/integration/test_theme.py +22 -21
  115. nautobot/core/tests/nautobot_config.py +3 -0
  116. nautobot/core/tests/nautobot_config_without_example_apps.py +4 -0
  117. nautobot/core/tests/runner.py +8 -1
  118. nautobot/core/tests/test_api.py +5 -3
  119. nautobot/core/tests/test_breadcrumbs.py +27 -28
  120. nautobot/core/tests/test_checks.py +28 -0
  121. nautobot/core/tests/test_cli.py +40 -0
  122. nautobot/core/tests/test_config.py +2 -1
  123. nautobot/core/tests/test_forms.py +55 -13
  124. nautobot/core/tests/test_jobs.py +144 -3
  125. nautobot/core/tests/test_nautobot_server.py +2 -0
  126. nautobot/core/tests/test_navigations.py +76 -1
  127. nautobot/core/tests/test_patch_social_django.py +42 -0
  128. nautobot/core/tests/test_renderers.py +59 -0
  129. nautobot/core/tests/test_settings_schema.py +1 -0
  130. nautobot/core/tests/test_tables.py +3 -1
  131. nautobot/core/tests/test_templatetags_helpers.py +62 -13
  132. nautobot/core/tests/test_templatetags_ui_framework.py +4 -4
  133. nautobot/core/tests/test_titles.py +0 -16
  134. nautobot/core/tests/test_tree_queries.py +14 -1
  135. nautobot/core/tests/test_ui.py +123 -4
  136. nautobot/core/tests/test_utils.py +72 -5
  137. nautobot/core/tests/test_views.py +159 -31
  138. nautobot/core/ui/breadcrumbs.py +70 -29
  139. nautobot/core/ui/bulk_buttons.py +1 -1
  140. nautobot/core/ui/choices.py +143 -27
  141. nautobot/core/ui/constants.py +76 -12
  142. nautobot/core/ui/echarts.py +15 -20
  143. nautobot/core/ui/object_detail.py +143 -55
  144. nautobot/core/ui/titles.py +3 -6
  145. nautobot/core/urls.py +20 -9
  146. nautobot/core/utils/cache.py +2 -1
  147. nautobot/core/utils/filtering.py +28 -18
  148. nautobot/core/utils/lookup.py +49 -8
  149. nautobot/core/utils/module_loading.py +21 -0
  150. nautobot/core/utils/patch_social_django.py +128 -0
  151. nautobot/core/views/__init__.py +38 -1
  152. nautobot/core/views/generic.py +3 -3
  153. nautobot/core/views/mixins.py +45 -22
  154. nautobot/core/views/renderers.py +4 -3
  155. nautobot/core/views/viewsets.py +2 -1
  156. nautobot/data_validation/apps.py +1 -5
  157. nautobot/data_validation/custom_validators.py +4 -4
  158. nautobot/data_validation/filters.py +1 -1
  159. nautobot/data_validation/forms.py +40 -0
  160. nautobot/data_validation/migrations/0001_initial.py +0 -7
  161. nautobot/data_validation/migrations/0002_data_migration_from_app.py +3 -14
  162. nautobot/data_validation/models.py +16 -7
  163. nautobot/data_validation/navigation.py +8 -1
  164. nautobot/data_validation/tables.py +12 -5
  165. nautobot/data_validation/templates/data_validation/datacompliance_tab.html +1 -0
  166. nautobot/data_validation/templates/data_validation/device_constraints.html +61 -0
  167. nautobot/data_validation/tests/__init__.py +2 -2
  168. nautobot/data_validation/tests/migrations/test_migrations.py +83 -3
  169. nautobot/data_validation/tests/test_data_compliance_rules.py +12 -7
  170. nautobot/data_validation/tests/test_filters.py +8 -6
  171. nautobot/data_validation/tests/test_models.py +15 -0
  172. nautobot/data_validation/tests/test_views.py +190 -32
  173. nautobot/data_validation/urls.py +2 -5
  174. nautobot/data_validation/views.py +73 -40
  175. nautobot/dcim/api/serializers.py +3 -13
  176. nautobot/dcim/apps.py +4 -0
  177. nautobot/dcim/choices.py +65 -0
  178. nautobot/dcim/constants.py +7 -0
  179. nautobot/dcim/custom_validators.py +84 -0
  180. nautobot/dcim/factory.py +1 -1
  181. nautobot/dcim/filter_mixins.py +353 -4
  182. nautobot/dcim/{filters/__init__.py → filters.py} +15 -36
  183. nautobot/dcim/forms.py +90 -4
  184. nautobot/dcim/migrations/0075_interface_duplex_interface_speed_and_more.py +32 -0
  185. nautobot/dcim/migrations/{0075_add_deviceclusterassignment.py → 0076_add_deviceclusterassignment.py} +1 -1
  186. nautobot/dcim/migrations/{0076_device_cluster_to_clusters_data_migration.py → 0077_device_cluster_to_clusters_data_migration.py} +1 -1
  187. nautobot/dcim/migrations/{0077_remove_device_cluster.py → 0078_remove_device_cluster.py} +1 -1
  188. nautobot/dcim/migrations/0079_remove_device_location_tenant_name_uniqueness.py +16 -0
  189. nautobot/dcim/migrations/0080_device_name_data_migration.py +59 -0
  190. nautobot/dcim/migrations/0081_alter_device_device_redundancy_group_priority_and_more.py +25 -0
  191. nautobot/dcim/models/device_component_templates.py +33 -1
  192. nautobot/dcim/models/device_components.py +98 -64
  193. nautobot/dcim/models/devices.py +30 -20
  194. nautobot/dcim/navigation.py +7 -6
  195. nautobot/dcim/tables/devices.py +18 -0
  196. nautobot/dcim/tables/devicetypes.py +8 -1
  197. nautobot/dcim/tables/racks.py +0 -2
  198. nautobot/dcim/tables/template_code.py +15 -15
  199. nautobot/dcim/templates/dcim/cable_connect.html +28 -112
  200. nautobot/dcim/templates/dcim/cable_trace.html +0 -4
  201. nautobot/dcim/templates/dcim/{cable_edit.html → cable_update.html} +1 -1
  202. nautobot/dcim/templates/dcim/consoleport.html +7 -6
  203. nautobot/dcim/templates/dcim/consoleserverport.html +7 -6
  204. nautobot/dcim/templates/dcim/device/config.html +2 -2
  205. nautobot/dcim/templates/dcim/device/lldp_neighbors.html +1 -1
  206. nautobot/dcim/templates/dcim/device/status.html +8 -8
  207. nautobot/dcim/templates/dcim/device.html +1 -1
  208. nautobot/dcim/templates/dcim/device_component_add.html +2 -2
  209. nautobot/dcim/templates/dcim/device_create.html +5 -3
  210. nautobot/dcim/templates/dcim/device_interface_delete.html +1 -1
  211. nautobot/dcim/templates/dcim/device_list.html +73 -10
  212. nautobot/dcim/templates/dcim/devicebay.html +1 -1
  213. nautobot/dcim/templates/dcim/devicebay_populate.html +2 -2
  214. nautobot/dcim/templates/dcim/devicetype_component_add.html +2 -2
  215. nautobot/dcim/templates/dcim/footer_convert_to_contact_or_team_record.html +14 -0
  216. nautobot/dcim/templates/dcim/frontport.html +10 -9
  217. nautobot/dcim/templates/dcim/inc/devicetype_component_table.html +1 -1
  218. nautobot/dcim/templates/dcim/inc/edit_form_softwareversion_js.html +2 -2
  219. nautobot/dcim/templates/dcim/inc/moduletype_component_table.html +1 -1
  220. nautobot/dcim/templates/dcim/inc/rack_elevation.html +1 -1
  221. nautobot/dcim/templates/dcim/interface.html +35 -7
  222. nautobot/dcim/templates/dcim/interface_bulk_delete.html +1 -1
  223. nautobot/dcim/templates/dcim/interface_edit.html +2 -0
  224. nautobot/dcim/templates/dcim/inventoryitem.html +1 -1
  225. nautobot/dcim/templates/dcim/inventoryitem_add.html +3 -1
  226. nautobot/dcim/templates/dcim/inventoryitem_bulk_delete.html +1 -1
  227. nautobot/dcim/templates/dcim/inventoryitem_edit.html +3 -1
  228. nautobot/dcim/templates/dcim/module/base.html +49 -9
  229. nautobot/dcim/templates/dcim/module_consoleports.html +1 -1
  230. nautobot/dcim/templates/dcim/module_consoleserverports.html +1 -1
  231. nautobot/dcim/templates/dcim/module_frontports.html +1 -1
  232. nautobot/dcim/templates/dcim/module_interfaces.html +1 -1
  233. nautobot/dcim/templates/dcim/module_list.html +57 -8
  234. nautobot/dcim/templates/dcim/module_modulebays.html +1 -1
  235. nautobot/dcim/templates/dcim/module_poweroutlets.html +1 -1
  236. nautobot/dcim/templates/dcim/module_powerports.html +1 -1
  237. nautobot/dcim/templates/dcim/module_rearports.html +1 -1
  238. nautobot/dcim/templates/dcim/modulefamily_retrieve.html +1 -1
  239. nautobot/dcim/templates/dcim/moduletype_list.html +2 -2
  240. nautobot/dcim/templates/dcim/moduletype_retrieve.html +49 -9
  241. nautobot/dcim/templates/dcim/platform_create.html +1 -1
  242. nautobot/dcim/templates/dcim/poweroutlet.html +1 -1
  243. nautobot/dcim/templates/dcim/powerport.html +6 -5
  244. nautobot/dcim/templates/dcim/rack_elevation_list.html +17 -5
  245. nautobot/dcim/templates/dcim/rack_retrieve.html +22 -15
  246. nautobot/dcim/templates/dcim/rearport.html +8 -7
  247. nautobot/dcim/templates/dcim/trace/cable.html +1 -1
  248. nautobot/dcim/templates/dcim/virtualchassis_add_member.html +16 -14
  249. nautobot/dcim/templates/dcim/virtualchassis_update.html +15 -7
  250. nautobot/dcim/tests/integration/test_controller.py +4 -6
  251. nautobot/dcim/tests/integration/test_controller_managed_device_group.py +1 -5
  252. nautobot/dcim/tests/integration/test_create_device.py +0 -2
  253. nautobot/dcim/tests/integration/test_device_bulk_operations.py +1 -3
  254. nautobot/dcim/tests/integration/test_fileinputpicker.py +6 -10
  255. nautobot/dcim/tests/integration/test_location_bulk_operations.py +0 -2
  256. nautobot/dcim/tests/integration/test_module_bay_position.py +3 -4
  257. nautobot/dcim/tests/test_api.py +194 -6
  258. nautobot/dcim/tests/test_custom_validators.py +229 -0
  259. nautobot/dcim/tests/test_filters.py +55 -7
  260. nautobot/dcim/tests/test_forms.py +110 -8
  261. nautobot/dcim/tests/test_graphql.py +44 -1
  262. nautobot/dcim/tests/test_models.py +328 -4
  263. nautobot/dcim/tests/test_tables.py +160 -0
  264. nautobot/dcim/tests/test_views.py +132 -29
  265. nautobot/dcim/urls.py +64 -21
  266. nautobot/dcim/utils.py +3 -3
  267. nautobot/dcim/views.py +777 -397
  268. nautobot/extras/api/views.py +60 -45
  269. nautobot/extras/choices.py +2 -13
  270. nautobot/extras/datasources/git.py +3 -1
  271. nautobot/extras/{filters/mixins.py → filter_mixins.py} +1 -1
  272. nautobot/extras/{filters/customfields.py → filter_mixins_customfields.py} +42 -6
  273. nautobot/extras/{filters/__init__.py → filters.py} +33 -48
  274. nautobot/extras/forms/forms.py +14 -15
  275. nautobot/extras/forms/mixins.py +0 -41
  276. nautobot/extras/jobs.py +2 -0
  277. nautobot/extras/jobs_ui.py +4 -3
  278. nautobot/extras/management/__init__.py +11 -0
  279. nautobot/extras/management/commands/refresh_dynamic_group_member_caches.py +4 -1
  280. nautobot/extras/migrations/0127_approval_workflow_models.py +6 -6
  281. nautobot/extras/migrations/0129_jobresult_debug_log_count_jobresult_error_log_count_and_more.py +37 -0
  282. nautobot/extras/migrations/0130_jobresult_generate_log_entry_counts.py +42 -0
  283. nautobot/extras/migrations/0131_configcontext_device_families.py +18 -0
  284. nautobot/extras/models/__init__.py +1 -2
  285. nautobot/extras/models/approvals.py +33 -14
  286. nautobot/extras/models/change_logging.py +4 -0
  287. nautobot/extras/models/contacts.py +2 -0
  288. nautobot/extras/models/groups.py +44 -5
  289. nautobot/extras/models/jobs.py +60 -4
  290. nautobot/extras/models/mixins.py +28 -0
  291. nautobot/extras/models/models.py +23 -2
  292. nautobot/extras/models/secrets.py +1 -0
  293. nautobot/extras/models/statuses.py +0 -15
  294. nautobot/extras/navigation.py +13 -9
  295. nautobot/extras/plugins/__init__.py +33 -55
  296. nautobot/extras/plugins/marketplace_manifest.yml +49 -1
  297. nautobot/extras/plugins/tables.py +3 -3
  298. nautobot/extras/plugins/urls.py +2 -21
  299. nautobot/extras/plugins/utils.py +1 -33
  300. nautobot/extras/plugins/views.py +0 -9
  301. nautobot/extras/querysets.py +8 -0
  302. nautobot/extras/signals.py +20 -19
  303. nautobot/extras/tables.py +64 -68
  304. nautobot/extras/templates/django_ajax_tables/ajax_wrapper.html +2 -0
  305. nautobot/extras/templates/extras/approval_dashboard.html +7 -5
  306. nautobot/extras/templates/extras/approvalworkflowdefinition_update.html +4 -2
  307. nautobot/extras/templates/extras/approvalworkflowstage_retrieve.html +20 -12
  308. nautobot/extras/templates/extras/configcontext_update.html +1 -0
  309. nautobot/extras/templates/extras/configcontextschema_validation.html +2 -2
  310. nautobot/extras/templates/extras/dynamicgroup_retrieve.html +11 -5
  311. nautobot/extras/templates/extras/dynamicgroup_update.html +1 -1
  312. nautobot/extras/templates/extras/gitrepository_result.html +0 -2
  313. nautobot/extras/templates/extras/inc/approval_buttons_column.html +20 -6
  314. nautobot/extras/templates/extras/inc/bulk_edit_overridable_field.html +8 -7
  315. nautobot/extras/templates/extras/inc/configcontext_format.html +10 -3
  316. nautobot/extras/templates/extras/inc/graphqlquery_execute.html +71 -0
  317. nautobot/extras/templates/extras/inc/job_tiles.html +15 -3
  318. nautobot/extras/templates/extras/inc/json_format.html +10 -3
  319. nautobot/extras/templates/extras/inc/overridable_field.html +13 -12
  320. nautobot/extras/templates/extras/job.html +29 -12
  321. nautobot/extras/templates/extras/job_bulk_edit.html +18 -0
  322. nautobot/extras/templates/extras/job_edit.html +52 -46
  323. nautobot/extras/templates/extras/job_list.html +29 -25
  324. nautobot/extras/templates/extras/marketplace.html +5 -9
  325. nautobot/extras/templates/extras/object_configcontext.html +1 -1
  326. nautobot/extras/templates/extras/object_dynamicgroups.html +2 -2
  327. nautobot/extras/templates/extras/objectchange_retrieve.html +19 -39
  328. nautobot/extras/templates/extras/plugin_detail.html +29 -24
  329. nautobot/extras/templates/extras/plugins_list.html +16 -26
  330. nautobot/extras/templates/extras/role_retrieve.html +64 -0
  331. nautobot/extras/templates/extras/scheduledjob.html +4 -2
  332. nautobot/extras/templates/extras/secret_create.html +1 -1
  333. nautobot/extras/templatetags/custom_links.py +12 -12
  334. nautobot/extras/templatetags/job_buttons.py +14 -12
  335. nautobot/extras/test_jobs/invalid_import.py +9 -0
  336. nautobot/extras/test_jobs/log_counts_by_level.py +23 -0
  337. nautobot/extras/test_jobs/missing_import.py +11 -0
  338. nautobot/extras/tests/integration/test_computedfields.py +8 -9
  339. nautobot/extras/tests/integration/test_configcontextschema.py +27 -26
  340. nautobot/extras/tests/integration/test_customfields.py +9 -10
  341. nautobot/extras/tests/integration/test_dynamicgroups.py +12 -9
  342. nautobot/extras/tests/integration/test_plugin_banner.py +3 -0
  343. nautobot/extras/tests/integration/test_plugins.py +18 -6
  344. nautobot/extras/tests/integration/test_relationships.py +0 -2
  345. nautobot/extras/tests/test_api.py +90 -18
  346. nautobot/extras/tests/test_approvals.py +38 -38
  347. nautobot/extras/tests/test_changelog.py +59 -5
  348. nautobot/extras/tests/test_customfields.py +22 -13
  349. nautobot/extras/tests/test_customfields_filters.py +479 -0
  350. nautobot/extras/tests/test_dynamicgroups.py +39 -1
  351. nautobot/extras/tests/test_filters.py +57 -22
  352. nautobot/extras/tests/test_forms.py +18 -21
  353. nautobot/extras/tests/test_jobs.py +25 -4
  354. nautobot/extras/tests/test_migrations.py +1 -0
  355. nautobot/extras/tests/test_models.py +51 -33
  356. nautobot/extras/tests/test_plugins.py +36 -10
  357. nautobot/extras/tests/test_utils.py +3 -4
  358. nautobot/extras/tests/test_views.py +52 -112
  359. nautobot/extras/urls.py +0 -14
  360. nautobot/extras/views.py +164 -71
  361. nautobot/ipam/factory.py +7 -0
  362. nautobot/ipam/filter_mixins.py +38 -0
  363. nautobot/ipam/filters.py +53 -38
  364. nautobot/ipam/formfields.py +1 -1
  365. nautobot/ipam/forms.py +6 -3
  366. nautobot/ipam/migrations/0030_ipam__namespaces.py +13 -0
  367. nautobot/ipam/migrations/0031_ipam___data_migrations.py +4 -1
  368. nautobot/ipam/migrations/0054_namespace_tenant.py +25 -0
  369. nautobot/ipam/models.py +29 -2
  370. nautobot/ipam/navigation.py +3 -2
  371. nautobot/ipam/signals.py +71 -0
  372. nautobot/ipam/tables.py +19 -6
  373. nautobot/ipam/templates/ipam/inc/toggle_available.html +10 -10
  374. nautobot/ipam/templates/ipam/inc/vlangroup_header.html +1 -0
  375. nautobot/ipam/templates/ipam/ipaddress.html +14 -0
  376. nautobot/ipam/templates/ipam/ipaddress_merge.html +3 -3
  377. nautobot/ipam/templates/ipam/ipaddresstointerface_retrieve.html +1 -0
  378. nautobot/ipam/templates/ipam/namespace_ip_addresses.html +1 -1
  379. nautobot/ipam/templates/ipam/namespace_prefixes.html +1 -1
  380. nautobot/ipam/templates/ipam/namespace_update.html +15 -0
  381. nautobot/ipam/templates/ipam/namespace_vrfs.html +1 -1
  382. nautobot/ipam/templates/ipam/prefix_delete.html +1 -1
  383. nautobot/ipam/templates/ipam/prefix_list.html +14 -13
  384. nautobot/ipam/templates/ipam/vlan_interfaces.html +1 -1
  385. nautobot/ipam/templates/ipam/vlan_vminterfaces.html +1 -1
  386. nautobot/ipam/tests/migration/test_migrations.py +89 -0
  387. nautobot/ipam/tests/test_api.py +13 -6
  388. nautobot/ipam/tests/test_filters.py +36 -1
  389. nautobot/ipam/tests/test_forms.py +1 -1
  390. nautobot/ipam/tests/test_models.py +44 -2
  391. nautobot/ipam/tests/test_tables.py +1 -2
  392. nautobot/ipam/tests/test_utils.py +1 -1
  393. nautobot/ipam/tests/test_views.py +13 -14
  394. nautobot/ipam/ui.py +0 -17
  395. nautobot/ipam/utils/migrations.py +16 -2
  396. nautobot/ipam/utils/testing.py +9 -3
  397. nautobot/ipam/views.py +53 -11
  398. nautobot/load_balancers/__init__.py +0 -0
  399. nautobot/load_balancers/api/__init__.py +1 -0
  400. nautobot/load_balancers/api/serializers.py +75 -0
  401. nautobot/load_balancers/api/urls.py +23 -0
  402. nautobot/load_balancers/api/views.py +61 -0
  403. nautobot/load_balancers/apps.py +17 -0
  404. nautobot/load_balancers/choices.py +167 -0
  405. nautobot/load_balancers/filters.py +225 -0
  406. nautobot/load_balancers/forms.py +532 -0
  407. nautobot/load_balancers/management/commands/__init__.py +0 -0
  408. nautobot/load_balancers/management/commands/generate_load_balancer_models_test_data.py +38 -0
  409. nautobot/load_balancers/migrations/0001_initial.py +465 -0
  410. nautobot/load_balancers/migrations/0002_create_default_statuses_pool_members.py +31 -0
  411. nautobot/load_balancers/migrations/__init__.py +0 -0
  412. nautobot/load_balancers/models.py +423 -0
  413. nautobot/load_balancers/navigation.py +80 -0
  414. nautobot/load_balancers/tables.py +255 -0
  415. nautobot/load_balancers/tests/__init__.py +474 -0
  416. nautobot/load_balancers/tests/test_api.py +353 -0
  417. nautobot/load_balancers/tests/test_filters.py +134 -0
  418. nautobot/load_balancers/tests/test_forms.py +266 -0
  419. nautobot/load_balancers/tests/test_models.py +195 -0
  420. nautobot/load_balancers/tests/test_views.py +229 -0
  421. nautobot/load_balancers/urls.py +17 -0
  422. nautobot/load_balancers/views.py +248 -0
  423. nautobot/project-static/dist/css/github-dark.min.css +10 -0
  424. nautobot/project-static/dist/css/github.min.css +10 -0
  425. nautobot/project-static/dist/css/nautobot.css +1 -11
  426. nautobot/project-static/dist/css/nautobot.css.map +1 -1
  427. nautobot/project-static/dist/js/libraries.js +1 -1
  428. nautobot/project-static/dist/js/libraries.js.map +1 -1
  429. nautobot/project-static/dist/js/nautobot.js +1 -1
  430. nautobot/project-static/dist/js/nautobot.js.map +1 -1
  431. nautobot/project-static/js/cabletrace.js +1 -1
  432. nautobot/project-static/js/forms.js +13 -0
  433. nautobot/project-static/js/interface_filtering.js +20 -16
  434. nautobot/project-static/nautobot-icons/battery-3.svg +3 -0
  435. nautobot/project-static/nautobot-icons/bus-globe.svg +3 -0
  436. nautobot/project-static/nautobot-icons/bus-shield-check.svg +3 -0
  437. nautobot/project-static/nautobot-icons/bus-shield.svg +3 -0
  438. nautobot/project-static/nautobot-icons/cloud.svg +1 -1
  439. nautobot/project-static/nautobot-icons/control-panel.svg +1 -1
  440. nautobot/project-static/nautobot-icons/device-lifecycle.svg +1 -1
  441. nautobot/project-static/nautobot-icons/elements.svg +1 -1
  442. nautobot/project-static/nautobot-icons/extensibility.svg +3 -0
  443. nautobot/project-static/nautobot-icons/hammer.svg +1 -1
  444. nautobot/project-static/nautobot-icons/organization.svg +3 -0
  445. nautobot/project-static/nautobot-icons/secrets.svg +1 -1
  446. nautobot/project-static/nautobot-icons/security.svg +3 -0
  447. nautobot/project-static/nautobot-icons/server.svg +1 -1
  448. nautobot/project-static/nautobot-icons/star-filled.svg +1 -1
  449. nautobot/project-static/nautobot-icons/star.svg +1 -1
  450. nautobot/tenancy/api/serializers.py +1 -0
  451. nautobot/tenancy/api/views.py +2 -1
  452. nautobot/tenancy/{filters/__init__.py → filters.py} +2 -10
  453. nautobot/tenancy/navigation.py +3 -1
  454. nautobot/tenancy/tests/test_filters.py +0 -2
  455. nautobot/tenancy/views.py +2 -1
  456. nautobot/ui/package-lock.json +87 -4
  457. nautobot/ui/package.json +2 -1
  458. nautobot/ui/src/js/collapse.js +3 -3
  459. nautobot/ui/src/js/nautobot.js +16 -1
  460. nautobot/ui/src/js/select2.js +53 -2
  461. nautobot/ui/src/scss/colors.scss +1 -1
  462. nautobot/ui/src/scss/nautobot.scss +112 -30
  463. nautobot/ui/webpack.config.js +13 -0
  464. nautobot/users/templates/users/preferences.html +11 -2
  465. nautobot/users/templates/users/profile.html +45 -12
  466. nautobot/users/templates/users/sessionkey_delete.html +1 -1
  467. nautobot/users/tests/test_api.py +4 -0
  468. nautobot/users/views.py +4 -2
  469. nautobot/virtualization/filters.py +6 -1
  470. nautobot/virtualization/models.py +1 -68
  471. nautobot/virtualization/navigation.py +3 -2
  472. nautobot/virtualization/templates/virtualization/virtual_machine_vminterface_delete.html +1 -1
  473. nautobot/virtualization/templates/virtualization/virtualmachine_list.html +2 -2
  474. nautobot/virtualization/templates/virtualization/virtualmachine_update.html +3 -1
  475. nautobot/virtualization/tests/test_api.py +3 -0
  476. nautobot/virtualization/tests/test_filters.py +10 -1
  477. nautobot/virtualization/tests/test_models.py +45 -4
  478. nautobot/virtualization/views.py +4 -1
  479. nautobot/vpn/__init__.py +0 -0
  480. nautobot/vpn/api/serializers.py +113 -0
  481. nautobot/vpn/api/urls.py +19 -0
  482. nautobot/vpn/api/views.py +70 -0
  483. nautobot/vpn/apps.py +8 -0
  484. nautobot/vpn/choices.py +171 -0
  485. nautobot/vpn/factory.py +219 -0
  486. nautobot/vpn/filters.py +234 -0
  487. nautobot/vpn/forms.py +487 -0
  488. nautobot/vpn/homepage.py +19 -0
  489. nautobot/vpn/migrations/0001_initial.py +541 -0
  490. nautobot/vpn/migrations/0002_populate_defaults.py +199 -0
  491. nautobot/vpn/migrations/__init__.py +0 -0
  492. nautobot/vpn/models.py +535 -0
  493. nautobot/vpn/navigation.py +98 -0
  494. nautobot/vpn/tables.py +383 -0
  495. nautobot/vpn/templates/vpn/vpnprofile_create.html +150 -0
  496. nautobot/vpn/tests/__init__.py +0 -0
  497. nautobot/vpn/tests/test_api.py +336 -0
  498. nautobot/vpn/tests/test_filters.py +139 -0
  499. nautobot/vpn/tests/test_forms.py +293 -0
  500. nautobot/vpn/tests/test_models.py +147 -0
  501. nautobot/vpn/tests/test_views.py +300 -0
  502. nautobot/vpn/urls.py +16 -0
  503. nautobot/vpn/views.py +495 -0
  504. nautobot/wireless/navigation.py +3 -2
  505. nautobot/wireless/tests/integration/test_radio_profile.py +1 -5
  506. nautobot/wireless/tests/test_api.py +1 -1
  507. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/METADATA +15 -15
  508. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/RECORD +514 -572
  509. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/entry_points.txt +1 -0
  510. nautobot/circuits/templates/circuits/circuit.html +0 -2
  511. nautobot/circuits/templates/circuits/circuit_edit.html +0 -2
  512. nautobot/circuits/templates/circuits/circuit_retrieve.html +0 -2
  513. nautobot/circuits/templates/circuits/circuit_update.html +0 -1
  514. nautobot/circuits/templates/circuits/circuittermination.html +0 -2
  515. nautobot/circuits/templates/circuits/circuittermination_edit.html +0 -2
  516. nautobot/circuits/templates/circuits/circuittermination_retrieve.html +0 -2
  517. nautobot/circuits/templates/circuits/circuittermination_update.html +0 -1
  518. nautobot/circuits/templates/circuits/circuittype.html +0 -2
  519. nautobot/circuits/templates/circuits/circuittype_retrieve.html +0 -2
  520. nautobot/circuits/templates/circuits/inc/circuit_termination.html +0 -85
  521. nautobot/circuits/templates/circuits/provider.html +0 -2
  522. nautobot/circuits/templates/circuits/provider_edit.html +0 -2
  523. nautobot/circuits/templates/circuits/provider_retrieve.html +0 -1
  524. nautobot/circuits/templates/circuits/provider_update.html +0 -1
  525. nautobot/circuits/templates/circuits/providernetwork.html +0 -2
  526. nautobot/circuits/templates/circuits/providernetwork_retrieve.html +0 -2
  527. nautobot/cloud/templates/cloud/cloudaccount_retrieve.html +0 -2
  528. nautobot/cloud/templates/cloud/cloudnetwork_retrieve.html +0 -2
  529. nautobot/cloud/templates/cloud/cloudresourcetype_retrieve.html +0 -2
  530. nautobot/cloud/templates/cloud/cloudservice_retrieve.html +0 -2
  531. nautobot/core/templates/buttons/import.html +0 -9
  532. nautobot/data_validation/template_content.py +0 -42
  533. nautobot/data_validation/templates/data_validation/datacompliance_retrieve.html +0 -1
  534. nautobot/dcim/filters/mixins.py +0 -354
  535. nautobot/dcim/templates/dcim/controller/base.html +0 -2
  536. nautobot/dcim/templates/dcim/controller_retrieve.html +0 -2
  537. nautobot/dcim/templates/dcim/controller_wirelessnetworks.html +0 -2
  538. nautobot/dcim/templates/dcim/controllermanageddevicegroup_retrieve.html +0 -2
  539. nautobot/dcim/templates/dcim/device/base.html +0 -2
  540. nautobot/dcim/templates/dcim/device/consoleports.html +0 -2
  541. nautobot/dcim/templates/dcim/device/consoleserverports.html +0 -2
  542. nautobot/dcim/templates/dcim/device/devicebays.html +0 -2
  543. nautobot/dcim/templates/dcim/device/frontports.html +0 -2
  544. nautobot/dcim/templates/dcim/device/interfaces.html +0 -2
  545. nautobot/dcim/templates/dcim/device/inventory.html +0 -2
  546. nautobot/dcim/templates/dcim/device/modulebays.html +0 -2
  547. nautobot/dcim/templates/dcim/device/poweroutlets.html +0 -2
  548. nautobot/dcim/templates/dcim/device/powerports.html +0 -2
  549. nautobot/dcim/templates/dcim/device/rearports.html +0 -2
  550. nautobot/dcim/templates/dcim/device/wireless.html +0 -2
  551. nautobot/dcim/templates/dcim/device_component.html +0 -2
  552. nautobot/dcim/templates/dcim/device_edit.html +0 -2
  553. nautobot/dcim/templates/dcim/devicefamily_retrieve.html +0 -2
  554. nautobot/dcim/templates/dcim/deviceredundancygroup_retrieve.html +0 -2
  555. nautobot/dcim/templates/dcim/devicetype.html +0 -2
  556. nautobot/dcim/templates/dcim/devicetype_edit.html +0 -2
  557. nautobot/dcim/templates/dcim/devicetype_retrieve.html +0 -2
  558. nautobot/dcim/templates/dcim/inc/device_napalm_tabs.html +0 -1
  559. nautobot/dcim/templates/dcim/interfaceredundancygroup_retrieve.html +0 -2
  560. nautobot/dcim/templates/dcim/location.html +0 -2
  561. nautobot/dcim/templates/dcim/location_edit.html +0 -2
  562. nautobot/dcim/templates/dcim/location_retrieve.html +0 -243
  563. nautobot/dcim/templates/dcim/locationtype.html +0 -2
  564. nautobot/dcim/templates/dcim/locationtype_retrieve.html +0 -2
  565. nautobot/dcim/templates/dcim/manufacturer.html +0 -2
  566. nautobot/dcim/templates/dcim/modulebay_retrieve.html +0 -1
  567. nautobot/dcim/templates/dcim/platform.html +0 -2
  568. nautobot/dcim/templates/dcim/powerfeed.html +0 -2
  569. nautobot/dcim/templates/dcim/powerfeed_retrieve.html +0 -2
  570. nautobot/dcim/templates/dcim/powerpanel.html +0 -2
  571. nautobot/dcim/templates/dcim/powerpanel_edit.html +0 -2
  572. nautobot/dcim/templates/dcim/powerpanel_retrieve.html +0 -2
  573. nautobot/dcim/templates/dcim/rack.html +0 -2
  574. nautobot/dcim/templates/dcim/rack_edit.html +0 -2
  575. nautobot/dcim/templates/dcim/rackgroup.html +0 -2
  576. nautobot/dcim/templates/dcim/rackreservation.html +0 -2
  577. nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +0 -2
  578. nautobot/dcim/templates/dcim/softwareversion_retrieve.html +0 -2
  579. nautobot/dcim/templates/dcim/virtualchassis.html +0 -2
  580. nautobot/dcim/templates/dcim/virtualchassis_add.html +0 -2
  581. nautobot/dcim/templates/dcim/virtualchassis_edit.html +0 -2
  582. nautobot/dcim/templates/dcim/virtualchassis_retrieve.html +0 -2
  583. nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +0 -2
  584. nautobot/dcim/ui.py +0 -29
  585. nautobot/extras/templates/extras/computedfield.html +0 -2
  586. nautobot/extras/templates/extras/computedfield_retrieve.html +0 -2
  587. nautobot/extras/templates/extras/configcontext.html +0 -2
  588. nautobot/extras/templates/extras/configcontext_edit.html +0 -2
  589. nautobot/extras/templates/extras/configcontext_retrieve.html +0 -2
  590. nautobot/extras/templates/extras/configcontextschema.html +0 -2
  591. nautobot/extras/templates/extras/configcontextschema_edit.html +0 -2
  592. nautobot/extras/templates/extras/contact_retrieve.html +0 -2
  593. nautobot/extras/templates/extras/customfield.html +0 -2
  594. nautobot/extras/templates/extras/customfield_edit.html +0 -2
  595. nautobot/extras/templates/extras/customfield_retrieve.html +0 -2
  596. nautobot/extras/templates/extras/customlink.html +0 -2
  597. nautobot/extras/templates/extras/dynamicgroup.html +0 -2
  598. nautobot/extras/templates/extras/dynamicgroup_edit.html +0 -2
  599. nautobot/extras/templates/extras/exporttemplate.html +0 -2
  600. nautobot/extras/templates/extras/gitrepository.html +0 -2
  601. nautobot/extras/templates/extras/gitrepository_object_edit.html +0 -2
  602. nautobot/extras/templates/extras/graphqlquery.html +0 -2
  603. nautobot/extras/templates/extras/graphqlquery_list.html +0 -1
  604. nautobot/extras/templates/extras/graphqlquery_retrieve.html +0 -97
  605. nautobot/extras/templates/extras/job_detail.html +0 -2
  606. nautobot/extras/templates/extras/jobbutton_retrieve.html +0 -2
  607. nautobot/extras/templates/extras/jobhook.html +0 -2
  608. nautobot/extras/templates/extras/jobqueue_retrieve.html +0 -2
  609. nautobot/extras/templates/extras/jobresult.html +0 -2
  610. nautobot/extras/templates/extras/metadatatype_retrieve.html +0 -2
  611. nautobot/extras/templates/extras/note.html +0 -2
  612. nautobot/extras/templates/extras/note_retrieve.html +0 -1
  613. nautobot/extras/templates/extras/object_changelog.html +0 -2
  614. nautobot/extras/templates/extras/object_notes.html +0 -2
  615. nautobot/extras/templates/extras/objectchange.html +0 -2
  616. nautobot/extras/templates/extras/objectchange_list.html +0 -3
  617. nautobot/extras/templates/extras/relationship.html +0 -1
  618. nautobot/extras/templates/extras/secret.html +0 -1
  619. nautobot/extras/templates/extras/secret_edit.html +0 -1
  620. nautobot/extras/templates/extras/secretsgroup.html +0 -2
  621. nautobot/extras/templates/extras/secretsgroup_edit.html +0 -2
  622. nautobot/extras/templates/extras/secretsgroup_retrieve.html +0 -2
  623. nautobot/extras/templates/extras/status.html +0 -2
  624. nautobot/extras/templates/extras/tag.html +0 -2
  625. nautobot/extras/templates/extras/tag_edit.html +0 -2
  626. nautobot/extras/templates/extras/tag_retrieve.html +0 -2
  627. nautobot/extras/templates/extras/team_retrieve.html +0 -2
  628. nautobot/ipam/templates/ipam/inc/prefix_header_extra_content_table.html +0 -4
  629. nautobot/ipam/templates/ipam/namespace_retrieve.html +0 -1
  630. nautobot/ipam/templates/ipam/prefix.html +0 -2
  631. nautobot/ipam/templates/ipam/prefix_edit.html +0 -1
  632. nautobot/ipam/templates/ipam/prefix_retrieve.html +0 -2
  633. nautobot/ipam/templates/ipam/rir.html +0 -2
  634. nautobot/ipam/templates/ipam/routetarget.html +0 -1
  635. nautobot/ipam/templates/ipam/service.html +0 -2
  636. nautobot/ipam/templates/ipam/service_edit.html +0 -2
  637. nautobot/ipam/templates/ipam/service_retrieve.html +0 -2
  638. nautobot/ipam/templates/ipam/vlan.html +0 -2
  639. nautobot/ipam/templates/ipam/vlan_edit.html +0 -2
  640. nautobot/ipam/templates/ipam/vlan_retrieve.html +0 -2
  641. nautobot/ipam/templates/ipam/vlangroup.html +0 -2
  642. nautobot/ipam/templates/ipam/vrf.html +0 -1
  643. nautobot/tenancy/templates/tenancy/tenant.html +0 -2
  644. nautobot/tenancy/templates/tenancy/tenant_edit.html +0 -2
  645. nautobot/tenancy/templates/tenancy/tenantgroup.html +0 -2
  646. nautobot/tenancy/templates/tenancy/tenantgroup_retrieve.html +0 -1
  647. nautobot/virtualization/templates/virtualization/clustergroup.html +0 -2
  648. nautobot/virtualization/templates/virtualization/clustertype.html +0 -2
  649. nautobot/virtualization/templates/virtualization/virtualmachine.html +0 -2
  650. nautobot/virtualization/templates/virtualization/virtualmachine_edit.html +0 -2
  651. nautobot/virtualization/templates/virtualization/virtualmachine_retrieve.html +0 -2
  652. nautobot/wireless/templates/wireless/radioprofile_retrieve.html +0 -2
  653. nautobot/wireless/templates/wireless/supporteddatarate_retrieve.html +0 -2
  654. nautobot/wireless/templates/wireless/wirelessnetwork_retrieve.html +0 -2
  655. /nautobot/dcim/templates/dcim/{cable.html → cable_retrieve.html} +0 -0
  656. /nautobot/tenancy/{filters/mixins.py → filter_mixins.py} +0 -0
  657. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/LICENSE.txt +0 -0
  658. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/NOTICE +0 -0
  659. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/WHEEL +0 -0
@@ -10,7 +10,9 @@ from nautobot.core.choices import ColorChoices
10
10
  from nautobot.dcim import choices as dcim_choices
11
11
  from nautobot.extras import choices as extras_choices
12
12
  from nautobot.ipam import choices as ipam_choices
13
+ from nautobot.load_balancers import choices as load_balancer_choices
13
14
  from nautobot.virtualization import choices as vm_choices
15
+ from nautobot.vpn import choices as vpn_choices
14
16
 
15
17
  # List of 2-tuples of (model_path, choiceset)
16
18
  # Add new mappings here as other models are supported.
@@ -34,8 +36,10 @@ STATUS_CHOICESET_MAP = {
34
36
  "ipam.Prefix": ipam_choices.PrefixStatusChoices,
35
37
  "ipam.VLAN": ipam_choices.VLANStatusChoices,
36
38
  "ipam.VRF": ipam_choices.VRFStatusChoices,
39
+ "load_balancers.LoadBalancerPoolMember": load_balancer_choices.LoadBalancerPoolMemberStatusChoices,
37
40
  "virtualization.VirtualMachine": vm_choices.VirtualMachineStatusChoices,
38
41
  "virtualization.VMInterface": vm_choices.VMInterfaceStatusChoices,
42
+ "vpn.VPNTunnel": vpn_choices.VPNTunnelStatusChoices,
39
43
  }
40
44
 
41
45
 
@@ -101,6 +105,7 @@ STATUS_DESCRIPTION_MAP = {
101
105
  # Add new mappings here as other models are supported.
102
106
  ROLE_CHOICESET_MAP = {
103
107
  "extras.ContactAssociation": extras_choices.ContactAssociationRoleChoices,
108
+ "vpn.VPNTunnelEndpoint": vpn_choices.VPNTunnelEndpointRoleChoices,
104
109
  }
105
110
 
106
111
  # Map of role name -> default hex_color used when importing color choices in `export_roles_from_choiceset()`.
@@ -110,6 +115,9 @@ ROLE_COLOR_MAP = {
110
115
  "Billing": ColorChoices.COLOR_GREEN,
111
116
  "Support": ColorChoices.COLOR_YELLOW,
112
117
  "On Site": ColorChoices.COLOR_BLACK,
118
+ "Hub": ColorChoices.COLOR_DARK_GREEN,
119
+ "Spoke": ColorChoices.COLOR_LIGHT_GREEN,
120
+ "Peer": ColorChoices.COLOR_ORANGE,
113
121
  }
114
122
 
115
123
  # Map of role name -> description used when importing role choices in `export_roles_from_choiceset()`.
@@ -118,6 +126,9 @@ ROLE_DESCRIPTION_MAP = {
118
126
  "Billing": "Unit plays a billing role",
119
127
  "Support": "Unit plays a support role",
120
128
  "On Site": "Unit plays an on site role",
129
+ "Hub": "Unit plays a Hub role",
130
+ "Spoke": "Unit plays a Spoke role",
131
+ "Peer": "Unit plays a Peer role",
121
132
  }
122
133
 
123
134
 
@@ -14,4 +14,7 @@ class Command(BaseCommand):
14
14
  dynamic_groups = DynamicGroup.objects.all()
15
15
 
16
16
  for dynamic_group in dynamic_groups:
17
- dynamic_group.update_cached_members()
17
+ try:
18
+ dynamic_group.update_cached_members()
19
+ except Exception as err:
20
+ self.stderr.write(self.style.ERROR(f"Error while refreshing {dynamic_group}: {err}"))
@@ -68,7 +68,7 @@ class Migration(migrations.Migration):
68
68
  ),
69
69
  ("name", models.CharField(max_length=255, unique=True)),
70
70
  ("model_constraints", models.JSONField(blank=True, default=dict)),
71
- ("priority", models.IntegerField(default=0)),
71
+ ("weight", models.IntegerField(default=0)),
72
72
  (
73
73
  "model_content_type",
74
74
  models.ForeignKey(
@@ -83,7 +83,7 @@ class Migration(migrations.Migration):
83
83
  options={
84
84
  "verbose_name": "Approval Workflow Definition",
85
85
  "ordering": ["name"],
86
- "unique_together": {("model_content_type", "priority")},
86
+ "unique_together": {("model_content_type", "weight")},
87
87
  },
88
88
  bases=(
89
89
  nautobot.extras.models.mixins.DynamicGroupMixin,
@@ -122,7 +122,7 @@ class Migration(migrations.Migration):
122
122
  ],
123
123
  options={
124
124
  "verbose_name": "Approval Workflow Stage",
125
- "ordering": ["approval_workflow", "approval_workflow_stage_definition__weight"],
125
+ "ordering": ["approval_workflow", "approval_workflow_stage_definition__sequence"],
126
126
  },
127
127
  bases=(
128
128
  nautobot.extras.models.mixins.DynamicGroupMixin,
@@ -179,7 +179,7 @@ class Migration(migrations.Migration):
179
179
  "_custom_field_data",
180
180
  models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
181
181
  ),
182
- ("weight", models.PositiveIntegerField()),
182
+ ("sequence", models.PositiveIntegerField()),
183
183
  ("name", models.CharField(max_length=255)),
184
184
  ("min_approvers", models.PositiveIntegerField()),
185
185
  ("denial_message", models.CharField(blank=True, max_length=255)),
@@ -202,10 +202,10 @@ class Migration(migrations.Migration):
202
202
  ],
203
203
  options={
204
204
  "verbose_name": "Approval Workflow Stage Definition",
205
- "ordering": ["approval_workflow_definition", "weight"],
205
+ "ordering": ["approval_workflow_definition", "sequence"],
206
206
  "unique_together": {
207
207
  ("approval_workflow_definition", "name"),
208
- ("approval_workflow_definition", "weight"),
208
+ ("approval_workflow_definition", "sequence"),
209
209
  },
210
210
  },
211
211
  bases=(
@@ -0,0 +1,37 @@
1
+ # Generated by Django 4.2.24 on 2025-10-04 00:03
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+ dependencies = [
8
+ ("extras", "0128_remove_job_approval_required_and_more"),
9
+ ]
10
+
11
+ operations = [
12
+ migrations.AddField(
13
+ model_name="jobresult",
14
+ name="debug_log_count",
15
+ field=models.PositiveIntegerField(blank=True, editable=False, null=True),
16
+ ),
17
+ migrations.AddField(
18
+ model_name="jobresult",
19
+ name="error_log_count",
20
+ field=models.PositiveIntegerField(blank=True, editable=False, null=True),
21
+ ),
22
+ migrations.AddField(
23
+ model_name="jobresult",
24
+ name="info_log_count",
25
+ field=models.PositiveIntegerField(blank=True, editable=False, null=True),
26
+ ),
27
+ migrations.AddField(
28
+ model_name="jobresult",
29
+ name="success_log_count",
30
+ field=models.PositiveIntegerField(blank=True, editable=False, null=True),
31
+ ),
32
+ migrations.AddField(
33
+ model_name="jobresult",
34
+ name="warning_log_count",
35
+ field=models.PositiveIntegerField(blank=True, editable=False, null=True),
36
+ ),
37
+ ]
@@ -0,0 +1,42 @@
1
+ """Migration to populate job_results.*_log_counts on existing JobResults."""
2
+
3
+ from django.db import migrations
4
+ from django.db.models import Count, Q
5
+
6
+
7
+ def _populate_log_counts(apps, *_):
8
+ JobResult = apps.get_model("extras", "JobResult")
9
+ job_results_missing_counts = JobResult.objects.filter(
10
+ Q(debug_log_count=None)
11
+ | Q(success_log_count=None)
12
+ | Q(info_log_count=None)
13
+ | Q(warning_log_count=None)
14
+ | Q(error_log_count=None)
15
+ )
16
+ for job_result in job_results_missing_counts:
17
+ db_log_counts = job_result.job_log_entries.aggregate(
18
+ debug_log_count=Count("pk", filter=Q(log_level="debug")),
19
+ success_log_count=Count("pk", filter=Q(log_level="success")),
20
+ info_log_count=Count("pk", filter=Q(log_level="info")),
21
+ warning_log_count=Count("pk", filter=Q(log_level="warning")),
22
+ error_log_count=Count(
23
+ "pk",
24
+ filter=Q(log_level__in=["failure", "error", "critical"]),
25
+ ),
26
+ )
27
+ job_result.debug_log_count = db_log_counts["debug_log_count"]
28
+ job_result.success_log_count = db_log_counts["success_log_count"]
29
+ job_result.info_log_count = db_log_counts["info_log_count"]
30
+ job_result.warning_log_count = db_log_counts["warning_log_count"]
31
+ job_result.error_log_count = db_log_counts["error_log_count"]
32
+ job_result.save()
33
+
34
+
35
+ class Migration(migrations.Migration):
36
+ dependencies = [
37
+ ("extras", "0129_jobresult_debug_log_count_jobresult_error_log_count_and_more"),
38
+ ]
39
+
40
+ operations = [
41
+ migrations.RunPython(code=_populate_log_counts, reverse_code=migrations.RunPython.noop),
42
+ ]
@@ -0,0 +1,18 @@
1
+ # Generated by Django 4.2.24 on 2025-09-26 19:15
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+ dependencies = [
8
+ ("dcim", "0074_alter_rack_u_height"),
9
+ ("extras", "0130_jobresult_generate_log_entry_counts"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name="configcontext",
15
+ name="device_families",
16
+ field=models.ManyToManyField(blank=True, related_name="+", to="dcim.devicefamily"),
17
+ ),
18
+ ]
@@ -43,7 +43,7 @@ from .models import (
43
43
  from .relationships import Relationship, RelationshipAssociation, RelationshipModel
44
44
  from .roles import Role, RoleField
45
45
  from .secrets import Secret, SecretsGroup, SecretsGroupAssociation
46
- from .statuses import Status, StatusField, StatusModel
46
+ from .statuses import Status, StatusField
47
47
  from .tags import Tag, TaggedItem
48
48
 
49
49
  __all__ = (
@@ -103,7 +103,6 @@ __all__ = (
103
103
  "StaticGroupAssociation",
104
104
  "Status",
105
105
  "StatusField",
106
- "StatusModel",
107
106
  "Tag",
108
107
  "TaggedItem",
109
108
  "Team",
@@ -4,7 +4,7 @@ from django.conf import settings
4
4
  from django.contrib.auth.models import Group
5
5
  from django.contrib.contenttypes.fields import GenericForeignKey
6
6
  from django.contrib.contenttypes.models import ContentType
7
- from django.core.exceptions import ValidationError
7
+ from django.core.exceptions import FieldError, ValidationError
8
8
  from django.db import models
9
9
  from django.utils import timezone
10
10
 
@@ -28,8 +28,8 @@ class ApprovalWorkflowDefinitionManager(BaseManager.from_queryset(RestrictedQuer
28
28
  """
29
29
  content_type = ContentType.objects.get_for_model(model_instance)
30
30
 
31
- # Get all workflow definitions for this content type, ordered by priority (lowest first = highest priority)
32
- workflow_definitions = self.get_queryset().filter(model_content_type=content_type).order_by("priority")
31
+ # Get all workflow definitions for this content type, ordered by weight (highest wins)
32
+ workflow_definitions = self.get_queryset().filter(model_content_type=content_type).order_by("-weight")
33
33
 
34
34
  for workflow_definition in workflow_definitions:
35
35
  if not workflow_definition.model_constraints:
@@ -77,11 +77,12 @@ class ApprovalWorkflowDefinition(PrimaryModel):
77
77
  default=dict,
78
78
  help_text="Constraints to filter the objects that can be approved using this workflow.",
79
79
  )
80
- priority = models.IntegerField(
81
- default=0, help_text="Determines workflow relevance when multiple apply. Lower values indicate higher priority."
80
+ weight = models.IntegerField(
81
+ default=0, help_text="Determines workflow relevance when multiple apply. Higher weight wins."
82
82
  )
83
83
  documentation_static_path = "docs/user-guide/platform-functionality/approval-workflow.html"
84
84
  is_dynamic_group_associable = False
85
+ is_data_compliance_model = False
85
86
  objects = ApprovalWorkflowDefinitionManager()
86
87
  is_version_controlled = False
87
88
 
@@ -90,12 +91,22 @@ class ApprovalWorkflowDefinition(PrimaryModel):
90
91
 
91
92
  verbose_name = "Approval Workflow Definition"
92
93
  ordering = ["name"]
93
- unique_together = [["model_content_type", "priority"]]
94
+ unique_together = [["model_content_type", "weight"]]
94
95
 
95
96
  def __str__(self):
96
97
  """Stringify instance."""
97
98
  return self.name
98
99
 
100
+ def clean(self):
101
+ super().clean()
102
+ model_class = self.model_content_type.model_class()
103
+ if model_class is None:
104
+ raise ValidationError({"model_content_type": "Couldn't find corresponding model class. Is it installed?"})
105
+ try:
106
+ model_class.objects.filter(**self.model_constraints)
107
+ except (FieldError, AttributeError) as exc:
108
+ raise ValidationError({"model_constraints": f"Invalid query filter: {exc}"})
109
+
99
110
 
100
111
  @extras_features(
101
112
  "custom_links",
@@ -114,12 +125,13 @@ class ApprovalWorkflowStageDefinition(OrganizationalModel):
114
125
  on_delete=models.CASCADE,
115
126
  help_text="Approval workflow definition to which this stage belongs.",
116
127
  )
117
- weight = models.PositiveIntegerField(
118
- help_text="The weight dictates the order in which this stage will need to be approved. The lower the number, the earlier it will be.",
128
+ sequence = models.PositiveIntegerField(
129
+ help_text="The sequence dictates the order in which this stage will need to be approved. The lower the number, the earlier it will be.",
119
130
  )
120
131
  name = models.CharField(max_length=CHARFIELD_MAX_LENGTH)
121
132
  min_approvers = models.PositiveIntegerField(
122
- help_text="Number of minimum approvers required to approve this stage.",
133
+ verbose_name="Minimum approvers",
134
+ help_text="Minimum number of approvers required to approve this stage.",
123
135
  )
124
136
  denial_message = models.CharField(
125
137
  max_length=CHARFIELD_MAX_LENGTH, blank=True, help_text="Message to show when the stage is denied."
@@ -128,18 +140,20 @@ class ApprovalWorkflowStageDefinition(OrganizationalModel):
128
140
  to=Group,
129
141
  related_name="approval_workflow_stage_definitions",
130
142
  verbose_name="Group",
131
- help_text="Group of users who are eligible to approve this stage.",
143
+ help_text="Group of users who are eligible to approve this stage. Only admin users can create new groups.",
132
144
  on_delete=models.PROTECT,
133
145
  )
134
146
  documentation_static_path = "docs/user-guide/platform-functionality/approval-workflow.html"
135
147
  is_version_controlled = False
136
148
 
149
+ is_data_compliance_model = False
150
+
137
151
  class Meta:
138
152
  """Meta class for ApprovalWorkflowStage."""
139
153
 
140
154
  verbose_name = "Approval Workflow Stage Definition"
141
- unique_together = [["approval_workflow_definition", "name"], ["approval_workflow_definition", "weight"]]
142
- ordering = ["approval_workflow_definition", "weight"]
155
+ unique_together = [["approval_workflow_definition", "name"], ["approval_workflow_definition", "sequence"]]
156
+ ordering = ["approval_workflow_definition", "sequence"]
143
157
 
144
158
  def __str__(self):
145
159
  """Stringify instance."""
@@ -195,6 +209,7 @@ class ApprovalWorkflow(OrganizationalModel):
195
209
  user_name = models.CharField(max_length=150, editable=False, db_index=True)
196
210
  documentation_static_path = "docs/user-guide/platform-functionality/approval-workflow.html"
197
211
 
212
+ is_data_compliance_model = False
198
213
  is_version_controlled = False
199
214
 
200
215
  class Meta:
@@ -225,7 +240,7 @@ class ApprovalWorkflow(OrganizationalModel):
225
240
  """
226
241
  first_nonapproved_stage = (
227
242
  self.approval_workflow_stages.exclude(state=ApprovalWorkflowStateChoices.APPROVED)
228
- .order_by("approval_workflow_stage_definition__weight")
243
+ .order_by("approval_workflow_stage_definition__sequence")
229
244
  .first()
230
245
  )
231
246
  return first_nonapproved_stage
@@ -317,12 +332,14 @@ class ApprovalWorkflowStage(OrganizationalModel):
317
332
  documentation_static_path = "docs/user-guide/platform-functionality/approval-workflow.html"
318
333
  is_version_controlled = False
319
334
 
335
+ is_data_compliance_model = False
336
+
320
337
  class Meta:
321
338
  """Meta class for ApprovalWorkflowStage."""
322
339
 
323
340
  verbose_name = "Approval Workflow Stage"
324
341
  unique_together = [["approval_workflow", "approval_workflow_stage_definition"]]
325
- ordering = ["approval_workflow", "approval_workflow_stage_definition__weight"]
342
+ ordering = ["approval_workflow", "approval_workflow_stage_definition__sequence"]
326
343
 
327
344
  def __str__(self):
328
345
  """Stringify instance."""
@@ -506,6 +523,8 @@ class ApprovalWorkflowStageResponse(BaseModel):
506
523
  documentation_static_path = "docs/user-guide/platform-functionality/approval-workflow.html"
507
524
  is_version_controlled = False
508
525
 
526
+ is_data_compliance_model = False
527
+
509
528
  class Meta:
510
529
  """Meta class for ApprovalWorkflowStageResponse."""
511
530
 
@@ -154,6 +154,10 @@ class ObjectChange(BaseModel):
154
154
  def __str__(self):
155
155
  return f"{self.changed_object_type} {self.object_repr} {self.get_action_display().lower()} by {self.user_name}"
156
156
 
157
+ @property
158
+ def page_title(self):
159
+ return f"{self.object_repr} - {self.time.strftime('%Y-%m-%d %H:%M')}"
160
+
157
161
  def save(self, *args, **kwargs):
158
162
  # Record the user's name and the object's representation as static strings
159
163
  if not self.user_name:
@@ -20,6 +20,7 @@ class ContactTeamSharedBase(PrimaryModel):
20
20
 
21
21
  comments = models.TextField(blank=True)
22
22
  is_contact_associable_model = False
23
+ is_data_compliance_model = False
23
24
 
24
25
  class Meta:
25
26
  abstract = True
@@ -94,6 +95,7 @@ class ContactAssociation(OrganizationalModel):
94
95
  is_contact_associable_model = False
95
96
  is_dynamic_group_associable_model = False
96
97
  is_saved_view_model = False
98
+ is_data_compliance_model = False
97
99
 
98
100
  class Meta:
99
101
  unique_together = (
@@ -76,6 +76,7 @@ class DynamicGroup(PrimaryModel):
76
76
 
77
77
  objects = BaseManager.from_queryset(DynamicGroupQuerySet)()
78
78
  is_dynamic_group_associable_model = False
79
+ is_data_compliance_model = False
79
80
 
80
81
  clone_fields = ["content_type", "group_type", "filter", "tenant"]
81
82
 
@@ -628,6 +629,10 @@ class DynamicGroup(PrimaryModel):
628
629
  else:
629
630
  # Validate against the filterset's internal form validation.
630
631
  filterset = self.filterset_class(self.filter) # pylint: disable=not-callable
632
+ # TODO: the below is more generous than one might expect. For example, passing a list of strings ["foo"]
633
+ # to a (single-input) CharFilter will quietly normalize the list to a string '["foo"]' instead of reporting
634
+ # any failure of is_valid(). We've had cases of such "should be invalid but isn't caught" DynamicGroups causing
635
+ # exceptions when trying to evaluate their membership; it would be good to be stricter here instead!
631
636
  if not filterset.is_valid():
632
637
  raise ValidationError(filterset.errors)
633
638
 
@@ -661,6 +666,22 @@ class DynamicGroup(PrimaryModel):
661
666
 
662
667
  # TODO limit most changes to self.group_type as well.
663
668
 
669
+ def save(self, *args, update_cached_members=True, **kwargs):
670
+ """
671
+ Save the DynamicGroup record.
672
+
673
+ Args:
674
+ update_cached_members (bool): If True, (re)calculate the cached members set of the related group(s) immediately.
675
+ Note that this is potentially quite expensive if there will be a large change in the members set!
676
+ If False (recommended), you can call `self.update_cached_members()` explicitly when ready.
677
+ """
678
+ super().save(*args, **kwargs)
679
+
680
+ if update_cached_members:
681
+ self.update_cached_members()
682
+ for ancestor in self.get_ancestors():
683
+ ancestor.update_cached_members()
684
+
664
685
  def _generate_query_for_filter(self, filter_field, value):
665
686
  """
666
687
  Return a `Q` object generated from a `filter_field` and `value`.
@@ -703,9 +724,12 @@ class DynamicGroup(PrimaryModel):
703
724
  # "ams02"]}`, the value being a list of location names (`["ams01", "ams02"]`).
704
725
  if value and isinstance(value, list) and isinstance(value[0], str) and not is_uuid(value[0]):
705
726
  model_field = django_filters.utils.get_model_field(self._model, filter_field.field_name)
706
- related_model = model_field.related_model
707
- lookup_kwargs = {f"{to_field_name}__in": value}
708
- gq_value = related_model.objects.filter(**lookup_kwargs)
727
+ if model_field is None:
728
+ gq_value = value
729
+ else:
730
+ related_model = model_field.related_model
731
+ lookup_kwargs = {f"{to_field_name}__in": value}
732
+ gq_value = related_model.objects.filter(**lookup_kwargs)
709
733
  else:
710
734
  gq_value = value
711
735
  query |= filter_field.generate_query(gq_value)
@@ -1172,12 +1196,26 @@ class DynamicGroupMembership(BaseModel):
1172
1196
  if self.group in self.parent_group.get_ancestors():
1173
1197
  raise ValidationError({"group": "Cannot add ancestor as a child"})
1174
1198
 
1175
- def save(self, *args, **kwargs):
1199
+ def save(self, *args, update_cached_members=True, **kwargs):
1200
+ """
1201
+ Save the DynamicGroupMembership record.
1202
+
1203
+ Args:
1204
+ update_cached_members (bool): If True, (re)calculate the cached members set of the related group(s) immediately.
1205
+ Note that this is potentially quite expensive if there will be a large change in the members set!
1206
+ If False (recommended), you can call `self.parent_group.update_cached_members()` explicitly when ready.
1207
+ """
1176
1208
  # For backwards compatibility
1177
1209
  if self.parent_group.group_type == DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER and not self.parent_group.filter:
1178
1210
  self.parent_group.group_type = DynamicGroupTypeChoices.TYPE_DYNAMIC_SET
1179
1211
  self.parent_group.save()
1180
- return super().save(*args, **kwargs)
1212
+
1213
+ super().save(*args, **kwargs)
1214
+
1215
+ if update_cached_members:
1216
+ self.parent_group.update_cached_members()
1217
+ for ancestor in self.parent_group.get_ancestors():
1218
+ ancestor.update_cached_members()
1181
1219
 
1182
1220
 
1183
1221
  class StaticGroupAssociationManager(BaseManager.from_queryset(RestrictedQuerySet)):
@@ -1218,6 +1256,7 @@ class StaticGroupAssociation(OrganizationalModel):
1218
1256
  is_contact_associable_model = False
1219
1257
  is_dynamic_group_associable_model = False
1220
1258
  is_saved_view_model = False
1259
+ is_data_compliance_model = False
1221
1260
 
1222
1261
  class Meta:
1223
1262
  unique_together = [["dynamic_group", "associated_object_type", "associated_object_id"]]
@@ -15,7 +15,7 @@ from django.contrib.contenttypes.models import ContentType
15
15
  from django.core.exceptions import ValidationError
16
16
  from django.core.validators import MinValueValidator
17
17
  from django.db import models, transaction
18
- from django.db.models import ProtectedError, signals
18
+ from django.db.models import Count, ProtectedError, Q, signals
19
19
  from django.utils import timezone
20
20
  from django.utils.functional import cached_property
21
21
  from django_celery_beat.clockedschedule import clocked
@@ -252,6 +252,7 @@ class Job(PrimaryModel):
252
252
  help_text="If set, the configured value will remain even if the underlying Job source code changes",
253
253
  )
254
254
  objects = BaseManager.from_queryset(JobQuerySet)()
255
+ is_data_compliance_model = False
255
256
 
256
257
  documentation_static_path = "docs/user-guide/platform-functionality/jobs/models.html"
257
258
 
@@ -439,6 +440,8 @@ class JobHook(OrganizationalModel):
439
440
  type_delete = models.BooleanField(default=False, help_text="Call this job hook when a matching object is deleted.")
440
441
  type_update = models.BooleanField(default=False, help_text="Call this job hook when a matching object is updated.")
441
442
 
443
+ is_data_compliance_model = False
444
+
442
445
  documentation_static_path = "docs/user-guide/platform-functionality/jobs/jobhook.html"
443
446
 
444
447
  class Meta:
@@ -529,6 +532,7 @@ class JobLogEntry(BaseModel):
529
532
  absolute_url = models.CharField(max_length=JOB_LOG_MAX_ABSOLUTE_URL_LENGTH, blank=True, default="")
530
533
 
531
534
  is_metadata_associable_model = False
535
+ is_data_compliance_model = False
532
536
 
533
537
  documentation_static_path = "docs/user-guide/platform-functionality/jobs/models.html"
534
538
  hide_in_diff_view = True
@@ -580,6 +584,7 @@ class JobQueue(PrimaryModel):
580
584
  )
581
585
 
582
586
  documentation_static_path = "docs/user-guide/platform-functionality/jobs/jobqueue.html"
587
+ is_data_compliance_model = False
583
588
 
584
589
  class Meta:
585
590
  ordering = ["name"]
@@ -610,6 +615,7 @@ class JobQueueAssignment(BaseModel):
610
615
  job = models.ForeignKey(Job, on_delete=models.CASCADE, related_name="job_queue_assignments")
611
616
  job_queue = models.ForeignKey(JobQueue, on_delete=models.CASCADE, related_name="job_assignments")
612
617
  is_metadata_associable_model = False
618
+ is_data_compliance_model = False
613
619
 
614
620
  class Meta:
615
621
  unique_together = ["job", "job_queue"]
@@ -678,11 +684,17 @@ class JobResult(SavedViewMixin, BaseModel, CustomFieldModel):
678
684
  traceback = models.TextField(blank=True, null=True) # noqa: DJ001 # django-nullable-model-string-field -- TODO: can we remove null=True?
679
685
  meta = models.JSONField(null=True, default=None, editable=False)
680
686
  scheduled_job = models.ForeignKey(to="extras.ScheduledJob", on_delete=models.SET_NULL, null=True, blank=True)
687
+ debug_log_count = models.PositiveIntegerField(blank=True, null=True, editable=False)
688
+ success_log_count = models.PositiveIntegerField(blank=True, null=True, editable=False)
689
+ info_log_count = models.PositiveIntegerField(blank=True, null=True, editable=False)
690
+ warning_log_count = models.PositiveIntegerField(blank=True, null=True, editable=False)
691
+ error_log_count = models.PositiveIntegerField(blank=True, null=True, editable=False)
681
692
 
682
693
  objects = JobResultManager()
683
694
 
684
695
  documentation_static_path = "docs/user-guide/platform-functionality/jobs/models.html"
685
696
  hide_in_diff_view = True
697
+ is_data_compliance_model = False
686
698
 
687
699
  def __init__(self, *args, **kwargs):
688
700
  super().__init__(*args, **kwargs)
@@ -762,6 +774,30 @@ class JobResult(SavedViewMixin, BaseModel, CustomFieldModel):
762
774
 
763
775
  set_status.alters_data = True
764
776
 
777
+ def count_logs_by_level(self):
778
+ """Helper method to count JobLogEntries after a Job is run, or update these values when missing or changed."""
779
+ db_log_counts = self.job_log_entries.aggregate(
780
+ debug_log_count=Count("pk", filter=Q(log_level=LogLevelChoices.LOG_DEBUG)),
781
+ success_log_count=Count("pk", filter=Q(log_level=LogLevelChoices.LOG_SUCCESS)),
782
+ info_log_count=Count("pk", filter=Q(log_level=LogLevelChoices.LOG_INFO)),
783
+ warning_log_count=Count("pk", filter=Q(log_level=LogLevelChoices.LOG_WARNING)),
784
+ error_log_count=Count(
785
+ "pk",
786
+ filter=Q(
787
+ log_level__in=[
788
+ LogLevelChoices.LOG_FAILURE,
789
+ LogLevelChoices.LOG_ERROR,
790
+ LogLevelChoices.LOG_CRITICAL,
791
+ ]
792
+ ),
793
+ ),
794
+ )
795
+ self.debug_log_count = db_log_counts["debug_log_count"]
796
+ self.success_log_count = db_log_counts["success_log_count"]
797
+ self.info_log_count = db_log_counts["info_log_count"]
798
+ self.warning_log_count = db_log_counts["warning_log_count"]
799
+ self.error_log_count = db_log_counts["error_log_count"]
800
+
765
801
  @classmethod
766
802
  def execute_job(cls, *args, **kwargs):
767
803
  """
@@ -1006,6 +1042,18 @@ class JobResult(SavedViewMixin, BaseModel, CustomFieldModel):
1006
1042
 
1007
1043
  log.alters_data = True
1008
1044
 
1045
+ def save(self, *args, **kwargs):
1046
+ """When a JobResult is saved and in a terminal state, store missing log counts for summary."""
1047
+ if self.status in JobResultStatusChoices.READY_STATES and None in [
1048
+ self.debug_log_count,
1049
+ self.info_log_count,
1050
+ self.success_log_count,
1051
+ self.warning_log_count,
1052
+ self.error_log_count,
1053
+ ]:
1054
+ self.count_logs_by_level()
1055
+ super().save(*args, **kwargs)
1056
+
1009
1057
 
1010
1058
  #
1011
1059
  # Job Button
@@ -1055,10 +1103,18 @@ class JobButton(ContactMixin, ChangeLoggedModel, DynamicGroupsModelMixin, NotesM
1055
1103
  )
1056
1104
 
1057
1105
  documentation_static_path = "docs/user-guide/platform-functionality/jobs/jobbutton.html"
1106
+ is_data_compliance_model = False
1058
1107
 
1059
1108
  class Meta:
1060
1109
  ordering = ["group_name", "weight", "name"]
1061
1110
 
1111
+ @property
1112
+ def button_class_css_class(self):
1113
+ """Map self.button_class database value to the correct CSS class for buttons."""
1114
+ if self.button_class == ButtonClassChoices.CLASS_DEFAULT:
1115
+ return "secondary"
1116
+ return self.button_class
1117
+
1062
1118
  def __str__(self):
1063
1119
  return self.name
1064
1120
 
@@ -1081,6 +1137,7 @@ class ScheduledJobs(models.Model):
1081
1137
  last_update = models.DateTimeField(null=False)
1082
1138
 
1083
1139
  objects = ScheduledJobsManager()
1140
+ is_data_compliance_model = False
1084
1141
 
1085
1142
  def __str__(self):
1086
1143
  return str(self.ident)
@@ -1218,6 +1275,7 @@ class ScheduledJob(ApprovableModelMixin, BaseModel):
1218
1275
  no_changes = False
1219
1276
 
1220
1277
  documentation_static_path = "docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html"
1278
+ is_data_compliance_model = False
1221
1279
 
1222
1280
  def __str__(self):
1223
1281
  return f"{self.name}: {self.interval}"
@@ -1355,7 +1413,6 @@ class ScheduledJob(ApprovableModelMixin, BaseModel):
1355
1413
  job_queue: Optional[JobQueue] = None,
1356
1414
  task_queue: Optional[str] = None, # deprecated!
1357
1415
  ignore_singleton_lock: bool = False,
1358
- validated_save: bool = True,
1359
1416
  **job_kwargs,
1360
1417
  ):
1361
1418
  """
@@ -1445,8 +1502,7 @@ class ScheduledJob(ApprovableModelMixin, BaseModel):
1445
1502
  crontab=crontab,
1446
1503
  job_queue=job_queue,
1447
1504
  )
1448
- if validated_save:
1449
- scheduled_job.validated_save()
1505
+ scheduled_job.validated_save()
1450
1506
  return scheduled_job
1451
1507
 
1452
1508
  create_schedule.__func__.alters_data = True