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
@@ -75,14 +75,14 @@
75
75
  </div>
76
76
 
77
77
  <div class="d-print-none justify-content-between mb-n20 nb-form-sticky-footer">
78
- <button type="reset" class="btn btn-secondary">
79
- <span aria-hidden="true" class="mdi mdi-close me-4"></span><!--
80
- -->Clear All
81
- </button>
82
78
  <button type="submit" class="btn btn-primary">
83
79
  <span class="mdi mdi-check me-4" aria-hidden="true"></span><!--
84
80
  -->Apply Specified
85
81
  </button>
82
+ <button type="reset" class="btn btn-secondary">
83
+ <span aria-hidden="true" class="mdi mdi-close me-4"></span><!--
84
+ -->Clear All
85
+ </button>
86
86
  </div>
87
87
  </form>
88
88
  </div>
@@ -124,6 +124,31 @@
124
124
  dynamicFilterForm: document.querySelector('#dynamic-filter-form'),
125
125
  });
126
126
 
127
+ const prepopulateFilterSelectsFromURL = (() => {
128
+ const { defaultFilterForm } = getFilterForms();
129
+ const urlParams = new URLSearchParams(window.location.search);
130
+ // Only target Select2 controls inside the default filter form.
131
+ [...defaultFilterForm.querySelectorAll('select.nautobot-select2-multi-value-char')].forEach((el) => {
132
+ const name = el.getAttribute('name');
133
+ if (!name) { return; }
134
+ const values = urlParams.getAll(name);
135
+ if (!values.length) { return; }
136
+ values.forEach((val) => {
137
+ let found = Array.prototype.find.call(el.options, (opt) => {
138
+ return String(opt.value) === String(val);
139
+ });
140
+ if (found) {
141
+ found.selected = true;
142
+ } else {
143
+ el.add(new Option(val, val, true, true));
144
+ }
145
+ });
146
+ if (window.jQuery && $(el).data('select2')) {
147
+ $(el).trigger('change');
148
+ }
149
+ });
150
+ })();
151
+
127
152
  /**
128
153
  * Synchronize given `filter` value from default to dynamic filter form.
129
154
  * @param {string} name - Filter name.
@@ -435,6 +460,14 @@
435
460
 
436
461
  const { defaultFilterForm, dynamicFilterForm } = getFilterForms();
437
462
 
463
+ /*
464
+ * Just before the form submission automatically add filter selected in "Advanced" tab (if any)
465
+ * which hasn't been manually applied for whatever reason (usually forgetfulness, speed, etc.).
466
+ * This is a requested UX flavor.
467
+ */
468
+ const add = dynamicFilterForm.querySelector('button.nb-dynamic-filter-add');
469
+ add.click();
470
+
438
471
  // Remove all field names which are not applied filters container descendants.
439
472
  [...dynamicFilterForm.querySelectorAll('input, select, textarea')].forEach((field) =>
440
473
  field.closest('.nb-dynamic-filter-items') ? undefined : field.removeAttribute('name'),
@@ -56,7 +56,7 @@
56
56
  {% export_button content_type list_element=True %}
57
57
  </ul>
58
58
  </div>
59
- <div class="btn-group">
59
+ <div class="dropdown d-inline-flex">
60
60
  <button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
61
61
  Actions
62
62
  </button>
@@ -234,11 +234,11 @@
234
234
  <div class="mb-10 d-flex justify-content-center">
235
235
  <label class="col-md-3 col-form-label nb-required" for="id_status">
236
236
  Status
237
- <span class="form-text">Help text for this form field</span>
238
237
  </label>
239
238
  <div class="col-md-9">
240
239
  <select name="status" class="nautobot-select2-api form-control select2-hidden-accessible" data-query-param-content_types="[&quot;dcim.device&quot;]" display-field="display" value-field="id" data-depth="0" required="" placeholder="Status" data-url="/api/extras/statuses/" id="id_status">
241
240
  </select>
241
+ <span class="form-text">Help text for this form field</span>
242
242
  </div>
243
243
  </div>
244
244
  </div>
@@ -471,6 +471,15 @@
471
471
  <button type="button" class="btn btn-link disabled">Link</button>
472
472
  </td>
473
473
  </tr>
474
+ <tr>
475
+ <td><code>btn-group</code> like radio buttons</td>
476
+ <td>
477
+ <div class="btn-group" role="group">
478
+ <button type="button" class="btn btn-primary bg-primary nb-text-body-bg">Current Active Option</button>
479
+ <button type="button" class="btn btn-primary">Alternative Option</button>
480
+ </div>
481
+ </td>
482
+ </tr>
474
483
  </table>
475
484
  </div>
476
485
  </div>
@@ -1120,15 +1129,19 @@ This:
1120
1129
  <h1>Nautobot Icons</h1>
1121
1130
  <div class="row">
1122
1131
  <div class="col-12">
1123
- <div class="column-gap-8 d-flex flex-wrap px-20 py-16 rounded row-gap-16" style="background-color: #121a25;">
1132
+ <div class="column-gap-8 d-flex flex-wrap px-20 py-16 rounded row-gap-16" style="background-color: #121212;">
1124
1133
  <div class="icon-preview"><img alt="360-degrees icon" src="{% static 'nautobot-icons/360-degrees.svg' %}">360-degrees</div>
1125
1134
  <div class="icon-preview"><img alt="arrow-decision icon" src="{% static 'nautobot-icons/arrow-decision.svg' %}">arrow-decision</div>
1126
1135
  <div class="icon-preview"><img alt="arrows-expand-rec icon" src="{% static 'nautobot-icons/arrows-expand-rec.svg' %}">arrows-expand-rec</div>
1127
1136
  <div class="icon-preview"><img alt="arrows-move-rec icon" src="{% static 'nautobot-icons/arrows-move-rec.svg' %}">arrows-move-rec</div>
1128
1137
  <div class="icon-preview"><img alt="arrows-move-2-rec icon" src="{% static 'nautobot-icons/arrows-move-2-rec.svg' %}">arrows-move-2-rec</div>
1129
1138
  <div class="icon-preview"><img alt="atom icon" src="{% static 'nautobot-icons/atom.svg' %}">atom</div>
1139
+ <div class="icon-preview"><img alt="power icon" src="{% static 'nautobot-icons/battery-3.svg' %}">battery-3</div>
1130
1140
  <div class="icon-preview"><img alt="branch icon" src="{% static 'nautobot-icons/branch.svg' %}">branch</div>
1131
1141
  <div class="icon-preview"><img alt="briefcase-2 icon" src="{% static 'nautobot-icons/briefcase-2.svg' %}">briefcase-2</div>
1142
+ <div class="icon-preview"><img alt="bus-globe icon" src="{% static 'nautobot-icons/bus-globe.svg' %}">bus-globe</div>
1143
+ <div class="icon-preview"><img alt="bus-shield icon" src="{% static 'nautobot-icons/bus-shield.svg' %}">bus-shield</div>
1144
+ <div class="icon-preview"><img alt="bus-shield-check icon" src="{% static 'nautobot-icons/bus-shield-check.svg' %}">bus-shield-check</div>
1132
1145
  <div class="icon-preview"><img alt="cable-data icon" src="{% static 'nautobot-icons/cable-data.svg' %}">cable-data</div>
1133
1146
  <div class="icon-preview"><img alt="cable-data-2 icon" src="{% static 'nautobot-icons/cable-data-2.svg' %}">cable-data-2</div>
1134
1147
  <div class="icon-preview"><img alt="cast icon" src="{% static 'nautobot-icons/cast.svg' %}">cast</div>
@@ -1145,6 +1158,7 @@ This:
1145
1158
  <div class="icon-preview"><img alt="device-lifecycle icon" src="{% static 'nautobot-icons/device-lifecycle.svg' %}">device-lifecycle</div>
1146
1159
  <div class="icon-preview"><img alt="direction icon" src="{% static 'nautobot-icons/direction.svg' %}">direction</div>
1147
1160
  <div class="icon-preview"><img alt="elements icon" src="{% static 'nautobot-icons/elements.svg' %}">elements</div>
1161
+ <div class="icon-preview"><img alt="extensibility icon" src="{% static 'nautobot-icons/extensibility.svg' %}">extensibility</div>
1148
1162
  <div class="icon-preview"><img alt="globe icon" src="{% static 'nautobot-icons/globe.svg' %}">globe</div>
1149
1163
  <div class="icon-preview"><img alt="globe-2 icon" src="{% static 'nautobot-icons/globe-2.svg' %}">globe-2</div>
1150
1164
  <div class="icon-preview"><img alt="hammer icon" src="{% static 'nautobot-icons/hammer.svg' %}">hammer</div>
@@ -1154,6 +1168,7 @@ This:
1154
1168
  <div class="icon-preview"><img alt="lightning icon" src="{% static 'nautobot-icons/lightning.svg' %}">lightning</div>
1155
1169
  <div class="icon-preview"><img alt="list-unordered icon" src="{% static 'nautobot-icons/list-unordered.svg' %}">list-unordered</div>
1156
1170
  <div class="icon-preview"><img alt="map-view icon" src="{% static 'nautobot-icons/map-view.svg' %}">map-view</div>
1171
+ <div class="icon-preview"><img alt="organization icon" src="{% static 'nautobot-icons/organization.svg' %}">organization</div>
1157
1172
  <div class="icon-preview"><img alt="pin-2 icon" src="{% static 'nautobot-icons/pin-2.svg' %}">pin-2</div>
1158
1173
  <div class="icon-preview"><img alt="pin-3 icon" src="{% static 'nautobot-icons/pin-3.svg' %}">pin-3</div>
1159
1174
  <div class="icon-preview"><img alt="plug icon" src="{% static 'nautobot-icons/plug.svg' %}">plug</div>
@@ -1162,6 +1177,7 @@ This:
1162
1177
  <div class="icon-preview"><img alt="rotate-cw icon" src="{% static 'nautobot-icons/rotate-cw.svg' %}">rotate-cw</div>
1163
1178
  <div class="icon-preview"><img alt="route icon" src="{% static 'nautobot-icons/route.svg' %}">route</div>
1164
1179
  <div class="icon-preview"><img alt="secrets icon" src="{% static 'nautobot-icons/secrets.svg' %}">secrets</div>
1180
+ <div class="icon-preview"><img alt="security icon" src="{% static 'nautobot-icons/security.svg' %}">security</div>
1165
1181
  <div class="icon-preview"><img alt="server icon" src="{% static 'nautobot-icons/server.svg' %}">server</div>
1166
1182
  <div class="icon-preview"><img alt="server-2 icon" src="{% static 'nautobot-icons/server-2.svg' %}">server-2</div>
1167
1183
  <div class="icon-preview"><img alt="share icon" src="{% static 'nautobot-icons/share.svg' %}">share</div>
@@ -0,0 +1,44 @@
1
+ <div class="input-group">
2
+ {% include 'django/forms/widgets/number.html' %}
3
+ {% if widget.choices %}
4
+ <span class="input-group-btn">
5
+ <button type="button" class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown">
6
+ <span class="mdi mdi-chevron-down"></span>
7
+ </button>
8
+ <ul class="dropdown-menu dropdown-menu-end">
9
+ {% for value, label in widget.choices %}
10
+ <li><a href="#" data-name="{{ widget.name }}" data-value="{{ value }}" class="set_value dropdown-item">{{ label }}</a></li>
11
+ {% endfor %}
12
+ </ul>
13
+ </span>
14
+ {% endif %}
15
+ </div>
16
+
17
+ {% if widget.choices %}
18
+ <script type="text/javascript">
19
+ (function() {
20
+ if (window.__nbNumberWithSelectWidgetBound) return;
21
+ window.__nbNumberWithSelectWidgetBound = true;
22
+ function bindNumberWithSelectHandler() {
23
+ document.addEventListener("click", function(e) {
24
+ if (!e.target) return;
25
+ var link = e.target.closest && e.target.closest("a.set_value");
26
+ if (!link) return;
27
+ e.preventDefault();
28
+ var container = link.closest(".input-group");
29
+ var name = link.getAttribute("data-name");
30
+ var value = link.getAttribute("data-value");
31
+ var input = container && name ? container.querySelector('input[name="' + name + '"]') : null;
32
+ if (input) {
33
+ input.value = value;
34
+ }
35
+ });
36
+ }
37
+ if (document.readyState === 'loading') {
38
+ document.addEventListener('DOMContentLoaded', bindNumberWithSelectHandler);
39
+ } else {
40
+ bindNumberWithSelectHandler();
41
+ }
42
+ })();
43
+ </script>
44
+ {% endif %}
@@ -1 +1,3 @@
1
- <option value="{{ widget.value }}"{% include "django/forms/widgets/attrs.html" %}{% if widget.label.disabled %} disabled="disabled"{% endif %}>{{ widget.label.label|default:widget.label }}</option>
1
+ <option value="{{ widget.value }}"
2
+ {% include "django/forms/widgets/attrs.html" %}
3
+ {% if widget.label.disabled %} disabled{% endif %}>{{ widget.label.label|default:widget.label }}</option>
@@ -1,5 +1,6 @@
1
1
  from collections.abc import Iterable
2
2
  import datetime
3
+ from importlib import resources
3
4
  import json
4
5
  import logging
5
6
  import re
@@ -7,6 +8,7 @@ from typing import Literal
7
8
  from urllib.parse import parse_qs, quote_plus
8
9
 
9
10
  from django import template
11
+ from django.apps import apps
10
12
  from django.conf import settings
11
13
  from django.contrib import messages
12
14
  from django.contrib.auth.models import AnonymousUser
@@ -14,9 +16,11 @@ from django.contrib.staticfiles.finders import find
14
16
  from django.core.exceptions import ObjectDoesNotExist
15
17
  from django.templatetags.static import static, StaticNode
16
18
  from django.urls import NoReverseMatch, reverse
19
+ from django.utils.formats import date_format
17
20
  from django.utils.html import format_html, format_html_join, strip_tags
18
21
  from django.utils.safestring import mark_safe
19
22
  from django.utils.text import slugify as django_slugify
23
+ from django.utils.translation import gettext as _
20
24
  from django_jinja import library
21
25
  from markdown import markdown
22
26
  import yaml
@@ -122,23 +126,29 @@ def placeholder(value):
122
126
 
123
127
  @library.filter()
124
128
  @register.filter()
125
- def pre_tag(value):
129
+ def pre_tag(value, format_empty_value=True):
126
130
  """Render a value within `<pre></pre>` tags to enable formatting.
127
131
 
128
132
  Args:
129
133
  value (any): Input value, can be any variable.
134
+ format_empty_value (bool): Whether format empty value or render placeholder.
130
135
 
131
136
  Returns:
132
- (str): Value wrapped in `<pre></pre>` tags.
137
+ (str): Value wrapped in `<pre></pre>` tags or placeholder if None or format_empty_values=False and empty
133
138
 
134
139
  Example:
135
140
  >>> pre_tag("")
136
141
  '<pre></pre>'
137
142
  >>> pre_tag("hello")
138
143
  '<pre>hello</pre>'
144
+ >>> pre_tag("", format_empty_value=False)
145
+ '<span class="text-secondary">&mdash;</span>'
139
146
  """
140
- if value is not None:
147
+ if format_empty_value and value is not None:
148
+ return format_html("<pre>{}</pre>", value)
149
+ elif value:
141
150
  return format_html("<pre>{}</pre>", value)
151
+
142
152
  return HTML_NONE
143
153
 
144
154
 
@@ -391,17 +401,19 @@ def humanize_speed(speed):
391
401
  1544 => "1.544 Mbps"
392
402
  100000 => "100 Mbps"
393
403
  10000000 => "10 Gbps"
404
+ 1000000000 => "1 Tbps"
405
+ 1600000000 => "1.6 Tbps"
406
+ 10000000000 => "10 Tbps"
394
407
  """
395
408
  if not speed:
396
409
  return ""
397
- if speed >= 1000000000 and speed % 1000000000 == 0:
398
- return f"{int(speed / 1000000000)} Tbps"
399
- elif speed >= 1000000 and speed % 1000000 == 0:
400
- return f"{int(speed / 1000000)} Gbps"
401
- elif speed >= 1000 and speed % 1000 == 0:
402
- return f"{int(speed / 1000)} Mbps"
410
+
411
+ if speed >= 1000000000:
412
+ return f"{speed / 1000000000:g} Tbps"
413
+ elif speed >= 1000000:
414
+ return f"{speed / 1000000:g} Gbps"
403
415
  elif speed >= 1000:
404
- return f"{float(speed) / 1000} Mbps"
416
+ return f"{speed / 1000:g} Mbps"
405
417
  else:
406
418
  return f"{speed} Kbps"
407
419
 
@@ -483,11 +495,13 @@ def percentage(x, y):
483
495
  @library.filter()
484
496
  @register.filter()
485
497
  def get_docs_url(model):
486
- """Return the likely static documentation path for the specified model, if it can be found/predicted.
498
+ """Return the documentation URL for the specified model, if it can be found/predicted.
487
499
 
488
500
  - Core models, as of 2.0, are usually at `docs/user-guide/core-data-model/{app_label}/{model_name}.html`.
489
501
  - Models in the `extras` app are usually at `docs/user-guide/platform-functionality/{model_name}.html`.
490
- - Apps (plugins) are generally expected to be documented at `{app_label}/docs/models/{model_name}.html`.
502
+ - Apps (plugins) are expected to be documented within their package at
503
+ ``docs/models/{model_name}.html`` and are served dynamically through
504
+ the ``AppDocsView`` endpoint (``/docs/<app_name>/<path>``).
491
505
 
492
506
  Any model can define a `documentation_static_path` class attribute if it needs to override the above expectations.
493
507
 
@@ -501,16 +515,36 @@ def get_docs_url(model):
501
515
 
502
516
  Example:
503
517
  >>> get_docs_url(location_instance)
504
- "static/docs/models/dcim/location.html"
518
+ "static/docs/user-guide/core-data-model/dcim/location.html"
519
+ >>> get_docs_url(virtual_server_instance)
520
+ "static/docs/user-guide/core-data-model/load-balancers/virtualserver.html"
521
+ >>> get_docs_url(example_model)
522
+ "/docs/example-app/models/examplemodel.html"
505
523
  """
506
524
  if hasattr(model, "documentation_static_path"):
507
525
  path = model.documentation_static_path
508
526
  elif model._meta.app_label in settings.PLUGINS:
527
+ app_label = model._meta.app_label
528
+ app_config = apps.get_app_config(app_label)
529
+ app_base_url = getattr(app_config, "base_url", None) or app_config.label
530
+ path = f"models/{model._meta.model_name}.html"
531
+ # Check that the file actually exists inside the app's docs folder
532
+ try:
533
+ base_dir = resources.files(app_label) / "docs"
534
+ file_path = base_dir / path
535
+ if file_path.is_file():
536
+ return reverse("docs_file", kwargs={"app_base_url": app_base_url, "path": path})
537
+ except ModuleNotFoundError:
538
+ pass
539
+ logger.debug("No documentation found for %s (expected at %s)", type(model), path)
540
+ # define path to try to get static
509
541
  path = f"{model._meta.app_label}/docs/models/{model._meta.model_name}.html"
510
542
  elif model._meta.app_label == "extras":
511
543
  path = f"docs/user-guide/platform-functionality/{model._meta.model_name}.html"
512
544
  else:
513
- path = f"docs/user-guide/core-data-model/{model._meta.app_label}/{model._meta.model_name}.html"
545
+ path = (
546
+ f"docs/user-guide/core-data-model/{model._meta.app_label.replace('_', '-')}/{model._meta.model_name}.html"
547
+ )
514
548
 
515
549
  # Check to see if documentation exists in any of the static paths.
516
550
  if find(path):
@@ -759,7 +793,7 @@ def render_address(address):
759
793
  quote_plus(address),
760
794
  )
761
795
  address = format_html_join("", "{}<br>", ((line,) for line in address.split("\n")))
762
- return format_html('<div class="pull-right d-print-none">{}</div>{}', map_link, address)
796
+ return format_html('<div class="float-end d-print-none">{}</div>{}', map_link, address)
763
797
  return HTML_NONE
764
798
 
765
799
 
@@ -798,7 +832,11 @@ def render_button_class(value):
798
832
  """
799
833
  if value:
800
834
  base = value.split()[0]
801
- return format_html('<button class="btn btn-{}">{}</button>', base.lower(), base.capitalize())
835
+ return format_html(
836
+ '<button class="btn btn-{}">{}</button>',
837
+ base.lower() if base.lower() != "default" else "secondary",
838
+ base.capitalize(),
839
+ )
802
840
  return ""
803
841
 
804
842
 
@@ -831,6 +869,26 @@ def label_list(value, suffix=""):
831
869
  )
832
870
 
833
871
 
872
+ @library.filter()
873
+ @register.filter()
874
+ def format_timezone(time_zone):
875
+ """
876
+ Return a human-readable representation of a time zone including:
877
+ - Time zone name and UTC offset on the first line
878
+ - Local date and time on the next line (in smaller font)
879
+ """
880
+ if not time_zone:
881
+ return HTML_NONE
882
+
883
+ now = datetime.datetime.now(time_zone)
884
+
885
+ # Locale-aware formatting (respects USE_L10N + active locale)
886
+ local_time = date_format(now, format="DATETIME_FORMAT", use_l10n=True)
887
+
888
+ result = f"{time_zone} (UTC {now.strftime('%z')})<br><small>{_('Local time')}: {local_time}</small>"
889
+ return format_html(result)
890
+
891
+
834
892
  #
835
893
  # Tags
836
894
  #
@@ -899,7 +957,7 @@ def django_querystring(context, query_dict=None, **kwargs):
899
957
  {% django_querystring my_query_dict foo=3 %}
900
958
  """
901
959
  if query_dict is None:
902
- query_dict = context.request.GET
960
+ query_dict = context["request"].GET
903
961
  params = query_dict.copy()
904
962
  for key, value in kwargs.items():
905
963
  if value is None:
@@ -932,7 +990,7 @@ def table_config_button(table, table_name=None, extra_classes="", disabled=False
932
990
  <span class="mdi mdi-cog" aria-hidden="true"></span>
933
991
  <span class="visually-hidden">Configure</span>
934
992
  </button>"""
935
- return format_html(html_template, extra_classes, table_name, 'disabled="disabled"' if disabled else "", table_name)
993
+ return format_html(html_template, extra_classes, table_name, "disabled" if disabled else "", table_name)
936
994
 
937
995
 
938
996
  @register.inclusion_tag("utilities/templatetags/utilization_graph.html")
@@ -17,6 +17,7 @@ from rest_framework import serializers, status
17
17
  from rest_framework.relations import ManyRelatedField
18
18
  from rest_framework.test import APITransactionTestCase as _APITransactionTestCase
19
19
 
20
+ from nautobot.core import constants
20
21
  from nautobot.core.api.utils import get_serializer_for_model
21
22
  from nautobot.core.models import fields as core_fields
22
23
  from nautobot.core.models.tree_queries import TreeModel
@@ -294,8 +295,9 @@ class APIViewTestCases:
294
295
  m2m_fields = self.get_m2m_fields()
295
296
  self.add_permissions(f"{self.model._meta.app_label}.view_{self.model._meta.model_name}")
296
297
  list_url = f"{self._get_list_url()}?depth=0"
298
+ # With exclude_m2m query parameter set to False
297
299
  with CaptureQueriesContext(connections[DEFAULT_DB_ALIAS]) as cqc:
298
- response = self.client.get(list_url, **self.header)
300
+ response = self.client.get(list_url + "&exclude_m2m=false", **self.header)
299
301
  base_num_queries = len(cqc)
300
302
 
301
303
  self.assertHttpStatus(response, status.HTTP_200_OK)
@@ -340,9 +342,9 @@ class APIViewTestCases:
340
342
  app_label, model_name = object_type.split(".")
341
343
  ContentType.objects.get(app_label=app_label, model=model_name)
342
344
 
343
- list_url += "&exclude_m2m=true"
345
+ # With exclude_m2m query parameter set to True
344
346
  with CaptureQueriesContext(connections[DEFAULT_DB_ALIAS]) as cqc:
345
- response = self.client.get(list_url, **self.header)
347
+ response = self.client.get(list_url + "&exclude_m2m=true", **self.header)
346
348
 
347
349
  self.assertHttpStatus(response, status.HTTP_200_OK)
348
350
  self.assertIsInstance(response.data, dict)
@@ -387,8 +389,9 @@ class APIViewTestCases:
387
389
  m2m_fields = self.get_m2m_fields()
388
390
  self.add_permissions(f"{self.model._meta.app_label}.view_{self.model._meta.model_name}")
389
391
  list_url = f"{self._get_list_url()}?depth=1"
392
+ # With exclude_m2m query parameter set to False
390
393
  with CaptureQueriesContext(connections[DEFAULT_DB_ALIAS]) as cqc:
391
- response = self.client.get(list_url, **self.header)
394
+ response = self.client.get(list_url + "&exclude_m2m=false", **self.header)
392
395
  base_num_queries = len(cqc)
393
396
 
394
397
  self.assertHttpStatus(response, status.HTTP_200_OK)
@@ -420,9 +423,9 @@ class APIViewTestCases:
420
423
  self.assertTrue(is_uuid(response_data[field]["id"]))
421
424
  self.assertGreater(len(response_data[field].keys()), 3, response_data[field])
422
425
 
423
- list_url += "&exclude_m2m=true"
426
+ # With exclude_m2m query parameter set to True
424
427
  with CaptureQueriesContext(connections[DEFAULT_DB_ALIAS]) as cqc:
425
- response = self.client.get(list_url, **self.header)
428
+ response = self.client.get(list_url + "&exclude_m2m=true", **self.header)
426
429
 
427
430
  self.assertHttpStatus(response, status.HTTP_200_OK)
428
431
  self.assertIsInstance(response.data, dict)
@@ -458,6 +461,54 @@ class APIViewTestCases:
458
461
  self.assertNotIn(field, response_data)
459
462
  # TODO: we should assert that all other fields are still present, but there's a few corner cases...
460
463
 
464
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
465
+ def test_list_objects_exclude_m2m(self):
466
+ """
467
+ GET a list of objects with or without the "exclude_m2m" parameter.
468
+
469
+ With exclude_m2m query parameter set to True, we should see no many-to-many fields.
470
+ With exclude_m2m query parameter set to False, we should see all many-to-many fields.
471
+ With exclude_m2m query parameter not set, we should only see the default many-to-many fields.
472
+ """
473
+ m2m_fields = self.get_m2m_fields()
474
+ if not m2m_fields:
475
+ self.skipTest("No many-to-many fields to test")
476
+ self.add_permissions(f"{self.model._meta.app_label}.view_{self.model._meta.model_name}")
477
+ list_url = f"{self._get_list_url()}"
478
+
479
+ # With exclude_m2m query parameter not set
480
+ response = self.client.get(list_url, **self.header)
481
+ self.assertHttpStatus(response, status.HTTP_200_OK)
482
+ self.assertIsInstance(response.data, dict)
483
+ self.assertIn("results", response.data)
484
+
485
+ for response_data in response.data["results"]:
486
+ for field in m2m_fields:
487
+ if field in constants.DEFAULT_M2M_FIELDS:
488
+ self.assertIn(field, response_data)
489
+ self.assertIsInstance(response_data[field], list)
490
+ else:
491
+ self.assertNotIn(field, response_data)
492
+
493
+ # With exclude_m2m query parameter set to True
494
+ response = self.client.get(list_url + "?exclude_m2m=true", **self.header)
495
+ self.assertHttpStatus(response, status.HTTP_200_OK)
496
+ self.assertIsInstance(response.data, dict)
497
+ self.assertIn("results", response.data)
498
+ for response_data in response.data["results"]:
499
+ for field in m2m_fields:
500
+ self.assertNotIn(field, response_data)
501
+
502
+ # With exclude_m2m query parameter set to False
503
+ response = self.client.get(list_url + "?exclude_m2m=false", **self.header)
504
+ self.assertHttpStatus(response, status.HTTP_200_OK)
505
+ self.assertIsInstance(response.data, dict)
506
+ self.assertIn("results", response.data)
507
+ for response_data in response.data["results"]:
508
+ for field in m2m_fields:
509
+ self.assertIn(field, response_data)
510
+ self.assertIsInstance(response_data[field], list)
511
+
461
512
  @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
462
513
  def test_list_objects_without_permission(self):
463
514
  """
@@ -1076,12 +1127,20 @@ class APIViewTestCases:
1076
1127
 
1077
1128
  self.assertIn("actions", data)
1078
1129
 
1079
- # Grab any field that has choices defined (fields with enums)
1130
+ # Grab any field that has choices defined (fields with enums including child fields with enums)
1080
1131
  field_choices = {}
1081
1132
  if "POST" in data["actions"]:
1082
- field_choices = {k for k, v in data["actions"]["POST"].items() if "choices" in v}
1133
+ field_choices = {
1134
+ k
1135
+ for k, v in data["actions"]["POST"].items()
1136
+ if "choices" in v or ("child" in v and "choices" in v["child"])
1137
+ }
1083
1138
  elif "PUT" in data["actions"]:
1084
- field_choices = {k for k, v in data["actions"]["PUT"].items() if "choices" in v}
1139
+ field_choices = {
1140
+ k
1141
+ for k, v in data["actions"]["PUT"].items()
1142
+ if "choices" in v or ("child" in v and "choices" in v["child"])
1143
+ }
1085
1144
  else:
1086
1145
  self.fail(f"Neither PUT nor POST are available actions in: {data['actions']}")
1087
1146
 
@@ -21,7 +21,6 @@ from nautobot.core.filters import (
21
21
  )
22
22
  from nautobot.core.models.generics import PrimaryModel
23
23
  from nautobot.core.testing import views
24
- from nautobot.core.utils.deprecation import class_deprecated_in_favor_of
25
24
  from nautobot.extras.models import Contact, ContactAssociation, Role, Status, Tag, Team
26
25
  from nautobot.tenancy import models
27
26
 
@@ -435,28 +434,6 @@ class FilterTestCases:
435
434
  ),
436
435
  )
437
436
 
438
- # Test cases should just explicitly include `name` as a generic_filter_tests entry
439
- @class_deprecated_in_favor_of(FilterTestCase) # pylint: disable=undefined-variable
440
- class NameOnlyFilterTestCase(FilterTestCase):
441
- """Add simple tests for filtering by name."""
442
-
443
- def test_filters_generic(self):
444
- if not any(test[0] == "name" for test in self.generic_filter_tests):
445
- self.generic_filter_tests = (["name"], *self.generic_filter_tests)
446
- super().test_filters_generic()
447
-
448
- # Test cases should just explicitly include `name` and `slug` as generic_filter_tests entries
449
- @class_deprecated_in_favor_of(FilterTestCase) # pylint: disable=undefined-variable
450
- class NameSlugFilterTestCase(FilterTestCase):
451
- """Add simple tests for filtering by name and by slug."""
452
-
453
- def test_filters_generic(self):
454
- if not any(test[0] == "slug" for test in self.generic_filter_tests):
455
- self.generic_filter_tests = (["slug"], *self.generic_filter_tests)
456
- if not any(test[0] == "name" for test in self.generic_filter_tests):
457
- self.generic_filter_tests = (["name"], *self.generic_filter_tests)
458
- super().test_filters_generic()
459
-
460
437
  class TenancyFilterTestCaseMixin(views.TestCase):
461
438
  """Add test cases for tenant and tenant-group filters."""
462
439