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
@@ -4,7 +4,6 @@ import contextlib
4
4
  from dataclasses import dataclass
5
5
  from enum import Enum
6
6
  import logging
7
- from typing import Any
8
7
  import uuid
9
8
 
10
9
  from django.contrib.contenttypes.models import ContentType
@@ -43,10 +42,11 @@ from nautobot.core.templatetags.helpers import (
43
42
  from nautobot.core.ui.choices import LayoutChoices, SectionChoices
44
43
  from nautobot.core.ui.echarts import EChartsBase
45
44
  from nautobot.core.ui.utils import render_component_template
46
- from nautobot.core.utils.lookup import get_filterset_for_model, get_route_for_model
45
+ from nautobot.core.utils.lookup import get_filterset_for_model, get_route_for_model, get_view_for_model
47
46
  from nautobot.core.utils.permissions import get_permission_for_model
48
47
  from nautobot.core.views.paginator import EnhancedPaginator, get_paginate_count
49
48
  from nautobot.core.views.utils import get_obj_from_context
49
+ from nautobot.data_validation.tables import DataComplianceTable
50
50
  from nautobot.dcim.models import Rack
51
51
  from nautobot.extras.choices import CustomFieldTypeChoices
52
52
  from nautobot.extras.tables import AssociatedContactsTable, DynamicGroupTable, ObjectMetadataTable
@@ -101,6 +101,7 @@ class ObjectDetailContent:
101
101
  _ObjectDetailContactsTab(),
102
102
  _ObjectDetailGroupsTab(),
103
103
  _ObjectDetailMetadataTab(),
104
+ _ObjectDetailDataComplianceTab(),
104
105
  ]
105
106
  if extra_tabs is not None:
106
107
  tabs.extend(extra_tabs)
@@ -245,6 +246,9 @@ class Button(Component):
245
246
  """
246
247
  if self.link_name and self.link_includes_pk:
247
248
  obj = get_obj_from_context(context, self.context_object_key)
249
+ if not obj:
250
+ logger.warning("Button %s has no object to link to", self.label)
251
+ return None
248
252
  return reverse(self.link_name, kwargs={"pk": obj.pk})
249
253
  elif self.link_name:
250
254
  return reverse(self.link_name)
@@ -262,8 +266,11 @@ class Button(Component):
262
266
  }
263
267
 
264
268
  def should_render(self, context: Context):
269
+ # Only show if the user has the permission, which is enforce in super.
265
270
  if not super().should_render(context):
266
271
  return False
272
+ if self.render_on_tab_id == "__all__":
273
+ return True
267
274
  return context.get("active_tab", "main") == self.render_on_tab_id
268
275
 
269
276
  def render(self, context: Context):
@@ -305,6 +312,7 @@ class FormButton(Button):
305
312
  self,
306
313
  form_id: str,
307
314
  link_name: str,
315
+ render_on_tab_id="__all__",
308
316
  template_path="components/button/formbutton.html",
309
317
  **kwargs,
310
318
  ):
@@ -326,7 +334,7 @@ class FormButton(Button):
326
334
  if not self.form_id:
327
335
  raise ValueError("FormButton requires 'form_id' to be set in ObjectsTablePanel.")
328
336
 
329
- super().__init__(link_name=link_name, template_path=template_path, **kwargs)
337
+ super().__init__(link_name=link_name, render_on_tab_id=render_on_tab_id, template_path=template_path, **kwargs)
330
338
 
331
339
  def get_extra_context(self, context: Context):
332
340
  return {
@@ -377,8 +385,9 @@ class Tab(Component):
377
385
  WEIGHT_CONTACTS_TAB = 300
378
386
  WEIGHT_GROUPS_TAB = 400
379
387
  WEIGHT_METADATA_TAB = 500
380
- WEIGHT_NOTES_TAB = 600 # reserved, not yet using this framework
381
- WEIGHT_CHANGELOG_TAB = 700 # reserved, not yet using this framework
388
+ WEIGHT_DATACOMPLIANCE_TAB = 600
389
+ WEIGHT_NOTES_TAB = 700 # reserved, not yet using this framework
390
+ WEIGHT_CHANGELOG_TAB = 800 # reserved, not yet using this framework
382
391
 
383
392
  def panels_for_section(self, section):
384
393
  """
@@ -540,6 +549,7 @@ class Panel(Component):
540
549
  self,
541
550
  *,
542
551
  label="",
552
+ css_class="default",
543
553
  section=SectionChoices.FULL_WIDTH,
544
554
  body_id=None,
545
555
  body_content_template_path=None,
@@ -554,6 +564,7 @@ class Panel(Component):
554
564
 
555
565
  Args:
556
566
  label (str): Label to display for this panel. Optional; if an empty string, the panel will have no label.
567
+ css_class (str): Panel variant to render as, e.g. "default", "warning", "info".
557
568
  section (str): One of the [`SectionChoices`](./ui.md#nautobot.apps.ui.SectionChoices) values, indicating the layout section this Panel belongs to.
558
569
  body_id (str): HTML element `id` to attach to the rendered body wrapper of the panel.
559
570
  body_content_template_path (str): Template path to render the content contained *within* the panel body.
@@ -565,6 +576,7 @@ class Panel(Component):
565
576
  (a `div` or `table`) as well as its contents. Generally you won't override this as a user.
566
577
  """
567
578
  self.label = label
579
+ self.css_class = css_class
568
580
  self.section = section
569
581
  self.body_id = body_id
570
582
  self.body_content_template_path = body_content_template_path
@@ -593,6 +605,7 @@ class Panel(Component):
593
605
  self.template_path,
594
606
  context,
595
607
  label=self.render_label(context),
608
+ css_class=self.css_class,
596
609
  header_extra_content=self.render_header_extra_content(context),
597
610
  body=self.render_body(context),
598
611
  footer_content=self.render_footer_content(context),
@@ -758,6 +771,7 @@ class ObjectsTablePanel(Panel):
758
771
  select_related_fields=None,
759
772
  prefetch_related_fields=None,
760
773
  order_by_fields=None,
774
+ # TODO: Is `table_title` redundant with the base Panel's `label`?
761
775
  table_title=None,
762
776
  max_display_count=None,
763
777
  paginate=True,
@@ -768,6 +782,8 @@ class ObjectsTablePanel(Panel):
768
782
  add_permissions=None,
769
783
  hide_hierarchy_ui=False,
770
784
  related_field_name=None,
785
+ related_list_url_name=None,
786
+ enable_related_link=True,
771
787
  enable_bulk_actions=False,
772
788
  tab_id=None,
773
789
  body_wrapper_template_path="components/panel/body_wrapper_table.html",
@@ -819,6 +835,11 @@ class ObjectsTablePanel(Panel):
819
835
  hide_hierarchy_ui (bool, optional): Don't display hierarchy-based indentation of tree models in this table
820
836
  related_field_name (str, optional): The name of the filter/form field for the related model that links back
821
837
  to the base model. Defaults to the same as `table_filter` if unset. Used to populate URLs.
838
+ related_list_url_name (str, optional): The URL used to generate the list button URL for the related model.
839
+ If not provided, the default table's model `list` route is used.
840
+ This can be useful when the related model is a many-to-many relationship with a custom through table.
841
+ enable_related_link (bool, optional): If True, the badge on the related model will be a link to the related model list view.
842
+ When False, the badge will still show the count of the related model, but will not be a link.
822
843
  enable_bulk_actions (bool, optional): Show the pk toggle columns on the table if the user has the
823
844
  appropriate permissions.
824
845
  tab_id (str, optional): The ID of the tab this panel belongs to. Used to append to a `return_url` when
@@ -869,6 +890,8 @@ class ObjectsTablePanel(Panel):
869
890
  self.add_permissions = add_permissions or []
870
891
  self.hide_hierarchy_ui = hide_hierarchy_ui
871
892
  self.related_field_name = related_field_name
893
+ self.related_list_url_name = related_list_url_name
894
+ self.enable_related_link = enable_related_link
872
895
  self.enable_bulk_actions = enable_bulk_actions
873
896
  self.tab_id = tab_id
874
897
  self.footer_buttons = footer_buttons
@@ -895,7 +918,12 @@ class ObjectsTablePanel(Panel):
895
918
  related_field_name = self.related_field_name or self.table_filter or obj._meta.model_name
896
919
  return_url = context.get("return_url", obj.get_absolute_url())
897
920
  if self.tab_id:
898
- return_url += f"?tab={self.tab_id}"
921
+ try:
922
+ # Check to see if the this is a NautobotUIViewset action
923
+ view = get_view_for_model(obj._meta.model)
924
+ return_url += getattr(view, self.tab_id).url_path + "/"
925
+ except AttributeError:
926
+ return_url += f"?tab={self.tab_id}"
899
927
 
900
928
  if self.add_button_route is not None:
901
929
  add_permissions = self.add_permissions
@@ -1024,29 +1052,35 @@ class ObjectsTablePanel(Panel):
1024
1052
  body_content_table_model = body_content_table.Meta.model
1025
1053
  related_field_name = self.related_field_name or self.table_filter or obj._meta.model_name
1026
1054
 
1027
- list_url = getattr(self.table_class, "list_url", None)
1028
- if not list_url:
1029
- list_url = get_route_for_model(body_content_table_model, "list")
1055
+ body_content_table_list_url = None
1056
+ body_content_table_add_url = self._get_table_add_url(context)
1057
+ table_title = self.table_title or body_content_table_model._meta.verbose_name_plural
1030
1058
 
1031
- try:
1032
- list_route = reverse(list_url)
1033
- except NoReverseMatch:
1034
- list_route = None
1059
+ if self.enable_related_link:
1060
+ list_url = self.related_list_url_name or getattr(self.table_class, "list_url", None)
1061
+ if not list_url:
1062
+ list_url = get_route_for_model(body_content_table_model, "list")
1035
1063
 
1036
- if list_route:
1037
- body_content_table_list_url = f"{list_route}?{related_field_name}={obj.pk}"
1038
- else:
1039
- body_content_table_list_url = None
1064
+ try:
1065
+ list_route = reverse(list_url)
1066
+ except NoReverseMatch:
1067
+ logger.warning(
1068
+ f"Unable to determine a valid list URL for ObjectsTablePanel `{table_title}`"
1069
+ f" related to `{body_content_table_model.__name__}` with `{list_url}`."
1070
+ " If the related object is using a through table, consider setting the `related_list_url_name`"
1071
+ " parameter or disabling the related link via 'enable_related_link=False'."
1072
+ )
1073
+ list_route = None
1040
1074
 
1041
- body_content_table_add_url = self._get_table_add_url(context)
1042
- body_content_table_verbose_name_plural = self.table_title or body_content_table_model._meta.verbose_name_plural
1075
+ if list_route:
1076
+ body_content_table_list_url = f"{list_route}?{related_field_name}={obj.pk}"
1043
1077
 
1044
1078
  return {
1045
1079
  "body_content_table": body_content_table,
1046
1080
  "body_content_table_add_url": body_content_table_add_url,
1047
1081
  "body_content_table_list_url": body_content_table_list_url,
1048
1082
  "body_content_table_verbose_name": body_content_table_model._meta.verbose_name,
1049
- "body_content_table_verbose_name_plural": body_content_table_verbose_name_plural,
1083
+ "body_content_table_verbose_name_plural": table_title,
1050
1084
  "footer_buttons": self.footer_buttons,
1051
1085
  "form_id": self.form_id,
1052
1086
  "more_queryset_count": more_queryset_count,
@@ -1299,28 +1333,13 @@ class EChartsPanel(Panel, EChartsBase):
1299
1333
  self.width = width
1300
1334
  self.height = height
1301
1335
  self.chart_container_id = chart_container_id
1336
+ self.body_id = (
1337
+ self.chart_container_id or f"{slugify('echart-' + chart_kwargs.get('header', ''))}-{uuid.uuid4().hex[:8]}"
1338
+ )
1302
1339
 
1303
- super().__init__(body_wrapper_template_path=body_wrapper_template_path, **kwargs)
1340
+ super().__init__(body_wrapper_template_path=body_wrapper_template_path, body_id=self.body_id, **kwargs)
1304
1341
  EChartsBase.__init__(self, **chart_kwargs)
1305
1342
 
1306
- def get_data(self, context: Context) -> dict[str, Any] | None:
1307
- """Get the data for chart.
1308
-
1309
- Args:
1310
- context (Context): The template or request context.
1311
-
1312
- Returns:
1313
- dict[str, Any] | None:
1314
- - A dictionary in internal chart format, e.g.:
1315
- {"x": [...], "series": [{"name": str, "data": [...]}]}
1316
- - A nested dictionary of series, e.g.:
1317
- {"Series1": {"x1": val1, "x2": val2}, ...}
1318
- - `None` if no data is set.
1319
- """
1320
- if callable(self.data):
1321
- return self.data(context) # pylint: disable=not-callable
1322
- return self.data
1323
-
1324
1343
  def should_render(self, context: Context):
1325
1344
  """Determine if the panel should be rendered."""
1326
1345
  if not super().should_render(context):
@@ -1336,16 +1355,14 @@ class EChartsPanel(Panel, EChartsBase):
1336
1355
 
1337
1356
  def get_extra_context(self, context: Context):
1338
1357
  """Add chart-specific context variables."""
1339
- self.data = self.get_data(context)
1340
- chart_config = self.get_config()
1358
+ chart_config = self.get_config(context=context)
1341
1359
  return {
1342
1360
  **super().get_extra_context(context),
1343
1361
  "chart": self,
1344
1362
  "chart_config": chart_config,
1345
1363
  "chart_width": self.width,
1346
1364
  "chart_height": self.height,
1347
- "chart_container_id": self.chart_container_id
1348
- or f"{slugify(f'echart-{self.header}')}-{uuid.uuid4().hex[:8]}",
1365
+ "chart_container_id": self.body_id,
1349
1366
  }
1350
1367
 
1351
1368
 
@@ -1542,7 +1559,7 @@ class GroupedKeyValueTablePanel(KeyValueTablePanel):
1542
1559
  super().__init__(body_id=body_id, **kwargs)
1543
1560
 
1544
1561
  def render_header_extra_content(self, context: Context):
1545
- """Add a "Collapse All" button to the header."""
1562
+ """Add a "Collapse All Groups" button to the header."""
1546
1563
  return format_html(
1547
1564
  """
1548
1565
  <button
@@ -1552,7 +1569,7 @@ class GroupedKeyValueTablePanel(KeyValueTablePanel):
1552
1569
  data-nb-toggle="collapse-all"
1553
1570
  type="button"
1554
1571
  >
1555
- Collapse All
1572
+ Collapse All Groups
1556
1573
  </button>
1557
1574
  """,
1558
1575
  body_id=self.body_id,
@@ -1727,7 +1744,7 @@ class StatsPanel(Panel):
1727
1744
  instance = get_obj_from_context(context)
1728
1745
  request = context["request"]
1729
1746
  if isinstance(instance, TreeModel):
1730
- self.filter_pks = (
1747
+ self.filter_pks = list(
1731
1748
  instance.descendants(include_self=True).restrict(request.user, "view").values_list("pk", flat=True)
1732
1749
  )
1733
1750
  else:
@@ -1743,16 +1760,17 @@ class StatsPanel(Panel):
1743
1760
  else:
1744
1761
  related_object_model_class, query = related_field, f"{self.filter_name}__in"
1745
1762
  filter_dict = {query: self.filter_pks}
1746
- related_object_count = (
1747
- related_object_model_class.objects.restrict(request.user, "view").filter(**filter_dict).count()
1748
- )
1763
+ qs = related_object_model_class.objects.restrict(request.user, "view").filter(**filter_dict)
1764
+ if len(self.filter_pks) > 1:
1765
+ qs = qs.distinct()
1766
+ related_object_count = qs.count()
1749
1767
  related_object_model_class_meta = related_object_model_class._meta
1750
1768
  related_object_list_url = validated_viewname(related_object_model_class, "list")
1751
1769
  related_object_title = bettertitle(related_object_model_class_meta.verbose_name_plural)
1752
1770
  value = [related_object_list_url, related_object_count, related_object_title]
1753
1771
  stats[related_object_model_class] = value
1754
1772
  related_object_model_filterset = get_filterset_for_model(related_object_model_class)
1755
- if self.filter_name not in related_object_model_filterset.get_filters():
1773
+ if self.filter_name not in related_object_model_filterset.base_filters:
1756
1774
  raise FieldDoesNotExist(
1757
1775
  f"{self.filter_name} is not a valid filter field for {related_object_model_class_meta.verbose_name}"
1758
1776
  )
@@ -2131,8 +2149,8 @@ class _ObjectDetailContactsTab(Tab):
2131
2149
  max_display_count=100, # since there isn't a separate list view for ContactAssociations!
2132
2150
  # TODO: we should provide a standard reusable component template for bulk-actions in the footer
2133
2151
  footer_content_template_path="components/panel/footer_contacts_table.html",
2134
- header_extra_content_template_path=None,
2135
- label="Contacts/Teams",
2152
+ enable_related_link=False,
2153
+ table_title="Contacts/Teams",
2136
2154
  ),
2137
2155
  )
2138
2156
  super().__init__(tab_id=tab_id, label=label, weight=weight, panels=panels, **kwargs)
@@ -2153,6 +2171,76 @@ class _ObjectDetailContactsTab(Tab):
2153
2171
  )
2154
2172
 
2155
2173
 
2174
+ class _ObjectDetailDataComplianceTab(DistinctViewTab):
2175
+ """Built-in class for a Tab displaying information about data compliance."""
2176
+
2177
+ def __init__(
2178
+ self,
2179
+ *,
2180
+ tab_id="data_compliance",
2181
+ label="Data Compliance",
2182
+ weight=Tab.WEIGHT_DATACOMPLIANCE_TAB,
2183
+ panels=None,
2184
+ **kwargs,
2185
+ ):
2186
+ if panels is None:
2187
+ panels = (
2188
+ ObjectsTablePanel(
2189
+ weight=100,
2190
+ table_class=DataComplianceTable,
2191
+ table_attribute="associated_data_compliance",
2192
+ related_field_name="object_id",
2193
+ table_title="Data Compliance",
2194
+ add_button_route=None,
2195
+ include_paginator=True,
2196
+ ),
2197
+ )
2198
+ super().__init__(url_name="", tab_id=tab_id, label=label, weight=weight, panels=panels, **kwargs)
2199
+
2200
+ def get_extra_context(self, context: Context):
2201
+ return {"url": get_obj_from_context(context).get_data_compliance_url()}
2202
+
2203
+ def should_render(self, context: Context):
2204
+ if not super().should_render(context):
2205
+ return False
2206
+ obj = get_obj_from_context(context)
2207
+ if getattr(obj, "is_data_compliance_model", False):
2208
+ if obj.get_data_compliance_url() is not None:
2209
+ return True
2210
+ logger.warning("Missing data-compliance URL for %r", obj)
2211
+ return False
2212
+
2213
+
2214
+ class DynamicGroupsTextPanel(BaseTextPanel):
2215
+ """Panel displaying a note about caching of dynamic groups."""
2216
+
2217
+ def __init__(
2218
+ self,
2219
+ *,
2220
+ weight,
2221
+ render_as=BaseTextPanel.RenderOptions.MARKDOWN,
2222
+ label="Dynamic Group caching",
2223
+ css_class="warning",
2224
+ **kwargs,
2225
+ ):
2226
+ super().__init__(weight=weight, render_as=render_as, label=label, css_class=css_class, **kwargs)
2227
+
2228
+ def get_value(self, context):
2229
+ dg_list_url = reverse("extras:dynamicgroup_list")
2230
+ job_run_url = reverse(
2231
+ "extras:job_run_by_class_path",
2232
+ kwargs={"class_path": "nautobot.core.jobs.groups.RefreshDynamicGroupCaches"},
2233
+ )
2234
+ return (
2235
+ "Dynamic group membership is cached for performance reasons, "
2236
+ "therefore this page may not always be up-to-date.\n\n"
2237
+ "You can refresh the membership of any specific group by accessing it from the list below or from the "
2238
+ f'[Dynamic Groups list view]({dg_list_url}) and clicking the "Refresh Members" button.\n\n'
2239
+ "You can also refresh the membership of **all** groups by running the "
2240
+ f"[Refresh Dynamic Group Caches job]({job_run_url})."
2241
+ )
2242
+
2243
+
2156
2244
  @dataclass
2157
2245
  class _ObjectDetailGroupsTab(Tab):
2158
2246
  """Built-in class for a Tab displaying information about associated dynamic groups."""
@@ -2169,8 +2257,9 @@ class _ObjectDetailGroupsTab(Tab):
2169
2257
  ):
2170
2258
  if panels is None:
2171
2259
  panels = (
2260
+ DynamicGroupsTextPanel(weight=100),
2172
2261
  ObjectsTablePanel(
2173
- weight=100,
2262
+ weight=200,
2174
2263
  table_class=DynamicGroupTable,
2175
2264
  table_attribute="dynamic_groups",
2176
2265
  exclude_columns=["content_type"],
@@ -2227,8 +2316,7 @@ class _ObjectDetailMetadataTab(Tab):
2227
2316
  exclude_columns=["assigned_object"],
2228
2317
  add_button_route=None,
2229
2318
  related_field_name="assigned_object_id",
2230
- header_extra_content_template_path=None,
2231
- label="Object Metadata",
2319
+ table_title="Object Metadata",
2232
2320
  ),
2233
2321
  )
2234
2322
  super().__init__(
@@ -6,17 +6,14 @@ from django.utils.html import strip_tags
6
6
  DEFAULT_TITLES: dict[str, str] = {
7
7
  "*": "{{ verbose_name_plural|bettertitle }}",
8
8
  "list": "{{ verbose_name_plural|bettertitle }}",
9
- "detail": "{{ object.display|default:object }}",
10
- "retrieve": "{{ object.display|default:object }}",
9
+ "detail": "{{ object.page_title|default:object }}",
10
+ "retrieve": "{{ object.page_title|default:object }}",
11
11
  "destroy": "Delete {{ verbose_name }}?",
12
12
  "create": "Add a new {{ verbose_name }}",
13
- "update": "Editing {{ verbose_name }} {{ object.display|default:object }}",
13
+ "update": "Editing {{ verbose_name }} {{ object.page_title|default:object }}",
14
14
  "bulk_destroy": "Delete {{ total_objs_to_delete }} {{ verbose_name_plural|bettertitle }}?",
15
15
  "bulk_rename": "Renaming {{ selected_objects|length }} {{ verbose_name_plural|bettertitle }} on {{ parent_name }}",
16
16
  "bulk_update": "Editing {{ objs_count }} {{ verbose_name_plural|bettertitle }}",
17
- "changelog": "{{ object.display|default:object }} - Change Log",
18
- "config_context": "{{ object.display|default:object }} - Config Context",
19
- "notes": "{{ object.display|default:object }} - Notes",
20
17
  "approve": "Approve {{ verbose_name|bettertitle }}?",
21
18
  "deny": "Deny {{ verbose_name|bettertitle }}?",
22
19
  }
nautobot/core/urls.py CHANGED
@@ -1,10 +1,11 @@
1
1
  from django.conf import settings
2
2
  from django.http import HttpResponse, HttpResponseNotFound
3
3
  from django.urls import include, path
4
- from django.views.generic import TemplateView
4
+ from django.views.generic import RedirectView, TemplateView
5
5
 
6
6
  from nautobot.core.views import (
7
7
  AboutView,
8
+ AppDocsView,
8
9
  CustomGraphQLView,
9
10
  get_file_with_authorization,
10
11
  HomeView,
@@ -41,11 +42,13 @@ urlpatterns = [
41
42
  path("dcim/", include("nautobot.dcim.urls")),
42
43
  path("extras/", include("nautobot.extras.urls")),
43
44
  path("ipam/", include("nautobot.ipam.urls")),
45
+ path("load-balancers/", include("nautobot.load_balancers.urls")),
44
46
  path("tenancy/", include("nautobot.tenancy.urls")),
45
47
  # TODO: deprecate this url and use users
46
48
  path("user/", include("nautobot.users.urls")),
47
49
  path("users/", include("nautobot.users.urls", "users")),
48
50
  path("virtualization/", include("nautobot.virtualization.urls")),
51
+ path("vpn/", include("nautobot.vpn.urls")),
49
52
  path("wireless/", include("nautobot.wireless.urls")),
50
53
  # API
51
54
  path("api/", include("nautobot.core.api.urls")),
@@ -59,6 +62,15 @@ urlpatterns = [
59
62
  path("media-failure/", StaticMediaFailureView.as_view(), name="media_failure"),
60
63
  # Apps
61
64
  path("apps/", include((apps_patterns, "apps"))),
65
+ # Redirect /docs/<app_base_url>/ -> /docs/<app_base_url>/index.html
66
+ path(
67
+ "docs/<str:app_base_url>/",
68
+ RedirectView.as_view(pattern_name="docs_file", permanent=False),
69
+ kwargs={"path": "index.html"},
70
+ name="docs_index_redirect",
71
+ ),
72
+ # Apps docs - Serve docs file
73
+ path("docs/<str:app_base_url>/<path:path>", AppDocsView.as_view(), name="docs_file"),
62
74
  path("plugins/", include((plugin_patterns, "plugins"))),
63
75
  path("admin/plugins/", include(plugin_admin_patterns)),
64
76
  # Social auth/SSO
@@ -92,15 +104,14 @@ urlpatterns = [
92
104
 
93
105
 
94
106
  if settings.DEBUG:
95
- try:
96
- import debug_toolbar
107
+ urlpatterns += [path("theme-preview/", ThemePreviewView.as_view(), name="theme_preview")]
108
+
109
+
110
+ if "debug_toolbar" in settings.INSTALLED_APPS:
111
+ from debug_toolbar.toolbar import debug_toolbar_urls
112
+
113
+ urlpatterns += debug_toolbar_urls()
97
114
 
98
- urlpatterns += [
99
- path("__debug__/", include(debug_toolbar.urls)),
100
- path("theme-preview/", ThemePreviewView.as_view(), name="theme_preview"),
101
- ]
102
- except ImportError:
103
- pass
104
115
 
105
116
  if settings.METRICS_ENABLED:
106
117
  if settings.METRICS_AUTHENTICATED:
@@ -66,5 +66,6 @@ def construct_cache_key(obj, *, method_name=None, branch_aware=True, **params):
66
66
  if params_tokens:
67
67
  cache_key += f"({','.join(params_tokens)})"
68
68
 
69
- logger.debug("Constructed cache key is %s", cache_key)
69
+ # Disabled as it's very noisy in some cases
70
+ # logger.debug("Constructed cache key is %s", cache_key)
70
71
  return cache_key
@@ -17,6 +17,21 @@ from nautobot.core.utils.lookup import get_filterset_for_model
17
17
  # e.g `name__ic` has lookup expr `ic (icontains)` while `name` has no lookup expr
18
18
  CONTAINS_LOOKUP_EXPR_RE = re.compile(r"(?<=__)\w+")
19
19
 
20
+ MODEL_VERBOSE_NAME_PLURAL_TO_FEATURE_NAME_MAPPING = {
21
+ "approval_workflow_definitions": "approval_workflows",
22
+ "cables": "cable_terminations",
23
+ "data_compliance": "custom_validators",
24
+ "location_types": "locations",
25
+ "metadata_types": "metadata",
26
+ "min_max_validation_rules": "custom_validators",
27
+ "object_metadata": "metadata",
28
+ "regular_expression_validation_rules": "custom_validators",
29
+ "relationship_associations": "relationships",
30
+ "required_validation_rules": "custom_validators",
31
+ "static_group_associations": "dynamic_groups",
32
+ "unique_validation_rules": "custom_validators",
33
+ }
34
+
20
35
 
21
36
  def build_lookup_label(field_name, _verbose_name):
22
37
  """
@@ -92,6 +107,7 @@ def get_filterset_parameter_form_field(model, parameter, filterset=None):
92
107
  BOOLEAN_CHOICES,
93
108
  DynamicModelMultipleChoiceField,
94
109
  MultipleContentTypeField,
110
+ MultiValueCharInput,
95
111
  StaticSelect2,
96
112
  StaticSelect2Multiple,
97
113
  )
@@ -112,7 +128,16 @@ def get_filterset_parameter_form_field(model, parameter, filterset=None):
112
128
  elif isinstance(field, (MultiValueDecimalFilter, MultiValueFloatFilter)):
113
129
  form_field = forms.DecimalField()
114
130
  elif isinstance(field, NumberFilter):
115
- form_field = forms.IntegerField()
131
+ # If "choices" are passed, then when 'exact' is used in an Advanced
132
+ # Filter, render a dropdown of choices instead of a free integer input
133
+ if field.lookup_expr == "exact" and getattr(field, "choices", None):
134
+ # Use a multi-value widget that allows both preset choices and free-form entries
135
+ form_field = forms.MultipleChoiceField(
136
+ choices=field.choices,
137
+ widget=MultiValueCharInput,
138
+ )
139
+ else:
140
+ form_field = forms.IntegerField()
116
141
  elif isinstance(field, ModelMultipleChoiceFilter):
117
142
  if getattr(field, "prefers_id", False):
118
143
  to_field_name = "id"
@@ -131,26 +156,11 @@ def get_filterset_parameter_form_field(model, parameter, filterset=None):
131
156
  elif isinstance(
132
157
  field, ContentTypeMultipleChoiceFilter
133
158
  ): # While there are other objects using `ContentTypeMultipleChoiceFilter`, the case where
134
- # models that have such a filter and the `verbose_name_plural` has multiple words is ony one: "dynamic groups".
159
+ # models that have such a filter and the `verbose_name_plural` does not match, we can lookup the feature name.
135
160
  from nautobot.core.models.fields import slugify_dashes_to_underscores # Avoid circular import
136
161
 
137
162
  plural_name = slugify_dashes_to_underscores(model._meta.verbose_name_plural)
138
-
139
- # Cable-connectable models use "cable_terminations", not "cables", as the feature name
140
- if plural_name == "cables":
141
- plural_name = "cable_terminations"
142
- elif plural_name == "metadata_types":
143
- plural_name = "metadata"
144
- elif plural_name == "object_metadata":
145
- plural_name = "metadata"
146
- elif plural_name in [
147
- "data_compliance",
148
- "min_max_validation_rules",
149
- "regular_expression_validation_rules",
150
- "required_validation_rules",
151
- "unique_validation_rules",
152
- ]:
153
- plural_name = "custom_validators"
163
+ plural_name = MODEL_VERBOSE_NAME_PLURAL_TO_FEATURE_NAME_MAPPING.get(plural_name, plural_name)
154
164
  try:
155
165
  form_field = MultipleContentTypeField(choices_as_strings=True, feature=plural_name)
156
166
  except KeyError: