nautobot 2.4.21__py3-none-any.whl → 3.0.0a3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of nautobot might be problematic. Click here for more details.

Files changed (919) hide show
  1. nautobot/apps/choices.py +2 -2
  2. nautobot/apps/filters.py +9 -9
  3. nautobot/apps/forms.py +2 -0
  4. nautobot/apps/models.py +7 -2
  5. nautobot/apps/ui.py +20 -1
  6. nautobot/apps/utils.py +2 -3
  7. nautobot/apps/views.py +7 -1
  8. nautobot/circuits/filters.py +8 -23
  9. nautobot/circuits/navigation.py +3 -1
  10. nautobot/circuits/templates/circuits/circuit_create.html +9 -9
  11. nautobot/circuits/templates/circuits/circuit_terminations_swap.html +2 -2
  12. nautobot/circuits/templates/circuits/circuittermination_create.html +24 -33
  13. nautobot/circuits/templates/circuits/inc/circuit_termination.html +10 -10
  14. nautobot/circuits/templates/circuits/inc/circuit_termination_cable_fragment.html +13 -13
  15. nautobot/circuits/templates/circuits/inc/circuit_termination_header_extra_content.html +6 -6
  16. nautobot/circuits/templates/circuits/inc/circuit_termination_speed_fragment.html +3 -3
  17. nautobot/circuits/templates/circuits/inc/speed_widget.html +13 -13
  18. nautobot/circuits/templates/circuits/provider_create.html +9 -9
  19. nautobot/circuits/tests/integration/test_circuit.py +19 -19
  20. nautobot/circuits/tests/integration/test_circuits_bulk_operations.py +3 -0
  21. nautobot/circuits/tests/integration/test_relationships.py +4 -12
  22. nautobot/circuits/views.py +0 -2
  23. nautobot/cloud/filters.py +1 -13
  24. nautobot/cloud/navigation.py +3 -1
  25. nautobot/cloud/templates/cloud/cloudnetwork_update.html +9 -9
  26. nautobot/cloud/templates/cloud/cloudservice_update.html +6 -6
  27. nautobot/core/api/fields.py +30 -2
  28. nautobot/core/api/schema.py +1 -1
  29. nautobot/core/api/serializers.py +9 -2
  30. nautobot/core/api/urls.py +2 -0
  31. nautobot/core/api/views.py +58 -37
  32. nautobot/core/apps/__init__.py +6 -12
  33. nautobot/core/branching.py +83 -0
  34. nautobot/core/celery/__init__.py +11 -6
  35. nautobot/core/celery/backends.py +2 -0
  36. nautobot/core/celery/encoders.py +7 -0
  37. nautobot/core/celery/task.py +44 -0
  38. nautobot/core/checks.py +60 -0
  39. nautobot/core/cli/bootstrap_v3_to_v5.py +776 -0
  40. nautobot/core/constants.py +9 -0
  41. nautobot/core/context_processors.py +84 -0
  42. nautobot/core/filters.py +131 -2
  43. nautobot/core/forms/__init__.py +4 -2
  44. nautobot/core/forms/fields.py +10 -8
  45. nautobot/core/forms/forms.py +21 -9
  46. nautobot/core/forms/search.py +0 -15
  47. nautobot/core/forms/widgets.py +3 -2
  48. nautobot/core/graphql/__init__.py +8 -26
  49. nautobot/core/graphql/generators.py +16 -6
  50. nautobot/core/graphql/schema.py +1 -1
  51. nautobot/core/graphql/schema_init.py +1 -2
  52. nautobot/core/graphql/utils.py +7 -9
  53. nautobot/core/jobs/__init__.py +158 -0
  54. nautobot/core/management/commands/generate_test_data.py +28 -9
  55. nautobot/core/models/__init__.py +17 -2
  56. nautobot/core/models/fields.py +3 -2
  57. nautobot/core/models/generics.py +9 -1
  58. nautobot/core/models/name_color_content_types.py +1 -1
  59. nautobot/core/models/ordering.py +7 -5
  60. nautobot/core/models/querysets.py +77 -2
  61. nautobot/core/models/tree_queries.py +6 -4
  62. nautobot/core/settings.py +30 -16
  63. nautobot/core/settings.yaml +13 -7
  64. nautobot/core/tables.py +114 -44
  65. nautobot/core/templates/403.html +1 -1
  66. nautobot/core/templates/403_csrf_failure.html +1 -1
  67. nautobot/core/templates/404.html +1 -1
  68. nautobot/core/templates/40x.html +8 -8
  69. nautobot/core/templates/500.html +10 -10
  70. nautobot/core/templates/about.html +13 -12
  71. nautobot/core/templates/admin/actions.html +1 -1
  72. nautobot/core/templates/admin/app_index.html +3 -3
  73. nautobot/core/templates/admin/base.html +45 -52
  74. nautobot/core/templates/admin/base_site.html +0 -9
  75. nautobot/core/templates/admin/change_form.html +5 -5
  76. nautobot/core/templates/admin/change_list.html +8 -12
  77. nautobot/core/templates/admin/change_list_results.html +3 -3
  78. nautobot/core/templates/admin/config/config.html +24 -24
  79. nautobot/core/templates/admin/delete_confirmation.html +5 -5
  80. nautobot/core/templates/admin/edit_inline/stacked.html +5 -5
  81. nautobot/core/templates/admin/edit_inline/tabular.html +3 -3
  82. nautobot/core/templates/admin/includes/fieldset.html +15 -15
  83. nautobot/core/templates/admin/index.html +8 -8
  84. nautobot/core/templates/admin/submit_line.html +5 -5
  85. nautobot/core/templates/base_django.html +36 -32
  86. nautobot/core/templates/buttons/add.html +1 -1
  87. nautobot/core/templates/buttons/consolidated_detail_view_action_buttons.html +2 -2
  88. nautobot/core/templates/buttons/export.html +17 -18
  89. nautobot/core/templates/buttons/job_import.html +2 -2
  90. nautobot/core/templates/components/breadcrumbs.html +19 -17
  91. nautobot/core/templates/components/button/dropdown.html +7 -5
  92. nautobot/core/templates/components/echarts.html +2 -0
  93. nautobot/core/templates/components/layout/one_over_two.html +3 -3
  94. nautobot/core/templates/components/layout/two_over_one.html +3 -3
  95. nautobot/core/templates/components/panel/body_content_data_table.html +2 -2
  96. nautobot/core/templates/components/panel/body_content_tags.html +1 -1
  97. nautobot/core/templates/components/panel/body_wrapper_generic.html +4 -2
  98. nautobot/core/templates/components/panel/body_wrapper_generic_table.html +1 -1
  99. nautobot/core/templates/components/panel/body_wrapper_key_value_table.html +5 -3
  100. nautobot/core/templates/components/panel/body_wrapper_table.html +4 -2
  101. nautobot/core/templates/components/panel/footer_contacts_table.html +4 -4
  102. nautobot/core/templates/components/panel/footer_content_table.html +3 -3
  103. nautobot/core/templates/components/panel/grouping_toggle.html +12 -11
  104. nautobot/core/templates/components/panel/header_extra_content_table.html +2 -11
  105. nautobot/core/templates/components/panel/panel.html +6 -3
  106. nautobot/core/templates/components/panel/stats_panel_body.html +9 -7
  107. nautobot/core/templates/components/tab/content_wrapper.html +29 -1
  108. nautobot/core/templates/components/tab/label_wrapper.html +10 -2
  109. nautobot/core/templates/components/tab/label_wrapper_distinct_view.html +11 -4
  110. nautobot/core/templates/echarts/echarts.html +20 -0
  111. nautobot/core/templates/exceptions/import_error.html +2 -2
  112. nautobot/core/templates/exceptions/permission_error.html +1 -1
  113. nautobot/core/templates/exceptions/programming_error.html +2 -2
  114. nautobot/core/templates/generic/object_bulk_add_component.html +29 -20
  115. nautobot/core/templates/generic/object_bulk_create.html +87 -75
  116. nautobot/core/templates/generic/object_bulk_destroy.html +35 -37
  117. nautobot/core/templates/generic/object_bulk_remove.html +30 -26
  118. nautobot/core/templates/generic/object_bulk_rename.html +53 -40
  119. nautobot/core/templates/generic/object_bulk_update.html +36 -29
  120. nautobot/core/templates/generic/object_create.html +40 -27
  121. nautobot/core/templates/generic/object_import.html +36 -24
  122. nautobot/core/templates/generic/object_list.html +279 -215
  123. nautobot/core/templates/generic/object_notes.html +21 -11
  124. nautobot/core/templates/generic/object_retrieve.html +161 -213
  125. nautobot/core/templates/graphene/graphiql.html +113 -60
  126. nautobot/core/templates/home.html +164 -87
  127. nautobot/core/templates/import_success.html +3 -2
  128. nautobot/core/templates/inc/ajax_loader.html +1 -1
  129. nautobot/core/templates/inc/computed_fields/panel_data.html +25 -13
  130. nautobot/core/templates/inc/created_updated.html +12 -7
  131. nautobot/core/templates/inc/custom_fields/panel_data.html +28 -16
  132. nautobot/core/templates/inc/custom_fields_panel.html +3 -3
  133. nautobot/core/templates/inc/dynamic_groups_panel.html +3 -3
  134. nautobot/core/templates/inc/extras_features_edit_form_fields.html +15 -15
  135. nautobot/core/templates/inc/footer.html +90 -40
  136. nautobot/core/templates/inc/form_static_field.html +6 -0
  137. nautobot/core/templates/inc/header.html +75 -0
  138. nautobot/core/templates/inc/header_banners.html +17 -0
  139. nautobot/core/templates/inc/header_messages.html +6 -0
  140. nautobot/core/templates/inc/image_attachments.html +9 -9
  141. nautobot/core/templates/inc/javascript.html +7 -24
  142. nautobot/core/templates/inc/media.html +4 -29
  143. nautobot/core/templates/inc/modal.html +2 -2
  144. nautobot/core/templates/inc/nav_favorites.html +27 -0
  145. nautobot/core/templates/inc/nav_menu.html +150 -108
  146. nautobot/core/templates/inc/object_details_advanced_panel.html +84 -71
  147. nautobot/core/templates/inc/page_title.html +23 -0
  148. nautobot/core/templates/inc/paginator.html +39 -28
  149. nautobot/core/templates/inc/relationships/panel_override.html +3 -3
  150. nautobot/core/templates/inc/relationships_panel.html +3 -3
  151. nautobot/core/templates/inc/relationships_table_rows.html +1 -1
  152. nautobot/core/templates/inc/search_panel.html +22 -16
  153. nautobot/core/templates/inc/table.html +61 -36
  154. nautobot/core/templates/inc/tenancy_form_panel.html +3 -3
  155. nautobot/core/templates/login.html +17 -59
  156. nautobot/core/templates/modals/modal_theme.html +12 -23
  157. nautobot/core/templates/nautobot_config.py.j2 +6 -5
  158. nautobot/core/templates/panel_table.html +8 -12
  159. nautobot/core/templates/redoc_ui.html +80 -0
  160. nautobot/core/templates/rest_framework/api.html +43 -21
  161. nautobot/core/templates/search.html +12 -13
  162. nautobot/core/templates/swagger_ui.html +19 -4
  163. nautobot/core/templates/system_jobs/import_objects.html +70 -58
  164. nautobot/core/templates/template.css +0 -6
  165. nautobot/core/templates/utilities/comment_form.html +34 -0
  166. nautobot/core/templates/utilities/confirmation_form.html +17 -9
  167. nautobot/core/templates/utilities/obj_table.html +19 -11
  168. nautobot/core/templates/utilities/render_field.html +27 -21
  169. nautobot/core/templates/utilities/render_jinja2.html +22 -25
  170. nautobot/core/templates/utilities/templatetags/advanced_filter_indicator.html +8 -0
  171. nautobot/core/templates/utilities/templatetags/badge.html +1 -1
  172. nautobot/core/templates/utilities/templatetags/dynamic_group_assignment_modal.html +2 -3
  173. nautobot/core/templates/utilities/templatetags/filter_form_drawer.html +482 -0
  174. nautobot/core/templates/utilities/templatetags/modal_form_as_dialog.html +14 -18
  175. nautobot/core/templates/utilities/templatetags/saved_view_modal.html +11 -11
  176. nautobot/core/templates/utilities/templatetags/table_config_form.html +51 -24
  177. nautobot/core/templates/utilities/templatetags/tag.html +1 -1
  178. nautobot/core/templates/utilities/templatetags/utilization_graph.html +3 -3
  179. nautobot/core/templates/utilities/theme_preview.html +829 -566
  180. nautobot/core/templates/utilities/worker_status.html +42 -41
  181. nautobot/core/templates/widgets/selectwithdisabled_option.html +3 -1
  182. nautobot/core/templates/widgets/sluginput.html +2 -2
  183. nautobot/core/templatetags/buttons.py +38 -40
  184. nautobot/core/templatetags/helpers.py +105 -28
  185. nautobot/core/templatetags/ui_framework.py +17 -0
  186. nautobot/core/testing/api.py +76 -12
  187. nautobot/core/testing/filters.py +11 -27
  188. nautobot/core/testing/integration.py +128 -10
  189. nautobot/core/testing/mixins.py +7 -4
  190. nautobot/core/testing/utils.py +28 -5
  191. nautobot/core/testing/views.py +125 -27
  192. nautobot/core/tests/integration/test_app_home.py +39 -35
  193. nautobot/core/tests/integration/test_app_navbar.py +60 -67
  194. nautobot/core/tests/integration/test_filters.py +123 -55
  195. nautobot/core/tests/integration/test_general_functionality.py +1 -1
  196. nautobot/core/tests/integration/test_home.py +10 -18
  197. nautobot/core/tests/integration/test_import_objects_ui.py +2 -9
  198. nautobot/core/tests/integration/test_navbar.py +41 -16
  199. nautobot/core/tests/integration/test_swagger.py +1 -7
  200. nautobot/core/tests/integration/test_theme.py +3 -0
  201. nautobot/core/tests/nautobot_config_without_example_apps.py +4 -0
  202. nautobot/core/tests/runner.py +6 -1
  203. nautobot/core/tests/test_api.py +5 -3
  204. nautobot/core/tests/test_branching.py +154 -0
  205. nautobot/core/tests/test_breadcrumbs.py +7 -8
  206. nautobot/core/tests/test_checks.py +28 -0
  207. nautobot/core/tests/test_commands.py +0 -41
  208. nautobot/core/tests/test_config.py +2 -1
  209. nautobot/core/tests/test_csv.py +4 -7
  210. nautobot/core/tests/test_filters.py +326 -318
  211. nautobot/core/tests/test_forms.py +19 -30
  212. nautobot/core/tests/test_graphql.py +67 -57
  213. nautobot/core/tests/test_models.py +1 -1
  214. nautobot/core/tests/test_nautobot_server.py +2 -0
  215. nautobot/core/tests/test_navigations.py +78 -10
  216. nautobot/core/tests/test_tables.py +3 -1
  217. nautobot/core/tests/test_templatetags_helpers.py +61 -21
  218. nautobot/core/tests/test_templatetags_ui_framework.py +36 -18
  219. nautobot/core/tests/test_ui.py +207 -2
  220. nautobot/core/tests/test_utils.py +147 -2
  221. nautobot/core/tests/test_views.py +201 -64
  222. nautobot/core/tests/test_views_utils.py +1 -1
  223. nautobot/core/ui/breadcrumbs.py +2 -12
  224. nautobot/core/ui/choices.py +190 -0
  225. nautobot/core/ui/constants.py +86 -0
  226. nautobot/core/ui/echarts.py +474 -0
  227. nautobot/core/ui/nav.py +5 -1
  228. nautobot/core/ui/object_detail.py +180 -16
  229. nautobot/core/urls.py +13 -1
  230. nautobot/core/utils/cache.py +71 -0
  231. nautobot/core/utils/data.py +8 -5
  232. nautobot/core/utils/filtering.py +8 -2
  233. nautobot/core/utils/git.py +3 -3
  234. nautobot/core/utils/lookup.py +87 -13
  235. nautobot/core/utils/migrations.py +22 -0
  236. nautobot/core/utils/module_loading.py +26 -0
  237. nautobot/core/utils/permissions.py +9 -5
  238. nautobot/core/views/__init__.py +114 -63
  239. nautobot/core/views/generic.py +34 -27
  240. nautobot/core/views/mixins.py +49 -27
  241. nautobot/core/views/renderers.py +3 -5
  242. nautobot/core/views/utils.py +10 -5
  243. nautobot/core/views/viewsets.py +2 -1
  244. nautobot/data_validation/__init__.py +0 -0
  245. nautobot/data_validation/api/__init__.py +1 -0
  246. nautobot/data_validation/api/serializers.py +80 -0
  247. nautobot/data_validation/api/urls.py +20 -0
  248. nautobot/data_validation/api/views.py +44 -0
  249. nautobot/data_validation/apps.py +18 -0
  250. nautobot/data_validation/custom_validators.py +330 -0
  251. nautobot/data_validation/filters.py +133 -0
  252. nautobot/data_validation/form_mixin.py +25 -0
  253. nautobot/data_validation/forms.py +342 -0
  254. nautobot/data_validation/migrations/0001_initial.py +224 -0
  255. nautobot/data_validation/migrations/0002_data_migration_from_app.py +324 -0
  256. nautobot/data_validation/migrations/__init__.py +0 -0
  257. nautobot/data_validation/models.py +361 -0
  258. nautobot/data_validation/navigation.py +74 -0
  259. nautobot/data_validation/signals.py +30 -0
  260. nautobot/data_validation/tables.py +259 -0
  261. nautobot/data_validation/templates/data_validation/datacompliance_retrieve.html +1 -0
  262. nautobot/data_validation/templates/data_validation/datacompliance_tab.html +11 -0
  263. nautobot/data_validation/templates/data_validation/device_constraints.html +61 -0
  264. nautobot/data_validation/tests/__init__.py +20 -0
  265. nautobot/data_validation/tests/migrations/__init__.py +0 -0
  266. nautobot/data_validation/tests/migrations/test_migrations.py +489 -0
  267. nautobot/data_validation/tests/test_api.py +238 -0
  268. nautobot/data_validation/tests/test_custom_validators.py +423 -0
  269. nautobot/data_validation/tests/test_data_compliance_rules.py +85 -0
  270. nautobot/data_validation/tests/test_filters.py +240 -0
  271. nautobot/data_validation/tests/test_form_mixin.py +115 -0
  272. nautobot/data_validation/tests/test_models.py +393 -0
  273. nautobot/data_validation/tests/test_views.py +435 -0
  274. nautobot/data_validation/urls.py +21 -0
  275. nautobot/data_validation/views.py +227 -0
  276. nautobot/dcim/api/serializers.py +10 -13
  277. nautobot/dcim/api/urls.py +2 -0
  278. nautobot/dcim/api/views.py +7 -0
  279. nautobot/dcim/apps.py +4 -0
  280. nautobot/dcim/choices.py +16 -0
  281. nautobot/dcim/custom_validators.py +84 -0
  282. nautobot/dcim/filter_mixins.py +353 -4
  283. nautobot/dcim/{filters/__init__.py → filters.py} +70 -157
  284. nautobot/dcim/forms.py +12 -6
  285. nautobot/dcim/graphql/types.py +1 -0
  286. nautobot/dcim/migrations/0075_add_deviceclusterassignment.py +52 -0
  287. nautobot/dcim/migrations/0076_device_cluster_to_clusters_data_migration.py +40 -0
  288. nautobot/dcim/migrations/0077_remove_device_cluster.py +14 -0
  289. nautobot/dcim/migrations/0078_remove_device_location_tenant_name_uniqueness.py +16 -0
  290. nautobot/dcim/migrations/0079_device_name_data_migration.py +59 -0
  291. nautobot/dcim/models/__init__.py +2 -0
  292. nautobot/dcim/models/device_components.py +3 -1
  293. nautobot/dcim/models/devices.py +115 -51
  294. nautobot/dcim/navigation.py +7 -3
  295. nautobot/dcim/querysets.py +6 -0
  296. nautobot/dcim/signals.py +19 -0
  297. nautobot/dcim/tables/devices.py +9 -5
  298. nautobot/dcim/tables/template_code.py +191 -102
  299. nautobot/dcim/templates/dcim/cable.html +1 -1
  300. nautobot/dcim/templates/dcim/cable_connect.html +62 -146
  301. nautobot/dcim/templates/dcim/cable_retrieve.html +10 -10
  302. nautobot/dcim/templates/dcim/cable_trace.html +15 -17
  303. nautobot/dcim/templates/dcim/console_port_connection_list.html +2 -2
  304. nautobot/dcim/templates/dcim/consoleport.html +18 -17
  305. nautobot/dcim/templates/dcim/consoleserverport.html +18 -17
  306. nautobot/dcim/templates/dcim/controller_create.html +12 -8
  307. nautobot/dcim/templates/dcim/controller_wirelessnetworks.html +1 -1
  308. nautobot/dcim/templates/dcim/controllermanageddevicegroup_create.html +6 -6
  309. nautobot/dcim/templates/dcim/controllermanageddevicegroup_retrieve.html +1 -1
  310. nautobot/dcim/templates/dcim/device/config.html +17 -19
  311. nautobot/dcim/templates/dcim/device/lldp_neighbors.html +4 -4
  312. nautobot/dcim/templates/dcim/device/status.html +20 -20
  313. nautobot/dcim/templates/dcim/device_component_add.html +24 -15
  314. nautobot/dcim/templates/dcim/device_create.html +120 -120
  315. nautobot/dcim/templates/dcim/device_list.html +75 -12
  316. nautobot/dcim/templates/dcim/devicebay.html +7 -7
  317. nautobot/dcim/templates/dcim/devicebay_populate.html +29 -23
  318. nautobot/dcim/templates/dcim/deviceredundancygroup_create.html +6 -6
  319. nautobot/dcim/templates/dcim/devicetype.html +1 -1
  320. nautobot/dcim/templates/dcim/devicetype_component_add.html +25 -19
  321. nautobot/dcim/templates/dcim/devicetype_list.html +4 -4
  322. nautobot/dcim/templates/dcim/devicetype_update.html +9 -9
  323. nautobot/dcim/templates/dcim/footer_convert_to_contact_or_team_record.html +3 -3
  324. nautobot/dcim/templates/dcim/frontport.html +21 -20
  325. nautobot/dcim/templates/dcim/inc/cable_form.html +7 -7
  326. nautobot/dcim/templates/dcim/inc/cable_termination.html +1 -1
  327. nautobot/dcim/templates/dcim/inc/cable_toggle_buttons.html +18 -9
  328. nautobot/dcim/templates/dcim/inc/detail_softwareversion_softwareimagefile_rows.html +1 -1
  329. nautobot/dcim/templates/dcim/inc/device_interface_filter.html +1 -1
  330. nautobot/dcim/templates/dcim/inc/devicetype_component_table.html +10 -10
  331. nautobot/dcim/templates/dcim/inc/edit_form_softwareversion_js.html +2 -2
  332. nautobot/dcim/templates/dcim/inc/homepage_connections.html +2 -2
  333. nautobot/dcim/templates/dcim/inc/moduletype_component_table.html +10 -10
  334. nautobot/dcim/templates/dcim/inc/rack_elevation.html +2 -2
  335. nautobot/dcim/templates/dcim/interface.html +42 -22
  336. nautobot/dcim/templates/dcim/interface_connection_list.html +2 -2
  337. nautobot/dcim/templates/dcim/interface_edit.html +26 -11
  338. nautobot/dcim/templates/dcim/interfaceredundancygroupassociation_create.html +3 -3
  339. nautobot/dcim/templates/dcim/inventoryitem.html +3 -3
  340. nautobot/dcim/templates/dcim/inventoryitem_add.html +21 -10
  341. nautobot/dcim/templates/dcim/inventoryitem_bulk_delete.html +1 -1
  342. nautobot/dcim/templates/dcim/inventoryitem_edit.html +6 -4
  343. nautobot/dcim/templates/dcim/location.html +1 -1
  344. nautobot/dcim/templates/dcim/location_migrate_data_to_contact.html +24 -18
  345. nautobot/dcim/templates/dcim/location_retrieve.html +1 -1
  346. nautobot/dcim/templates/dcim/location_update.html +9 -9
  347. nautobot/dcim/templates/dcim/locationtype.html +0 -1
  348. nautobot/dcim/templates/dcim/module/base.html +67 -27
  349. nautobot/dcim/templates/dcim/module_consoleports.html +13 -15
  350. nautobot/dcim/templates/dcim/module_consoleserverports.html +13 -15
  351. nautobot/dcim/templates/dcim/module_frontports.html +13 -15
  352. nautobot/dcim/templates/dcim/module_interfaces.html +14 -16
  353. nautobot/dcim/templates/dcim/module_list.html +59 -10
  354. nautobot/dcim/templates/dcim/module_modulebays.html +12 -14
  355. nautobot/dcim/templates/dcim/module_poweroutlets.html +13 -15
  356. nautobot/dcim/templates/dcim/module_powerports.html +13 -15
  357. nautobot/dcim/templates/dcim/module_rearports.html +13 -15
  358. nautobot/dcim/templates/dcim/module_retrieve.html +3 -3
  359. nautobot/dcim/templates/dcim/module_update.html +15 -9
  360. nautobot/dcim/templates/dcim/modulebay_retrieve.html +0 -93
  361. nautobot/dcim/templates/dcim/modulefamily_retrieve.html +7 -7
  362. nautobot/dcim/templates/dcim/moduletype_list.html +2 -2
  363. nautobot/dcim/templates/dcim/moduletype_retrieve.html +74 -35
  364. nautobot/dcim/templates/dcim/platform_create.html +9 -9
  365. nautobot/dcim/templates/dcim/power_port_connection_list.html +3 -3
  366. nautobot/dcim/templates/dcim/powerfeed.html +1 -1
  367. nautobot/dcim/templates/dcim/powerfeed_edit.html +15 -15
  368. nautobot/dcim/templates/dcim/poweroutlet.html +13 -13
  369. nautobot/dcim/templates/dcim/powerpanel.html +1 -1
  370. nautobot/dcim/templates/dcim/powerport.html +17 -16
  371. nautobot/dcim/templates/dcim/rack.html +1 -1
  372. nautobot/dcim/templates/dcim/rack_elevation.html +2 -2
  373. nautobot/dcim/templates/dcim/rack_elevation_list.html +21 -9
  374. nautobot/dcim/templates/dcim/rack_retrieve.html +75 -57
  375. nautobot/dcim/templates/dcim/rack_update.html +14 -14
  376. nautobot/dcim/templates/dcim/rackreservation.html +1 -1
  377. nautobot/dcim/templates/dcim/rackreservation_edit.html +6 -6
  378. nautobot/dcim/templates/dcim/rearport.html +19 -18
  379. nautobot/dcim/templates/dcim/trace/cable.html +1 -1
  380. nautobot/dcim/templates/dcim/trace/circuit.html +1 -1
  381. nautobot/dcim/templates/dcim/trace/device.html +1 -1
  382. nautobot/dcim/templates/dcim/trace/powerpanel.html +1 -1
  383. nautobot/dcim/templates/dcim/trace/termination.html +1 -1
  384. nautobot/dcim/templates/dcim/virtualchassis.html +1 -1
  385. nautobot/dcim/templates/dcim/virtualchassis_add_member.html +25 -16
  386. nautobot/dcim/templates/dcim/virtualchassis_create.html +6 -6
  387. nautobot/dcim/templates/dcim/virtualchassis_edit.html +1 -1
  388. nautobot/dcim/templates/dcim/virtualchassis_retrieve.html +1 -1
  389. nautobot/dcim/templates/dcim/virtualchassis_update.html +36 -22
  390. nautobot/dcim/templates/dcim/virtualdevicecontext_update.html +9 -9
  391. nautobot/dcim/tests/integration/test_controller.py +6 -6
  392. nautobot/dcim/tests/integration/test_controller_managed_device_group.py +7 -7
  393. nautobot/dcim/tests/integration/test_create_device.py +9 -9
  394. nautobot/dcim/tests/integration/test_device_bulk_operations.py +7 -2
  395. nautobot/dcim/tests/integration/test_fileinputpicker.py +5 -7
  396. nautobot/dcim/tests/integration/test_location_bulk_operations.py +2 -0
  397. nautobot/dcim/tests/integration/test_module_bay_position.py +4 -1
  398. nautobot/dcim/tests/test_api.py +86 -6
  399. nautobot/dcim/tests/test_custom_validators.py +229 -0
  400. nautobot/dcim/tests/test_filters.py +159 -110
  401. nautobot/dcim/tests/test_graphql.py +32 -36
  402. nautobot/dcim/tests/test_jobs.py +1 -1
  403. nautobot/dcim/tests/test_models.py +229 -1
  404. nautobot/dcim/tests/test_views.py +31 -20
  405. nautobot/dcim/utils.py +3 -3
  406. nautobot/dcim/views.py +77 -41
  407. nautobot/extras/api/serializers.py +83 -19
  408. nautobot/extras/api/urls.py +7 -0
  409. nautobot/extras/api/views.py +243 -140
  410. nautobot/extras/choices.py +34 -13
  411. nautobot/extras/constants.py +1 -1
  412. nautobot/extras/context_managers.py +26 -26
  413. nautobot/extras/datasources/git.py +22 -0
  414. nautobot/extras/datasources/registry.py +3 -0
  415. nautobot/extras/exceptions.py +5 -0
  416. nautobot/extras/factory.py +11 -1
  417. nautobot/extras/{filters/mixins.py → filter_mixins.py} +4 -3
  418. nautobot/extras/{filters/__init__.py → filters.py} +203 -58
  419. nautobot/extras/forms/base.py +2 -1
  420. nautobot/extras/forms/forms.py +225 -20
  421. nautobot/extras/forms/mixins.py +0 -41
  422. nautobot/extras/homepage.py +21 -2
  423. nautobot/extras/jobs.py +2 -8
  424. nautobot/extras/jobs_ui.py +2 -2
  425. nautobot/extras/management/__init__.py +9 -0
  426. nautobot/extras/managers.py +31 -22
  427. nautobot/extras/migrations/0126_approval_workflow_pre_check.py +58 -0
  428. nautobot/extras/migrations/0127_approval_workflow_models.py +266 -0
  429. nautobot/extras/migrations/0128_remove_job_approval_required_and_more.py +29 -0
  430. nautobot/extras/migrations/0129_jobresult_debug_log_count_jobresult_error_log_count_and_more.py +37 -0
  431. nautobot/extras/migrations/0130_jobresult_generate_log_entry_counts.py +42 -0
  432. nautobot/extras/models/__init__.py +14 -3
  433. nautobot/extras/models/approvals.py +556 -0
  434. nautobot/extras/models/change_logging.py +1 -0
  435. nautobot/extras/models/contacts.py +2 -0
  436. nautobot/extras/models/customfields.py +57 -22
  437. nautobot/extras/models/datasources.py +21 -0
  438. nautobot/extras/models/groups.py +2 -0
  439. nautobot/extras/models/jobs.py +122 -39
  440. nautobot/extras/models/metadata.py +2 -3
  441. nautobot/extras/models/mixins.py +129 -1
  442. nautobot/extras/models/models.py +22 -14
  443. nautobot/extras/models/relationships.py +47 -10
  444. nautobot/extras/models/secrets.py +1 -0
  445. nautobot/extras/models/statuses.py +0 -15
  446. nautobot/extras/models/tags.py +1 -1
  447. nautobot/extras/navigation.py +42 -15
  448. nautobot/extras/plugins/__init__.py +33 -55
  449. nautobot/extras/plugins/marketplace_manifest.yml +1 -23
  450. nautobot/extras/plugins/tables.py +8 -6
  451. nautobot/extras/plugins/urls.py +2 -21
  452. nautobot/extras/plugins/utils.py +1 -33
  453. nautobot/extras/plugins/validators.py +10 -10
  454. nautobot/extras/plugins/views.py +1 -5
  455. nautobot/extras/querysets.py +17 -21
  456. nautobot/extras/signals.py +23 -8
  457. nautobot/extras/tables.py +420 -99
  458. nautobot/extras/templates/extras/approval_dashboard.html +15 -0
  459. nautobot/extras/templates/extras/approval_workflow/approve.html +11 -0
  460. nautobot/extras/templates/extras/approval_workflow/comment.html +9 -0
  461. nautobot/extras/templates/extras/approval_workflow/deny.html +10 -0
  462. nautobot/extras/templates/extras/approvalworkflowdefinition_update.html +77 -0
  463. nautobot/extras/templates/extras/approvalworkflowstage_retrieve.html +29 -0
  464. nautobot/extras/templates/extras/configcontext_update.html +12 -12
  465. nautobot/extras/templates/extras/configcontextschema.html +1 -1
  466. nautobot/extras/templates/extras/configcontextschema_retrieve.html +9 -9
  467. nautobot/extras/templates/extras/configcontextschema_update.html +6 -6
  468. nautobot/extras/templates/extras/configcontextschema_validation.html +2 -2
  469. nautobot/extras/templates/extras/customfield_update.html +12 -12
  470. nautobot/extras/templates/extras/dynamicgroup.html +1 -1
  471. nautobot/extras/templates/extras/dynamicgroup_edit.html +1 -1
  472. nautobot/extras/templates/extras/dynamicgroup_retrieve.html +17 -17
  473. nautobot/extras/templates/extras/dynamicgroup_update.html +24 -24
  474. nautobot/extras/templates/extras/externalintegration_update.html +6 -6
  475. nautobot/extras/templates/extras/gitrepository.html +1 -1
  476. nautobot/extras/templates/extras/gitrepository_object_edit.html +1 -1
  477. nautobot/extras/templates/extras/gitrepository_result.html +1 -1
  478. nautobot/extras/templates/extras/gitrepository_retrieve.html +12 -12
  479. nautobot/extras/templates/extras/gitrepository_update.html +25 -7
  480. nautobot/extras/templates/extras/graphqlquery_retrieve.html +1 -1
  481. nautobot/extras/templates/extras/inc/approval_buttons_column.html +38 -0
  482. nautobot/extras/templates/extras/inc/bulk_edit_overridable_field.html +14 -13
  483. nautobot/extras/templates/extras/inc/configcontext_format.html +11 -4
  484. nautobot/extras/templates/extras/inc/graphqlquery_execute.html +7 -7
  485. nautobot/extras/templates/extras/inc/job_label.html +5 -5
  486. nautobot/extras/templates/extras/inc/job_table.html +23 -10
  487. nautobot/extras/templates/extras/inc/job_tiles.html +33 -21
  488. nautobot/extras/templates/extras/inc/jobresult.html +6 -6
  489. nautobot/extras/templates/extras/inc/json_format.html +11 -4
  490. nautobot/extras/templates/extras/inc/object_contact_header.html +6 -6
  491. nautobot/extras/templates/extras/inc/overridable_field.html +16 -15
  492. nautobot/extras/templates/extras/inc/panel_approvalworkflowstage.html +34 -0
  493. nautobot/extras/templates/extras/inc/panel_changelog.html +9 -9
  494. nautobot/extras/templates/extras/inc/panel_jobhistory.html +8 -6
  495. nautobot/extras/templates/extras/inc/tags_panel.html +3 -3
  496. nautobot/extras/templates/extras/job.html +154 -155
  497. nautobot/extras/templates/extras/job_approval_confirmation.html +4 -27
  498. nautobot/extras/templates/extras/job_bulk_edit.html +18 -1
  499. nautobot/extras/templates/extras/job_detail.html +1 -1
  500. nautobot/extras/templates/extras/job_edit.html +69 -64
  501. nautobot/extras/templates/extras/job_list.html +37 -60
  502. nautobot/extras/templates/extras/jobresult.html +1 -1
  503. nautobot/extras/templates/extras/jobresult_retrieve.html +17 -17
  504. nautobot/extras/templates/extras/marketplace.html +62 -71
  505. nautobot/extras/templates/extras/metadatatype_create.html +9 -9
  506. nautobot/extras/templates/extras/note.html +1 -1
  507. nautobot/extras/templates/extras/object_approvalworkflow.html +36 -0
  508. nautobot/extras/templates/extras/object_assign_contact_or_team.html +16 -7
  509. nautobot/extras/templates/extras/object_configcontext.html +20 -20
  510. nautobot/extras/templates/extras/object_new_contact.html +6 -6
  511. nautobot/extras/templates/extras/object_new_team.html +6 -6
  512. nautobot/extras/templates/extras/objectchange.html +1 -1
  513. nautobot/extras/templates/extras/objectchange_retrieve.html +37 -56
  514. nautobot/extras/templates/extras/plugin_detail.html +40 -41
  515. nautobot/extras/templates/extras/plugins_list.html +23 -38
  516. nautobot/extras/templates/extras/plugins_tiles.html +28 -28
  517. nautobot/extras/templates/extras/role_retrieve.html +112 -48
  518. nautobot/extras/templates/extras/scheduledjob.html +25 -28
  519. nautobot/extras/templates/extras/secret_create.html +11 -11
  520. nautobot/extras/templates/extras/secretsgroup_update.html +6 -6
  521. nautobot/extras/templates/extras/staticgroupassociation_retrieve.html +3 -3
  522. nautobot/extras/templates/extras/status.html +1 -1
  523. nautobot/extras/templates/extras/tag.html +1 -1
  524. nautobot/extras/templates/extras/tag_update.html +3 -3
  525. nautobot/extras/templates/extras/templatetags/log_level.html +1 -1
  526. nautobot/extras/templates/extras/templatetags/plugin_object_detail_tabs.html +2 -2
  527. nautobot/extras/templates/extras/webhook.html +12 -12
  528. nautobot/extras/templatetags/approvals.py +19 -0
  529. nautobot/extras/templatetags/custom_links.py +12 -12
  530. nautobot/extras/templatetags/job_buttons.py +14 -12
  531. nautobot/extras/test_jobs/invalid_import.py +9 -0
  532. nautobot/extras/test_jobs/log_counts_by_level.py +23 -0
  533. nautobot/extras/test_jobs/missing_import.py +11 -0
  534. nautobot/extras/tests/integration/test_computedfields.py +5 -8
  535. nautobot/extras/tests/integration/test_configcontextschema.py +43 -48
  536. nautobot/extras/tests/integration/test_customfields.py +33 -33
  537. nautobot/extras/tests/integration/test_dynamicgroups.py +5 -10
  538. nautobot/extras/tests/integration/test_jobs.py +2 -4
  539. nautobot/extras/tests/integration/test_notes.py +3 -9
  540. nautobot/extras/tests/integration/test_plugin_banner.py +3 -0
  541. nautobot/extras/tests/integration/test_plugins.py +35 -27
  542. nautobot/extras/tests/integration/test_relationships.py +7 -11
  543. nautobot/extras/tests/integration/test_tagfilter.py +3 -11
  544. nautobot/extras/tests/test_api.py +786 -242
  545. nautobot/extras/tests/test_approvals.py +715 -0
  546. nautobot/extras/tests/test_changelog.py +18 -14
  547. nautobot/extras/tests/test_customfields.py +14 -13
  548. nautobot/extras/tests/test_datasources.py +1 -1
  549. nautobot/extras/tests/test_dynamicgroups.py +9 -4
  550. nautobot/extras/tests/test_filters.py +443 -13
  551. nautobot/extras/tests/test_forms.py +18 -57
  552. nautobot/extras/tests/test_jobs.py +25 -4
  553. nautobot/extras/tests/test_migrations.py +81 -1
  554. nautobot/extras/tests/test_models.py +378 -47
  555. nautobot/extras/tests/test_plugins.py +47 -13
  556. nautobot/extras/tests/test_relationships.py +7 -2
  557. nautobot/extras/tests/test_utils.py +2 -0
  558. nautobot/extras/tests/test_views.py +780 -493
  559. nautobot/extras/urls.py +36 -12
  560. nautobot/extras/utils.py +58 -12
  561. nautobot/extras/views.py +668 -209
  562. nautobot/ipam/factory.py +7 -0
  563. nautobot/ipam/filter_mixins.py +38 -0
  564. nautobot/ipam/filters.py +35 -71
  565. nautobot/ipam/formfields.py +1 -1
  566. nautobot/ipam/forms.py +6 -3
  567. nautobot/ipam/migrations/0030_ipam__namespaces.py +13 -0
  568. nautobot/ipam/migrations/0031_ipam___data_migrations.py +4 -1
  569. nautobot/ipam/migrations/0054_namespace_tenant.py +25 -0
  570. nautobot/ipam/models.py +29 -2
  571. nautobot/ipam/navigation.py +3 -1
  572. nautobot/ipam/querysets.py +1 -2
  573. nautobot/ipam/tables.py +26 -17
  574. nautobot/ipam/templates/ipam/inc/ipadress_edit_header.html +6 -6
  575. nautobot/ipam/templates/ipam/inc/service.html +8 -8
  576. nautobot/ipam/templates/ipam/inc/toggle_available.html +10 -10
  577. nautobot/ipam/templates/ipam/inc/vlangroup_header.html +3 -2
  578. nautobot/ipam/templates/ipam/ipaddress.html +27 -13
  579. nautobot/ipam/templates/ipam/ipaddress_assign.html +31 -24
  580. nautobot/ipam/templates/ipam/ipaddress_bulk_add.html +3 -3
  581. nautobot/ipam/templates/ipam/ipaddress_edit.html +9 -9
  582. nautobot/ipam/templates/ipam/ipaddress_interfaces.html +7 -9
  583. nautobot/ipam/templates/ipam/ipaddress_merge.html +195 -186
  584. nautobot/ipam/templates/ipam/ipaddress_vm_interfaces.html +7 -9
  585. nautobot/ipam/templates/ipam/ipaddresstointerface_retrieve.html +7 -5
  586. nautobot/ipam/templates/ipam/namespace_ip_addresses.html +1 -1
  587. nautobot/ipam/templates/ipam/namespace_prefixes.html +1 -1
  588. nautobot/ipam/templates/ipam/namespace_update.html +15 -0
  589. nautobot/ipam/templates/ipam/namespace_vrfs.html +1 -1
  590. nautobot/ipam/templates/ipam/prefix_create.html +9 -9
  591. nautobot/ipam/templates/ipam/prefix_list.html +15 -14
  592. nautobot/ipam/templates/ipam/prefix_retrieve.html +0 -1
  593. nautobot/ipam/templates/ipam/vlan.html +1 -1
  594. nautobot/ipam/templates/ipam/vlan_interfaces.html +1 -1
  595. nautobot/ipam/templates/ipam/vlan_update.html +6 -6
  596. nautobot/ipam/templates/ipam/vlan_vminterfaces.html +1 -1
  597. nautobot/ipam/templates/ipam/vrf_edit.html +15 -15
  598. nautobot/ipam/tests/integration/test_prefixes.py +5 -13
  599. nautobot/ipam/tests/migration/test_migrations.py +89 -0
  600. nautobot/ipam/tests/test_api.py +20 -7
  601. nautobot/ipam/tests/test_filters.py +10 -0
  602. nautobot/ipam/tests/test_forms.py +1 -1
  603. nautobot/ipam/tests/test_models.py +1 -1
  604. nautobot/ipam/tests/test_tables.py +1 -2
  605. nautobot/ipam/tests/test_utils.py +1 -1
  606. nautobot/ipam/tests/test_views.py +24 -21
  607. nautobot/ipam/ui.py +0 -17
  608. nautobot/ipam/utils/migrations.py +16 -2
  609. nautobot/ipam/utils/testing.py +9 -3
  610. nautobot/ipam/views.py +49 -7
  611. nautobot/project-static/dist/css/graphql-libraries.css +655 -0
  612. nautobot/project-static/dist/css/graphql-libraries.css.map +1 -0
  613. nautobot/project-static/dist/css/materialdesignicons.css +3 -0
  614. nautobot/project-static/dist/css/materialdesignicons.css.map +1 -0
  615. nautobot/project-static/dist/css/nautobot.css +13 -0
  616. nautobot/project-static/dist/css/nautobot.css.map +1 -0
  617. nautobot/project-static/dist/js/graphql-libraries.js +3 -0
  618. nautobot/project-static/dist/js/graphql-libraries.js.LICENSE.txt +62 -0
  619. nautobot/project-static/dist/js/graphql-libraries.js.map +1 -0
  620. nautobot/project-static/dist/js/libraries.js +3 -0
  621. nautobot/project-static/dist/js/libraries.js.LICENSE.txt +65 -0
  622. nautobot/project-static/dist/js/libraries.js.map +1 -0
  623. nautobot/project-static/dist/js/materialdesignicons.js +0 -0
  624. nautobot/project-static/dist/js/nautobot-graphiql.js +2 -0
  625. nautobot/project-static/dist/js/nautobot-graphiql.js.map +1 -0
  626. nautobot/project-static/dist/js/nautobot.js +2 -0
  627. nautobot/project-static/dist/js/nautobot.js.map +1 -0
  628. nautobot/project-static/fonts/Montserrat-v30-Bold.woff2 +0 -0
  629. nautobot/project-static/fonts/Montserrat-v30-Light.woff2 +0 -0
  630. nautobot/project-static/fonts/Montserrat-v30-Regular.woff2 +0 -0
  631. nautobot/project-static/fonts/Roboto-v48-Bold.woff2 +0 -0
  632. nautobot/project-static/fonts/Roboto-v48-Light.woff2 +0 -0
  633. nautobot/project-static/fonts/Roboto-v48-Regular.woff2 +0 -0
  634. nautobot/project-static/img/jinja_logo.svg +21 -92
  635. nautobot/project-static/js/cabletrace.js +1 -1
  636. nautobot/project-static/js/editor.js +4 -4
  637. nautobot/project-static/js/forms.js +67 -717
  638. nautobot/project-static/js/job_result.js +2 -2
  639. nautobot/project-static/nautobot-icons/360-degrees.svg +3 -0
  640. nautobot/project-static/nautobot-icons/arrow-decision.svg +3 -0
  641. nautobot/project-static/nautobot-icons/arrows-expand-rec.svg +3 -0
  642. nautobot/project-static/nautobot-icons/arrows-move-2-rec.svg +3 -0
  643. nautobot/project-static/nautobot-icons/arrows-move-rec.svg +3 -0
  644. nautobot/project-static/nautobot-icons/atom.svg +3 -0
  645. nautobot/project-static/nautobot-icons/battery-3.svg +3 -0
  646. nautobot/project-static/nautobot-icons/branch.svg +3 -0
  647. nautobot/project-static/nautobot-icons/briefcase-2.svg +3 -0
  648. nautobot/project-static/nautobot-icons/cable-data-2.svg +3 -0
  649. nautobot/project-static/nautobot-icons/cable-data.svg +3 -0
  650. nautobot/project-static/nautobot-icons/cast.svg +3 -0
  651. nautobot/project-static/nautobot-icons/check-circle.svg +3 -0
  652. nautobot/project-static/nautobot-icons/checkbox-circle.svg +3 -0
  653. nautobot/project-static/nautobot-icons/checkbox-rec.svg +3 -0
  654. nautobot/project-static/nautobot-icons/cloud-check.svg +3 -0
  655. nautobot/project-static/nautobot-icons/cloud-lightning.svg +3 -0
  656. nautobot/project-static/nautobot-icons/cloud-upload.svg +3 -0
  657. nautobot/project-static/nautobot-icons/cloud.svg +3 -0
  658. nautobot/project-static/nautobot-icons/compass.svg +3 -0
  659. nautobot/project-static/nautobot-icons/control-panel.svg +3 -0
  660. nautobot/project-static/nautobot-icons/credit-card.svg +3 -0
  661. nautobot/project-static/nautobot-icons/device-lifecycle.svg +3 -0
  662. nautobot/project-static/nautobot-icons/direction.svg +3 -0
  663. nautobot/project-static/nautobot-icons/elements.svg +3 -0
  664. nautobot/project-static/nautobot-icons/extensibility.svg +3 -0
  665. nautobot/project-static/nautobot-icons/globe-2.svg +3 -0
  666. nautobot/project-static/nautobot-icons/globe.svg +3 -0
  667. nautobot/project-static/nautobot-icons/hammer.svg +3 -0
  668. nautobot/project-static/nautobot-icons/history.svg +3 -0
  669. nautobot/project-static/nautobot-icons/ip.svg +3 -0
  670. nautobot/project-static/nautobot-icons/laptop.svg +3 -0
  671. nautobot/project-static/nautobot-icons/lightning.svg +3 -0
  672. nautobot/project-static/nautobot-icons/list-unordered.svg +3 -0
  673. nautobot/project-static/nautobot-icons/map-view.svg +3 -0
  674. nautobot/project-static/nautobot-icons/organization.svg +3 -0
  675. nautobot/project-static/nautobot-icons/pin-2.svg +3 -0
  676. nautobot/project-static/nautobot-icons/pin-3.svg +3 -0
  677. nautobot/project-static/nautobot-icons/plug.svg +3 -0
  678. nautobot/project-static/nautobot-icons/refresh-cw.svg +3 -0
  679. nautobot/project-static/nautobot-icons/rocket-2.svg +3 -0
  680. nautobot/project-static/nautobot-icons/rotate-cw.svg +3 -0
  681. nautobot/project-static/nautobot-icons/route.svg +3 -0
  682. nautobot/project-static/nautobot-icons/secrets.svg +3 -0
  683. nautobot/project-static/nautobot-icons/security.svg +3 -0
  684. nautobot/project-static/nautobot-icons/server-2.svg +3 -0
  685. nautobot/project-static/nautobot-icons/server.svg +3 -0
  686. nautobot/project-static/nautobot-icons/share.svg +3 -0
  687. nautobot/project-static/nautobot-icons/shield-check.svg +3 -0
  688. nautobot/project-static/nautobot-icons/sitemap-outline.svg +3 -0
  689. nautobot/project-static/nautobot-icons/sliders-vert-2.svg +3 -0
  690. nautobot/project-static/nautobot-icons/sliders-vert.svg +3 -0
  691. nautobot/project-static/nautobot-icons/star-filled.svg +3 -0
  692. nautobot/project-static/nautobot-icons/star.svg +3 -0
  693. nautobot/project-static/nautobot-icons/transform.svg +3 -0
  694. nautobot/project-static/nautobot-icons/wifi.svg +3 -0
  695. nautobot/tenancy/api/serializers.py +1 -0
  696. nautobot/tenancy/api/views.py +2 -1
  697. nautobot/tenancy/{filters/__init__.py → filters.py} +2 -10
  698. nautobot/tenancy/navigation.py +3 -1
  699. nautobot/tenancy/templates/tenancy/tenant_create.html +6 -6
  700. nautobot/tenancy/tests/test_filters.py +0 -2
  701. nautobot/tenancy/views.py +2 -1
  702. nautobot/ui/.gitignore +137 -0
  703. nautobot/ui/.node-version +1 -0
  704. nautobot/ui/.prettierignore +3 -0
  705. nautobot/ui/eslint.config.js +33 -0
  706. nautobot/ui/package-lock.json +6594 -0
  707. nautobot/ui/package.json +67 -0
  708. nautobot/ui/prettier.config.js +9 -0
  709. nautobot/ui/src/js/collapse.js +69 -0
  710. nautobot/ui/src/js/cookie.js +31 -0
  711. nautobot/ui/src/js/draggable.js +101 -0
  712. nautobot/ui/src/js/drawer.js +106 -0
  713. nautobot/ui/src/js/form.js +23 -0
  714. nautobot/ui/src/js/history.js +51 -0
  715. nautobot/ui/src/js/nautobot-graphiql.js +19 -0
  716. nautobot/ui/src/js/nautobot.js +128 -0
  717. nautobot/ui/src/js/search.js +274 -0
  718. nautobot/ui/src/js/select2.js +318 -0
  719. nautobot/ui/src/js/sidenav.js +87 -0
  720. nautobot/ui/src/js/tabs.js +139 -0
  721. nautobot/ui/src/js/theme.js +104 -0
  722. nautobot/ui/src/js/utils.js +54 -0
  723. nautobot/ui/src/scss/colors.scss +58 -0
  724. nautobot/ui/src/scss/nautobot.scss +2471 -0
  725. nautobot/ui/webpack.config.js +148 -0
  726. nautobot/users/apps.py +3 -0
  727. nautobot/users/filters.py +7 -11
  728. nautobot/users/forms.py +10 -0
  729. nautobot/users/models.py +8 -0
  730. nautobot/users/templates/users/advanced_settings_edit.html +31 -21
  731. nautobot/users/templates/users/api_tokens.html +61 -51
  732. nautobot/users/templates/users/base.html +23 -31
  733. nautobot/users/templates/users/change_password.html +29 -19
  734. nautobot/users/templates/users/preferences.html +55 -45
  735. nautobot/users/templates/users/profile.html +45 -14
  736. nautobot/users/tests/test_api.py +4 -0
  737. nautobot/users/urls.py +2 -0
  738. nautobot/users/views.py +70 -2
  739. nautobot/virtualization/api/views.py +1 -1
  740. nautobot/virtualization/filters.py +18 -32
  741. nautobot/virtualization/forms.py +22 -59
  742. nautobot/virtualization/models.py +1 -19
  743. nautobot/virtualization/navigation.py +3 -1
  744. nautobot/virtualization/tables.py +10 -6
  745. nautobot/virtualization/templates/virtualization/cluster.html +13 -13
  746. nautobot/virtualization/templates/virtualization/cluster_edit.html +6 -6
  747. nautobot/virtualization/templates/virtualization/inc/virtualmachine_vminterface_filter.html +1 -1
  748. nautobot/virtualization/templates/virtualization/virtualmachine.html +1 -1
  749. nautobot/virtualization/templates/virtualization/virtualmachine_component_add.html +24 -16
  750. nautobot/virtualization/templates/virtualization/virtualmachine_edit.html +1 -1
  751. nautobot/virtualization/templates/virtualization/virtualmachine_list.html +4 -4
  752. nautobot/virtualization/templates/virtualization/virtualmachine_update.html +27 -25
  753. nautobot/virtualization/templates/virtualization/vminterface.html +5 -5
  754. nautobot/virtualization/templates/virtualization/vminterface_edit.html +27 -11
  755. nautobot/virtualization/tests/test_api.py +3 -0
  756. nautobot/virtualization/tests/test_models.py +20 -5
  757. nautobot/virtualization/tests/test_views.py +3 -5
  758. nautobot/virtualization/urls.py +0 -11
  759. nautobot/virtualization/views.py +5 -122
  760. nautobot/vpn/__init__.py +0 -0
  761. nautobot/vpn/api/serializers.py +113 -0
  762. nautobot/vpn/api/urls.py +19 -0
  763. nautobot/vpn/api/views.py +70 -0
  764. nautobot/vpn/apps.py +8 -0
  765. nautobot/vpn/choices.py +171 -0
  766. nautobot/vpn/factory.py +209 -0
  767. nautobot/vpn/filters.py +233 -0
  768. nautobot/vpn/forms.py +486 -0
  769. nautobot/vpn/homepage.py +19 -0
  770. nautobot/vpn/migrations/0001_initial.py +541 -0
  771. nautobot/vpn/migrations/0002_populate_defaults.py +199 -0
  772. nautobot/vpn/migrations/__init__.py +0 -0
  773. nautobot/vpn/models.py +527 -0
  774. nautobot/vpn/navigation.py +98 -0
  775. nautobot/vpn/tables.py +380 -0
  776. nautobot/vpn/templates/vpn/vpnprofile.html +2 -0
  777. nautobot/vpn/templates/vpn/vpnprofile_create.html +150 -0
  778. nautobot/vpn/tests/__init__.py +0 -0
  779. nautobot/vpn/tests/test_api.py +341 -0
  780. nautobot/vpn/tests/test_filters.py +139 -0
  781. nautobot/vpn/tests/test_forms.py +294 -0
  782. nautobot/vpn/tests/test_models.py +97 -0
  783. nautobot/vpn/tests/test_views.py +281 -0
  784. nautobot/vpn/urls.py +16 -0
  785. nautobot/vpn/views.py +437 -0
  786. nautobot/wireless/filters.py +0 -8
  787. nautobot/wireless/navigation.py +3 -1
  788. nautobot/wireless/templates/wireless/wirelessnetwork_create.html +6 -6
  789. nautobot/wireless/tests/integration/test_radio_profile.py +3 -7
  790. nautobot/wireless/tests/test_api.py +1 -1
  791. {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/METADATA +5 -4
  792. {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/RECORD +802 -707
  793. {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/entry_points.txt +1 -0
  794. nautobot/core/management/commands/check_job_approval_status.py +0 -47
  795. nautobot/core/templates/search_form.html +0 -9
  796. nautobot/core/templates/utilities/templatetags/filter_form_modal.html +0 -87
  797. nautobot/dcim/filters/mixins.py +0 -354
  798. nautobot/extras/templates/extras/job_approval_request.html +0 -134
  799. nautobot/extras/templates/extras/scheduled_jobs_approval_queue_list.html +0 -28
  800. nautobot/ipam/mixins.py +0 -32
  801. nautobot/ipam/templates/ipam/inc/prefix_header_extra_content_table.html +0 -4
  802. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap-theme.css +0 -587
  803. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap-theme.css.map +0 -1
  804. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap-theme.min.css +0 -6
  805. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap-theme.min.css.map +0 -1
  806. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.css +0 -6865
  807. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.css.map +0 -1
  808. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.min.css +0 -6
  809. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.min.css.map +0 -1
  810. nautobot/project-static/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.eot +0 -0
  811. nautobot/project-static/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.svg +0 -288
  812. nautobot/project-static/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.ttf +0 -0
  813. nautobot/project-static/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.woff +0 -0
  814. nautobot/project-static/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.woff2 +0 -0
  815. nautobot/project-static/bootstrap-3.4.1-dist/js/bootstrap.js +0 -2580
  816. nautobot/project-static/bootstrap-3.4.1-dist/js/bootstrap.min.js +0 -6
  817. nautobot/project-static/bootstrap-3.4.1-dist/js/npm.js +0 -13
  818. nautobot/project-static/clipboard.js-2.0.9/clipboard.min.js +0 -7
  819. nautobot/project-static/css/base.css +0 -1040
  820. nautobot/project-static/css/dark.css +0 -282
  821. nautobot/project-static/flatpickr-4.6.9/flatpickr.min.js +0 -2
  822. nautobot/project-static/flatpickr-4.6.9/themes/light.min.css +0 -1
  823. nautobot/project-static/graphiql-1.5.16/graphiql.min.css +0 -12
  824. nautobot/project-static/graphiql-1.5.16/graphiql.min.js +0 -11
  825. nautobot/project-static/highlight.js-11.9.0/github-dark.min.css +0 -10
  826. nautobot/project-static/highlight.js-11.9.0/github.min.css +0 -10
  827. nautobot/project-static/highlight.js-11.9.0/highlight.min.js +0 -378
  828. nautobot/project-static/jquery/jquery-3.7.1.min.js +0 -2
  829. nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_444444_256x240.png +0 -0
  830. nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_555555_256x240.png +0 -0
  831. nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_777620_256x240.png +0 -0
  832. nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_777777_256x240.png +0 -0
  833. nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_cc0000_256x240.png +0 -0
  834. nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_ffffff_256x240.png +0 -0
  835. nautobot/project-static/jquery-ui-1.13.2/jquery-ui.min.css +0 -7
  836. nautobot/project-static/jquery-ui-1.13.2/jquery-ui.min.js +0 -6
  837. nautobot/project-static/jquery-ui-1.13.2/jquery-ui.structure.min.css +0 -5
  838. nautobot/project-static/jquery-ui-1.13.2/jquery-ui.theme.min.css +0 -5
  839. nautobot/project-static/js/homepage_layout.js +0 -182
  840. nautobot/project-static/js/nav_menu.js +0 -250
  841. nautobot/project-static/js/theme.js +0 -133
  842. nautobot/project-static/materialdesignicons-7.4.47/LICENSE +0 -20
  843. nautobot/project-static/materialdesignicons-7.4.47/css/materialdesignicons.min.css +0 -3
  844. nautobot/project-static/react-16.14.0/react.production.min.js +0 -32
  845. nautobot/project-static/react-dom-16.14.0/react-dom.production.min.js +0 -239
  846. nautobot/project-static/select2-4.0.13/i18n/af.js +0 -3
  847. nautobot/project-static/select2-4.0.13/i18n/ar.js +0 -3
  848. nautobot/project-static/select2-4.0.13/i18n/az.js +0 -3
  849. nautobot/project-static/select2-4.0.13/i18n/bg.js +0 -3
  850. nautobot/project-static/select2-4.0.13/i18n/bn.js +0 -3
  851. nautobot/project-static/select2-4.0.13/i18n/bs.js +0 -3
  852. nautobot/project-static/select2-4.0.13/i18n/ca.js +0 -3
  853. nautobot/project-static/select2-4.0.13/i18n/cs.js +0 -3
  854. nautobot/project-static/select2-4.0.13/i18n/da.js +0 -3
  855. nautobot/project-static/select2-4.0.13/i18n/de.js +0 -3
  856. nautobot/project-static/select2-4.0.13/i18n/dsb.js +0 -3
  857. nautobot/project-static/select2-4.0.13/i18n/el.js +0 -3
  858. nautobot/project-static/select2-4.0.13/i18n/en.js +0 -3
  859. nautobot/project-static/select2-4.0.13/i18n/es.js +0 -3
  860. nautobot/project-static/select2-4.0.13/i18n/et.js +0 -3
  861. nautobot/project-static/select2-4.0.13/i18n/eu.js +0 -3
  862. nautobot/project-static/select2-4.0.13/i18n/fa.js +0 -3
  863. nautobot/project-static/select2-4.0.13/i18n/fi.js +0 -3
  864. nautobot/project-static/select2-4.0.13/i18n/fr.js +0 -3
  865. nautobot/project-static/select2-4.0.13/i18n/gl.js +0 -3
  866. nautobot/project-static/select2-4.0.13/i18n/he.js +0 -3
  867. nautobot/project-static/select2-4.0.13/i18n/hi.js +0 -3
  868. nautobot/project-static/select2-4.0.13/i18n/hr.js +0 -3
  869. nautobot/project-static/select2-4.0.13/i18n/hsb.js +0 -3
  870. nautobot/project-static/select2-4.0.13/i18n/hu.js +0 -3
  871. nautobot/project-static/select2-4.0.13/i18n/hy.js +0 -3
  872. nautobot/project-static/select2-4.0.13/i18n/id.js +0 -3
  873. nautobot/project-static/select2-4.0.13/i18n/is.js +0 -3
  874. nautobot/project-static/select2-4.0.13/i18n/it.js +0 -3
  875. nautobot/project-static/select2-4.0.13/i18n/ja.js +0 -3
  876. nautobot/project-static/select2-4.0.13/i18n/ka.js +0 -3
  877. nautobot/project-static/select2-4.0.13/i18n/km.js +0 -3
  878. nautobot/project-static/select2-4.0.13/i18n/ko.js +0 -3
  879. nautobot/project-static/select2-4.0.13/i18n/lt.js +0 -3
  880. nautobot/project-static/select2-4.0.13/i18n/lv.js +0 -3
  881. nautobot/project-static/select2-4.0.13/i18n/mk.js +0 -3
  882. nautobot/project-static/select2-4.0.13/i18n/ms.js +0 -3
  883. nautobot/project-static/select2-4.0.13/i18n/nb.js +0 -3
  884. nautobot/project-static/select2-4.0.13/i18n/ne.js +0 -3
  885. nautobot/project-static/select2-4.0.13/i18n/nl.js +0 -3
  886. nautobot/project-static/select2-4.0.13/i18n/pl.js +0 -3
  887. nautobot/project-static/select2-4.0.13/i18n/ps.js +0 -3
  888. nautobot/project-static/select2-4.0.13/i18n/pt-BR.js +0 -3
  889. nautobot/project-static/select2-4.0.13/i18n/pt.js +0 -3
  890. nautobot/project-static/select2-4.0.13/i18n/ro.js +0 -3
  891. nautobot/project-static/select2-4.0.13/i18n/ru.js +0 -3
  892. nautobot/project-static/select2-4.0.13/i18n/sk.js +0 -3
  893. nautobot/project-static/select2-4.0.13/i18n/sl.js +0 -3
  894. nautobot/project-static/select2-4.0.13/i18n/sq.js +0 -3
  895. nautobot/project-static/select2-4.0.13/i18n/sr-Cyrl.js +0 -3
  896. nautobot/project-static/select2-4.0.13/i18n/sr.js +0 -3
  897. nautobot/project-static/select2-4.0.13/i18n/sv.js +0 -3
  898. nautobot/project-static/select2-4.0.13/i18n/th.js +0 -3
  899. nautobot/project-static/select2-4.0.13/i18n/tk.js +0 -3
  900. nautobot/project-static/select2-4.0.13/i18n/tr.js +0 -3
  901. nautobot/project-static/select2-4.0.13/i18n/uk.js +0 -3
  902. nautobot/project-static/select2-4.0.13/i18n/vi.js +0 -3
  903. nautobot/project-static/select2-4.0.13/i18n/zh-CN.js +0 -3
  904. nautobot/project-static/select2-4.0.13/i18n/zh-TW.js +0 -3
  905. nautobot/project-static/select2-4.0.13/select2.min.css +0 -1
  906. nautobot/project-static/select2-4.0.13/select2.min.js +0 -2
  907. nautobot/project-static/select2-bootstrap-0.1.0-beta.10/select2-bootstrap.min.css +0 -7
  908. nautobot/project-static/subscriptions-transport-ws-0.9.18/client.min.js +0 -8
  909. nautobot/project-static/whatwg-fetch-3.6.2/fetch.umd.min.js +0 -8
  910. nautobot/virtualization/templates/virtualization/cluster_add_devices.html +0 -37
  911. /nautobot/extras/{filters/customfields.py → filter_mixins_customfields.py} +0 -0
  912. /nautobot/project-static/{materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.ttf → dist/1fcc36272ea3e53d0031.ttf} +0 -0
  913. /nautobot/project-static/{materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.eot → dist/2146c3c82b553977abc7.eot} +0 -0
  914. /nautobot/project-static/{materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff → dist/e55a20c80650829ec5fd.woff} +0 -0
  915. /nautobot/project-static/{materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff2 → dist/ec024da790d2972da002.woff2} +0 -0
  916. /nautobot/tenancy/{filters/mixins.py → filter_mixins.py} +0 -0
  917. {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/LICENSE.txt +0 -0
  918. {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/NOTICE +0 -0
  919. {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/WHEEL +0 -0
@@ -36,6 +36,7 @@ from nautobot.dcim.filters import (
36
36
  ControllerManagedDeviceGroupFilterSet,
37
37
  DeviceBayFilterSet,
38
38
  DeviceBayTemplateFilterSet,
39
+ DeviceClusterAssignmentFilterSet,
39
40
  DeviceFamilyFilterSet,
40
41
  DeviceFilterSet,
41
42
  DeviceRedundancyGroupFilterSet,
@@ -85,6 +86,7 @@ from nautobot.dcim.models import (
85
86
  Device,
86
87
  DeviceBay,
87
88
  DeviceBayTemplate,
89
+ DeviceClusterAssignment,
88
90
  DeviceFamily,
89
91
  DeviceRedundancyGroup,
90
92
  DeviceType,
@@ -122,7 +124,7 @@ from nautobot.dcim.models import (
122
124
  VirtualChassis,
123
125
  VirtualDeviceContext,
124
126
  )
125
- from nautobot.extras.filters.mixins import RoleFilter, StatusFilter
127
+ from nautobot.extras.filter_mixins import RoleFilter, StatusFilter
126
128
  from nautobot.extras.models import ExternalIntegration, Role, SecretsGroup, Status, Tag
127
129
  from nautobot.extras.tests.test_customfields_filters import CustomFieldsFilters
128
130
  from nautobot.ipam.models import IPAddress, Namespace, Prefix, Service, VLAN, VLANGroup
@@ -994,13 +996,13 @@ class ModuleDeviceCommonTestsMixin:
994
996
  "position": 1,
995
997
  }
996
998
  )
997
- with self.subTest():
999
+ with self.subTest("has_empty_module_bays: True"):
998
1000
  params = {"has_empty_module_bays": True}
999
1001
  qs = self.filterset(params, self.queryset).qs
1000
1002
  self.assertGreater(qs.count(), 0)
1001
1003
  for instance in qs:
1002
1004
  self.assertTrue(instance.module_bays.filter(installed_module__isnull=True).exists())
1003
- with self.subTest():
1005
+ with self.subTest("has_empty_module_bays: False"):
1004
1006
  params = {"has_empty_module_bays": False}
1005
1007
  qs = self.filterset(params, self.queryset).qs
1006
1008
  self.assertGreater(qs.count(), 0)
@@ -1008,13 +1010,13 @@ class ModuleDeviceCommonTestsMixin:
1008
1010
  self.assertFalse(instance.module_bays.filter(installed_module__isnull=True).exists())
1009
1011
 
1010
1012
  def test_has_modules(self):
1011
- with self.subTest():
1013
+ with self.subTest("has_modules: True"):
1012
1014
  params = {"has_modules": True}
1013
1015
  qs = self.filterset(params, self.queryset).qs
1014
1016
  self.assertGreater(qs.count(), 0)
1015
1017
  for instance in qs:
1016
1018
  self.assertTrue(instance.module_bays.filter(installed_module__isnull=False).exists())
1017
- with self.subTest():
1019
+ with self.subTest("has_modules: False"):
1018
1020
  params = {"has_modules": False}
1019
1021
  qs = self.filterset(params, self.queryset).qs
1020
1022
  self.assertGreater(qs.count(), 0)
@@ -1024,13 +1026,13 @@ class ModuleDeviceCommonTestsMixin:
1024
1026
 
1025
1027
  class PathEndpointModelTestMixin:
1026
1028
  def test_connected(self):
1027
- with self.subTest():
1029
+ with self.subTest("connected: True"):
1028
1030
  params = {"connected": True}
1029
1031
  self.assertQuerysetEqualAndNotEmpty(
1030
1032
  self.filterset(params, self.queryset).qs,
1031
1033
  self.queryset.filter(_path__is_active=True),
1032
1034
  )
1033
- with self.subTest():
1035
+ with self.subTest("connected: False"):
1034
1036
  params = {"connected": False}
1035
1037
  self.assertQuerysetEqualAndNotEmpty(
1036
1038
  self.filterset(params, self.queryset).qs,
@@ -1060,14 +1062,14 @@ class LocationTypeFilterSetTestCase(
1060
1062
  cls.lt2.validated_save()
1061
1063
 
1062
1064
  def test_content_types(self):
1063
- with self.subTest():
1065
+ with self.subTest("rackgroup"):
1064
1066
  params = {"content_types": ["dcim.rackgroup"]}
1065
1067
  ct = ContentType.objects.get_for_model(RackGroup)
1066
1068
  self.assertEqual(
1067
1069
  self.filterset(params, self.queryset).qs.count(),
1068
1070
  LocationType.objects.filter(content_types=ct).count(),
1069
1071
  )
1070
- with self.subTest():
1072
+ with self.subTest("device and rack"):
1071
1073
  params = {"content_types": ["dcim.device", "dcim.rack"]}
1072
1074
  ct_1 = [ContentType.objects.get_for_model(Device)]
1073
1075
  ct_2 = [ContentType.objects.get_for_model(Rack)]
@@ -1215,16 +1217,16 @@ class RackGroupTestCase(FilterTestCases.FilterTestCase, CustomFieldsFilters.Cust
1215
1217
 
1216
1218
  def test_children(self):
1217
1219
  child_groups = RackGroup.objects.filter(name__startswith="Child").filter(parent__isnull=False)[:2]
1218
- with self.subTest():
1220
+ with self.subTest("2 child groups"):
1219
1221
  params = {"children": [child_groups[0].pk, child_groups[1].pk]}
1220
1222
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
1221
- with self.subTest():
1223
+ with self.subTest("repeated group"):
1222
1224
  rack_group_4 = RackGroup.objects.filter(name="Rack Group 4").first()
1223
1225
  params = {"children": [rack_group_4.pk, rack_group_4.pk]}
1224
1226
  self.assertFalse(self.filterset(params, self.queryset).qs.exists())
1225
1227
 
1226
1228
  def test_ancestors(self):
1227
- with self.subTest():
1229
+ with self.subTest("self.loc3"):
1228
1230
  pk_list = []
1229
1231
  parent_locations = self.loc3.ancestors(include_self=True)
1230
1232
  pk_list.extend([v.pk for v in parent_locations])
@@ -1232,7 +1234,7 @@ class RackGroupTestCase(FilterTestCases.FilterTestCase, CustomFieldsFilters.Cust
1232
1234
  expected_queryset = RackGroup.objects.filter(params)
1233
1235
  params = {"ancestors": [self.loc3.pk]}
1234
1236
  self.assertQuerysetEqualAndNotEmpty(self.filterset(params, self.queryset).qs, expected_queryset)
1235
- with self.subTest():
1237
+ with self.subTest("self.loc2"):
1236
1238
  pk_list = []
1237
1239
  parent_locations = self.loc2.ancestors(include_self=True)
1238
1240
  pk_list.extend([v.pk for v in parent_locations])
@@ -1297,19 +1299,19 @@ class RackTestCase(FilterTestCases.FilterTestCase, FilterTestCases.TenancyFilter
1297
1299
 
1298
1300
  def test_desc_units(self):
1299
1301
  # TODO: not a generic_filter_test since this is a boolean filter but not a RelatedMembershipBooleanFilter
1300
- with self.subTest():
1302
+ with self.subTest("desc_units: True"):
1301
1303
  params = {"desc_units": True}
1302
1304
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
1303
- with self.subTest():
1305
+ with self.subTest("desc_units: False"):
1304
1306
  params = {"desc_units": False}
1305
1307
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
1306
1308
 
1307
1309
  def test_outer_unit(self):
1308
1310
  # TODO: Not a generic_filter_test since this is a single-value filter
1309
1311
  # 2.0 TODO: Support filtering for multiple values
1310
- with self.subTest():
1312
+ with self.subTest("queryset exclude blank"):
1311
1313
  self.assertEqual(Rack.objects.exclude(outer_unit="").count(), 3)
1312
- with self.subTest():
1314
+ with self.subTest("multiple values"):
1313
1315
  params = {"outer_unit": [RackDimensionUnitChoices.UNIT_MILLIMETER]}
1314
1316
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
1315
1317
 
@@ -1446,137 +1448,129 @@ class DeviceTypeTestCase(FilterTestCases.FilterTestCase, CustomFieldsFilters.Cus
1446
1448
  )
1447
1449
  device_type.tags.set(Tag.objects.get_for_model(DeviceType))
1448
1450
 
1449
- def test_is_full_depth(self):
1451
+ def test_special_filters(self):
1450
1452
  # TODO: Not a generic_filter_test because this is a boolean filter but not a RelatedMembershipBooleanFilter
1451
- with self.subTest():
1453
+ with self.subTest("is_full_depth: True"):
1452
1454
  params = {"is_full_depth": True}
1453
1455
  self.assertQuerysetEqual(
1454
1456
  self.filterset(params, self.queryset).qs,
1455
1457
  self.queryset.filter(is_full_depth=True),
1456
1458
  )
1457
- with self.subTest():
1459
+ with self.subTest("is_full_depth: False"):
1458
1460
  params = {"is_full_depth": False}
1459
1461
  self.assertQuerysetEqual(
1460
1462
  self.filterset(params, self.queryset).qs,
1461
1463
  self.queryset.filter(is_full_depth=False),
1462
1464
  )
1463
1465
 
1464
- def test_subdevice_role(self):
1465
1466
  # TODO: Not a generic_filter_test because this is a single-value filter
1466
1467
  # 2.0 TODO: Support filtering for multiple values
1467
- with self.subTest():
1468
+ with self.subTest("subdevice_role: PARENT"):
1468
1469
  params = {"subdevice_role": [SubdeviceRoleChoices.ROLE_PARENT]}
1469
1470
  self.assertQuerysetEqual(
1470
1471
  self.filterset(params, self.queryset).qs,
1471
1472
  self.queryset.filter(subdevice_role=SubdeviceRoleChoices.ROLE_PARENT),
1472
1473
  )
1473
- with self.subTest():
1474
+ with self.subTest("subdevice_role: CHILD"):
1474
1475
  params = {"subdevice_role": [SubdeviceRoleChoices.ROLE_CHILD]}
1475
1476
  self.assertQuerysetEqual(
1476
1477
  self.filterset(params, self.queryset).qs,
1477
1478
  self.queryset.filter(subdevice_role=SubdeviceRoleChoices.ROLE_CHILD),
1478
1479
  )
1479
1480
 
1480
- def test_console_ports(self):
1481
1481
  # TODO: Not a generic_filter_test because this is a boolean filter but not a RelatedMembershipBooleanFilter
1482
- with self.subTest():
1482
+ with self.subTest("console_ports: True"):
1483
1483
  params = {"console_ports": True}
1484
1484
  self.assertQuerysetEqual(
1485
1485
  self.filterset(params, self.queryset).qs,
1486
1486
  self.queryset.exclude(console_port_templates__isnull=True),
1487
1487
  )
1488
- with self.subTest():
1488
+ with self.subTest("console_ports: False"):
1489
1489
  params = {"console_ports": False}
1490
1490
  self.assertQuerysetEqual(
1491
1491
  self.filterset(params, self.queryset).qs,
1492
1492
  self.queryset.exclude(console_port_templates__isnull=False),
1493
1493
  )
1494
1494
 
1495
- def test_console_server_ports(self):
1496
1495
  # TODO: Not a generic_filter_test because this is a boolean filter but not a RelatedMembershipBooleanFilter
1497
- with self.subTest():
1496
+ with self.subTest("console_server_ports: True"):
1498
1497
  params = {"console_server_ports": True}
1499
1498
  self.assertQuerysetEqual(
1500
1499
  self.filterset(params, self.queryset).qs,
1501
1500
  self.queryset.exclude(console_server_port_templates__isnull=True),
1502
1501
  )
1503
- with self.subTest():
1502
+ with self.subTest("console_server_ports: False"):
1504
1503
  params = {"console_server_ports": False}
1505
1504
  self.assertQuerysetEqual(
1506
1505
  self.filterset(params, self.queryset).qs,
1507
1506
  self.queryset.exclude(console_server_port_templates__isnull=False),
1508
1507
  )
1509
1508
 
1510
- def test_power_ports(self):
1511
1509
  # TODO: Not a generic_filter_test because this is a boolean filter but not a RelatedMembershipBooleanFilter
1512
- with self.subTest():
1510
+ with self.subTest("power_ports: True"):
1513
1511
  params = {"power_ports": True}
1514
1512
  self.assertQuerysetEqual(
1515
1513
  self.filterset(params, self.queryset).qs,
1516
1514
  self.queryset.exclude(power_port_templates__isnull=True),
1517
1515
  )
1518
- with self.subTest():
1516
+ with self.subTest("power_ports: False"):
1519
1517
  params = {"power_ports": False}
1520
1518
  self.assertQuerysetEqual(
1521
1519
  self.filterset(params, self.queryset).qs,
1522
1520
  self.queryset.exclude(power_port_templates__isnull=False),
1523
1521
  )
1524
1522
 
1525
- def test_power_outlets(self):
1526
1523
  # TODO: Not a generic_filter_test because this is a boolean filter but not a RelatedMembershipBooleanFilter
1527
- with self.subTest():
1524
+ with self.subTest("power_outlets: True"):
1528
1525
  params = {"power_outlets": True}
1529
1526
  self.assertQuerysetEqual(
1530
1527
  self.filterset(params, self.queryset).qs,
1531
1528
  self.queryset.exclude(power_outlet_templates__isnull=True),
1532
1529
  )
1533
- with self.subTest():
1530
+ with self.subTest("power_outlets: False"):
1534
1531
  params = {"power_outlets": False}
1535
1532
  self.assertQuerysetEqual(
1536
1533
  self.filterset(params, self.queryset).qs,
1537
1534
  self.queryset.exclude(power_outlet_templates__isnull=False),
1538
1535
  )
1539
1536
 
1540
- def test_interfaces(self):
1541
1537
  # TODO: Not a generic_filter_test because this is a boolean filter but not a RelatedMembershipBooleanFilter
1542
- with self.subTest():
1538
+ with self.subTest("interfaces: True"):
1543
1539
  params = {"interfaces": True}
1544
1540
  self.assertQuerysetEqual(
1545
1541
  self.filterset(params, self.queryset).qs,
1546
1542
  self.queryset.exclude(interface_templates__isnull=True),
1547
1543
  )
1548
- with self.subTest():
1544
+ with self.subTest("interfaces: False"):
1549
1545
  params = {"interfaces": False}
1550
1546
  self.assertQuerysetEqual(
1551
1547
  self.filterset(params, self.queryset).qs,
1552
1548
  self.queryset.exclude(interface_templates__isnull=False),
1553
1549
  )
1554
1550
 
1555
- def test_pass_through_ports(self):
1556
1551
  # TODO: Not a generic_filter_test because this is a boolean filter but not a RelatedMembershipBooleanFilter
1557
1552
  query = Q(front_port_templates__isnull=False, rear_port_templates__isnull=False)
1558
- with self.subTest():
1553
+ with self.subTest("pass_through_ports: True"):
1559
1554
  params = {"pass_through_ports": True}
1560
1555
  self.assertQuerysetEqual(
1561
1556
  self.filterset(params, self.queryset).qs,
1562
1557
  self.queryset.filter(query).distinct(),
1563
1558
  )
1564
- with self.subTest():
1559
+ with self.subTest("pass_through_ports: False"):
1565
1560
  params = {"pass_through_ports": False}
1566
1561
  self.assertQuerysetEqual(
1567
1562
  self.filterset(params, self.queryset).qs,
1568
1563
  self.queryset.filter(~query).distinct(),
1569
1564
  )
1570
1565
 
1571
- def test_device_bays(self):
1572
1566
  # TODO: Not a generic_filter_test because this is a boolean filter but not a RelatedMembershipBooleanFilter
1573
- with self.subTest():
1567
+ with self.subTest("device_bays: True"):
1574
1568
  params = {"device_bays": True}
1575
1569
  self.assertQuerysetEqual(
1576
1570
  self.filterset(params, self.queryset).qs,
1577
1571
  self.queryset.exclude(device_bay_templates__isnull=True),
1578
1572
  )
1579
- with self.subTest():
1573
+ with self.subTest("device_bays: False"):
1580
1574
  params = {"device_bays": False}
1581
1575
  self.assertQuerysetEqual(
1582
1576
  self.filterset(params, self.queryset).qs,
@@ -1654,13 +1648,13 @@ class InterfaceTemplateTestCase(ModularComponentTemplateTestMixin, FilterTestCas
1654
1648
 
1655
1649
  def test_mgmt_only(self):
1656
1650
  # TODO: Not a generic_filter_test because this is a boolean filter but not a RelatedMembershipBooleanFilter
1657
- with self.subTest():
1651
+ with self.subTest("mgmt_only: True"):
1658
1652
  params = {"mgmt_only": True}
1659
1653
  self.assertQuerysetEqual(
1660
1654
  self.filterset(params, self.queryset).qs,
1661
1655
  self.queryset.filter(**params),
1662
1656
  )
1663
- with self.subTest():
1657
+ with self.subTest("mgmt_only: False"):
1664
1658
  params = {"mgmt_only": False}
1665
1659
  self.assertQuerysetEqual(
1666
1660
  self.filterset(params, self.queryset).qs,
@@ -1783,8 +1777,10 @@ class DeviceTestCase(
1783
1777
  tenancy_related_name = "devices"
1784
1778
  generic_filter_tests = [
1785
1779
  ("asset_tag",),
1786
- ("cluster", "cluster__id"),
1787
- ("cluster", "cluster__name"),
1780
+ ("cluster", "clusters__id"),
1781
+ ("cluster", "clusters__name"),
1782
+ ("clusters", "clusters__id"),
1783
+ ("clusters", "clusters__name"),
1788
1784
  ("console_ports", "console_ports__id"),
1789
1785
  ("console_server_ports", "console_server_ports__id"),
1790
1786
  ("device_bays", "device_bays__id"),
@@ -1938,101 +1934,96 @@ class DeviceTestCase(
1938
1934
  virtual_chassis_2 = VirtualChassis.objects.create(name="vc2", master=devices[2])
1939
1935
  Device.objects.filter(pk=devices[2].pk).update(virtual_chassis=virtual_chassis_2, vc_position=1, vc_priority=1)
1940
1936
 
1941
- def test_face(self):
1937
+ def test_special_filters(self):
1942
1938
  # TODO: Not a generic_filter_test because this is a single-value filter
1943
- params = {"face": [DeviceFaceChoices.FACE_FRONT]}
1944
- self.assertQuerysetEqualAndNotEmpty(
1945
- self.filterset(params, self.queryset).qs,
1946
- Device.objects.filter(face=DeviceFaceChoices.FACE_FRONT),
1947
- )
1939
+ with self.subTest("face"):
1940
+ params = {"face": [DeviceFaceChoices.FACE_FRONT]}
1941
+ self.assertQuerysetEqualAndNotEmpty(
1942
+ self.filterset(params, self.queryset).qs,
1943
+ Device.objects.filter(face=DeviceFaceChoices.FACE_FRONT),
1944
+ )
1948
1945
 
1949
- def test_is_full_depth(self):
1950
1946
  # TODO: Not a generic_filter_test because this is a boolean filter but not a RelatedMembershipBooleanFilter
1951
- with self.subTest():
1947
+ with self.subTest("is_full_depth: True"):
1952
1948
  params = {"is_full_depth": True}
1953
1949
  self.assertQuerysetEqualAndNotEmpty(
1954
1950
  self.filterset(params, self.queryset).qs,
1955
1951
  Device.objects.filter(device_type__is_full_depth=True),
1956
1952
  )
1957
- with self.subTest():
1953
+ with self.subTest("is_full_depth: False"):
1958
1954
  params = {"is_full_depth": False}
1959
1955
  self.assertQuerysetEqualAndNotEmpty(
1960
1956
  self.filterset(params, self.queryset).qs,
1961
1957
  Device.objects.filter(device_type__is_full_depth=False),
1962
1958
  )
1963
1959
 
1964
- def test_serial(self):
1965
1960
  # TODO: Not a generic_filter_test because this is a single-value filter
1966
1961
  # 2.0 TODO: Support filtering for multiple values
1967
- with self.subTest():
1962
+ with self.subTest("serial: uppercase"):
1968
1963
  params = {"serial": "ABC"}
1969
1964
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
1970
- with self.subTest():
1965
+ with self.subTest("serial: lowercase"):
1971
1966
  params = {"serial": "abc"}
1972
1967
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
1973
1968
 
1974
- def test_has_primary_ip(self):
1975
1969
  # TODO: Not a generic_filter_test because this is a boolean filter but not a RelatedMembershipBooleanFilter
1976
- with self.subTest():
1970
+ with self.subTest("has_primary_ip: True"):
1977
1971
  params = {"has_primary_ip": True}
1978
1972
  self.assertQuerysetEqualAndNotEmpty(
1979
1973
  self.filterset(params, self.queryset).qs,
1980
1974
  Device.objects.filter(Q(primary_ip4__isnull=False) | Q(primary_ip6__isnull=False)),
1981
1975
  )
1982
- with self.subTest():
1976
+ with self.subTest("has_primary_ip: False"):
1983
1977
  params = {"has_primary_ip": False}
1984
1978
  self.assertQuerysetEqualAndNotEmpty(
1985
1979
  self.filterset(params, self.queryset).qs,
1986
1980
  Device.objects.filter(primary_ip4__isnull=True, primary_ip6__isnull=True),
1987
1981
  )
1988
1982
 
1989
- def test_ip_addresses(self):
1990
- addresses = list(IPAddress.objects.filter(interfaces__isnull=False)[:2])
1991
- params = {"ip_addresses": [addresses[0].address, addresses[1].id]}
1992
- self.assertQuerysetEqualAndNotEmpty(
1993
- self.filterset(params, self.queryset).qs,
1994
- self.queryset.filter(interfaces__ip_addresses__in=addresses).distinct(),
1995
- )
1983
+ with self.subTest("ip_addresses"):
1984
+ addresses = list(IPAddress.objects.filter(interfaces__isnull=False)[:2])
1985
+ params = {"ip_addresses": [addresses[0].address, addresses[1].id]}
1986
+ self.assertQuerysetEqualAndNotEmpty(
1987
+ self.filterset(params, self.queryset).qs,
1988
+ self.queryset.filter(interfaces__ip_addresses__in=addresses).distinct(),
1989
+ )
1996
1990
 
1997
- def test_virtual_chassis_member(self):
1998
1991
  # TODO: Not a generic_filter_test because this is a boolean filter but not a RelatedMembershipBooleanFilter
1999
- with self.subTest():
1992
+ with self.subTest("virtual_chassis_member: True"):
2000
1993
  params = {"virtual_chassis_member": True}
2001
1994
  self.assertQuerysetEqualAndNotEmpty(
2002
1995
  self.filterset(params, self.queryset).qs,
2003
1996
  Device.objects.filter(virtual_chassis__isnull=False),
2004
1997
  )
2005
- with self.subTest():
1998
+ with self.subTest("virtual_chassis_member: False"):
2006
1999
  params = {"virtual_chassis_member": False}
2007
2000
  self.assertQuerysetEqualAndNotEmpty(
2008
2001
  self.filterset(params, self.queryset).qs,
2009
2002
  Device.objects.filter(virtual_chassis__isnull=True),
2010
2003
  )
2011
2004
 
2012
- def test_is_virtual_chassis_member(self):
2013
2005
  # TODO: Not a generic_filter_test because this is a boolean filter but not a RelatedMembershipBooleanFilter
2014
- with self.subTest():
2006
+ with self.subTest("is_virtual_chassis_member: True"):
2015
2007
  params = {"is_virtual_chassis_member": True}
2016
2008
  self.assertQuerysetEqualAndNotEmpty(
2017
2009
  self.filterset(params, self.queryset).qs,
2018
2010
  Device.objects.filter(virtual_chassis__isnull=False),
2019
2011
  )
2020
- with self.subTest():
2012
+ with self.subTest("is_virtual_chassis_member: False"):
2021
2013
  params = {"is_virtual_chassis_member": False}
2022
2014
  self.assertQuerysetEqualAndNotEmpty(
2023
2015
  self.filterset(params, self.queryset).qs,
2024
2016
  Device.objects.filter(virtual_chassis__isnull=True),
2025
2017
  )
2026
2018
 
2027
- def test_local_config_context_data(self):
2028
2019
  # TODO: Not a generic_filter_test because this is a boolean filter but not a RelatedMembershipBooleanFilter
2029
- with self.subTest():
2020
+ with self.subTest("local_config_context_data: True"):
2030
2021
  params = {"local_config_context_data": True}
2031
2022
  self.assertQuerysetEqualAndNotEmpty(
2032
2023
  self.filterset(params, self.queryset).qs,
2033
2024
  Device.objects.filter(local_config_context_data__isnull=False),
2034
2025
  )
2035
- with self.subTest():
2026
+ with self.subTest("local_config_context_data: False"):
2036
2027
  params = {"local_config_context_data": False}
2037
2028
  self.assertQuerysetEqualAndNotEmpty(
2038
2029
  self.filterset(params, self.queryset).qs,
@@ -2525,13 +2516,13 @@ class InterfaceTestCase(PathEndpointModelTestMixin, ModularDeviceComponentTestMi
2525
2516
 
2526
2517
  def test_enabled(self):
2527
2518
  # TODO: Not a generic_filter_test because this is a boolean filter but not a RelatedMembershipBooleanFilter
2528
- with self.subTest():
2519
+ with self.subTest("enabled: True"):
2529
2520
  params = {"enabled": True}
2530
2521
  self.assertQuerysetEqualAndNotEmpty(
2531
2522
  self.filterset(params, self.queryset).qs,
2532
2523
  self.queryset.filter(**params),
2533
2524
  )
2534
- with self.subTest():
2525
+ with self.subTest("enabled: False"):
2535
2526
  params = {"enabled": False}
2536
2527
  self.assertQuerysetEqualAndNotEmpty(
2537
2528
  self.filterset(params, self.queryset).qs,
@@ -2540,13 +2531,13 @@ class InterfaceTestCase(PathEndpointModelTestMixin, ModularDeviceComponentTestMi
2540
2531
 
2541
2532
  def test_mgmt_only(self):
2542
2533
  # TODO: Not a generic_filter_test because this is a boolean filter but not a RelatedMembershipBooleanFilter
2543
- with self.subTest():
2534
+ with self.subTest("mgmt_only: True"):
2544
2535
  params = {"mgmt_only": True}
2545
2536
  self.assertQuerysetEqualAndNotEmpty(
2546
2537
  self.filterset(params, self.queryset).qs,
2547
2538
  self.queryset.filter(**params),
2548
2539
  )
2549
- with self.subTest():
2540
+ with self.subTest("mgmt_only: False"):
2550
2541
  params = {"mgmt_only": False}
2551
2542
  self.assertQuerysetEqualAndNotEmpty(
2552
2543
  self.filterset(params, self.queryset).qs,
@@ -2603,17 +2594,17 @@ class InterfaceTestCase(PathEndpointModelTestMixin, ModularDeviceComponentTestMi
2603
2594
 
2604
2595
  # Capture the first device so that we can use it in the next test.
2605
2596
  device = Device.objects.get(pk=devices[0].pk)
2606
- with self.subTest():
2597
+ with self.subTest("device_with_common_vc"):
2607
2598
  self.assertQuerysetEqual(
2608
2599
  queryset,
2609
2600
  self.queryset.filter(pk__in=device.common_vc_interfaces.values_list("pk", flat=True)),
2610
2601
  )
2611
2602
  # Assert interface of a device belonging to same VC as `device` are returned
2612
- with self.subTest():
2603
+ with self.subTest("interface of a device belonging to same vc as 'device'"):
2613
2604
  self.assertTrue(queryset.filter(name="int3").exists())
2614
2605
 
2615
2606
  # Assert interface of a device not belonging as `device` to same VC are not returned
2616
- with self.subTest():
2607
+ with self.subTest("interface of a device NOT belonging to same vc as 'device'"):
2617
2608
  self.assertFalse(queryset.filter(name="int4").exists())
2618
2609
 
2619
2610
  def test_device(self):
@@ -2767,13 +2758,13 @@ class InterfaceTestCase(PathEndpointModelTestMixin, ModularDeviceComponentTestMi
2767
2758
  def test_kind(self):
2768
2759
  # TODO: Not a generic_filter_test because this is a single-value filter
2769
2760
  # 2.0 TODO: Support filtering for multiple values
2770
- with self.subTest():
2761
+ with self.subTest("kind: physical"):
2771
2762
  params = {"kind": "physical"}
2772
2763
  self.assertQuerysetEqualAndNotEmpty(
2773
2764
  self.filterset(params, self.queryset).qs,
2774
2765
  self.queryset.exclude(type__in=NONCONNECTABLE_IFACE_TYPES),
2775
2766
  )
2776
- with self.subTest():
2767
+ with self.subTest("kind: virtual"):
2777
2768
  params = {"kind": "virtual"}
2778
2769
  self.assertQuerysetEqualAndNotEmpty(
2779
2770
  self.filterset(params, self.queryset).qs,
@@ -3156,20 +3147,20 @@ class InventoryItemTestCase(DeviceComponentTestMixin, FilterTestCases.FilterTest
3156
3147
  def test_discovered(self):
3157
3148
  # TODO: Not a generic_filter_test because this is a boolean filter but not a RelatedMembershipBooleanFilter
3158
3149
  # 2.0 TODO: Fix boolean value
3159
- with self.subTest():
3150
+ with self.subTest("discovered: True"):
3160
3151
  params = {"discovered": True}
3161
3152
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
3162
- with self.subTest():
3153
+ with self.subTest("discovered: False"):
3163
3154
  params = {"discovered": False}
3164
3155
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
3165
3156
 
3166
3157
  def test_serial(self):
3167
3158
  # TODO: Not a generic_filter_test because this is a single-value filter
3168
3159
  # 2.0 TODO: Support filtering for multiple values
3169
- with self.subTest():
3160
+ with self.subTest("serial: uppercase"):
3170
3161
  params = {"serial": "ABC"}
3171
3162
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
3172
- with self.subTest():
3163
+ with self.subTest("serial: lowercase"):
3173
3164
  params = {"serial": "abc"}
3174
3165
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
3175
3166
 
@@ -3541,17 +3532,17 @@ class CableTestCase(FilterTestCases.FilterTestCase):
3541
3532
  def test_rack(self):
3542
3533
  # TODO: Not a generic_filter_test because this is a method filter.
3543
3534
  racks = Rack.objects.all()[:2]
3544
- with self.subTest():
3535
+ with self.subTest("rack_id"):
3545
3536
  params = {"rack_id": [racks[0].pk, racks[1].pk]}
3546
3537
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
3547
- with self.subTest():
3538
+ with self.subTest("rack"):
3548
3539
  params = {"rack": [racks[0].name, racks[1].name]}
3549
3540
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
3550
3541
 
3551
3542
  def test_tenant(self):
3552
3543
  # TODO: Not a generic_filter_test because this is a method filter.
3553
3544
  tenants = list(Tenant.objects.filter(devices__isnull=False))[:2]
3554
- with self.subTest():
3545
+ with self.subTest("tenant_id"):
3555
3546
  params = {"tenant_id": [tenants[0].pk, tenants[1].pk]}
3556
3547
  self.assertQuerysetEqual(
3557
3548
  self.filterset(params, self.queryset).qs,
@@ -3559,7 +3550,7 @@ class CableTestCase(FilterTestCases.FilterTestCase):
3559
3550
  Q(_termination_a_device__tenant__in=tenants) | Q(_termination_b_device__tenant__in=tenants)
3560
3551
  ),
3561
3552
  )
3562
- with self.subTest():
3553
+ with self.subTest("tenant"):
3563
3554
  params = {"tenant": [tenants[0].name, tenants[1].name]}
3564
3555
  self.assertQuerysetEqual(
3565
3556
  self.filterset(params, self.queryset).qs,
@@ -3574,22 +3565,22 @@ class CableTestCase(FilterTestCases.FilterTestCase):
3574
3565
  type_interface = "dcim.interface"
3575
3566
  type_console_port = "dcim.consoleport"
3576
3567
  type_console_server_port = "dcim.consoleserverport"
3577
- with self.subTest():
3568
+ with self.subTest("termination_a_type: interface, console_port"):
3578
3569
  params = {"termination_a_type": [type_interface, type_console_port]}
3579
3570
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
3580
- with self.subTest():
3571
+ with self.subTest("termination_a_type: interface"):
3581
3572
  params = {"termination_a_type": [type_interface]}
3582
3573
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
3583
- with self.subTest():
3574
+ with self.subTest("termination_b_type: interface, console_server_port"):
3584
3575
  params = {"termination_b_type": [type_interface, type_console_server_port]}
3585
3576
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
3586
- with self.subTest():
3577
+ with self.subTest("termination_b_type: interface"):
3587
3578
  params = {"termination_b_type": [type_interface]}
3588
3579
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
3589
- with self.subTest():
3580
+ with self.subTest("termination_type: interface"):
3590
3581
  params = {"termination_type": [type_interface]}
3591
3582
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
3592
- with self.subTest():
3583
+ with self.subTest("termination_type: console_port, console_server_port"):
3593
3584
  params = {"termination_type": [type_console_port, type_console_server_port]}
3594
3585
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
3595
3586
 
@@ -3785,13 +3776,13 @@ class DeviceRedundancyGroupTestCase(FilterTestCases.FilterTestCase):
3785
3776
  def test_failover_strategy(self):
3786
3777
  # TODO: Not a generic_filter_test because this is a single-value filter
3787
3778
  # 2.0 TODO: Support filtering for multiple values
3788
- with self.subTest():
3779
+ with self.subTest("failover_strategy: active-active"):
3789
3780
  params = {"failover_strategy": ["active-active"]}
3790
3781
  self.assertQuerysetEqualAndNotEmpty(
3791
3782
  self.filterset(params, self.queryset).qs,
3792
3783
  DeviceRedundancyGroup.objects.filter(failover_strategy="active-active"),
3793
3784
  )
3794
- with self.subTest():
3785
+ with self.subTest("failover_strategy: active-passive"):
3795
3786
  params = {"failover_strategy": ["active-passive"]}
3796
3787
  self.assertQuerysetEqualAndNotEmpty(
3797
3788
  self.filterset(params, self.queryset).qs,
@@ -3865,13 +3856,13 @@ class InterfaceRedundancyGroupTestCase(FilterTestCases.FilterTestCase):
3865
3856
  interface_redundancy_groups[1].validated_save()
3866
3857
 
3867
3858
  def test_virtual_ip(self):
3868
- with self.subTest():
3859
+ with self.subTest("virtual_ip: pks"):
3869
3860
  params = {"virtual_ip": [self.ips[0].pk, self.ips[1].pk]}
3870
3861
  self.assertQuerysetEqualAndNotEmpty(
3871
3862
  self.filterset(params, self.queryset).qs,
3872
3863
  InterfaceRedundancyGroup.objects.filter(virtual_ip__in=params["virtual_ip"]),
3873
3864
  )
3874
- with self.subTest():
3865
+ with self.subTest("virtual_ip: addresses"):
3875
3866
  params = {"virtual_ip": [str(self.ips[2].address), str(self.ips[3].address)]}
3876
3867
  self.assertQuerysetEqualAndNotEmpty(
3877
3868
  self.filterset(params, self.queryset).qs,
@@ -4239,13 +4230,13 @@ class ModuleBayTemplateTestCase(FilterTestCases.FilterTestCase):
4239
4230
 
4240
4231
  def test_requires_first_party_modules(self):
4241
4232
  # TODO: Not a generic_filter_test because this is a boolean filter but not a RelatedMembershipBooleanFilter
4242
- with self.subTest():
4233
+ with self.subTest("requires_first_party_modules: True"):
4243
4234
  params = {"requires_first_party_modules": True}
4244
4235
  self.assertQuerysetEqual(
4245
4236
  self.filterset(params, self.queryset).qs,
4246
4237
  self.queryset.filter(requires_first_party_modules=True),
4247
4238
  )
4248
- with self.subTest():
4239
+ with self.subTest("requires_first_party_modules: False"):
4249
4240
  params = {"requires_first_party_modules": False}
4250
4241
  self.assertQuerysetEqual(
4251
4242
  self.filterset(params, self.queryset).qs,
@@ -4335,13 +4326,13 @@ class VirtualDeviceContextTestCase(FilterTestCases.FilterTestCase, FilterTestCas
4335
4326
 
4336
4327
  def test_has_primary_ip(self):
4337
4328
  # TODO: Not a generic_filter_test because this is a boolean filter but not a RelatedMembershipBooleanFilter
4338
- with self.subTest():
4329
+ with self.subTest("has_primary_ip: True"):
4339
4330
  params = {"has_primary_ip": True}
4340
4331
  self.assertQuerysetEqualAndNotEmpty(
4341
4332
  self.filterset(params, self.queryset).qs,
4342
4333
  VirtualDeviceContext.objects.filter(Q(primary_ip4__isnull=False) | Q(primary_ip6__isnull=False)),
4343
4334
  )
4344
- with self.subTest():
4335
+ with self.subTest("has_primary_ip: False"):
4345
4336
  params = {"has_primary_ip": False}
4346
4337
  self.assertQuerysetEqualAndNotEmpty(
4347
4338
  self.filterset(params, self.queryset).qs,
@@ -4451,3 +4442,61 @@ class ModuleFamilyTestCase(FilterTestCases.FilterTestCase):
4451
4442
  manufacturer=manufacturers[1], model="Model 2", module_family=cls.module_families[1]
4452
4443
  ),
4453
4444
  )
4445
+
4446
+
4447
+ class DeviceClusterAssignmentTestCase(FilterTestCases.FilterTestCase):
4448
+ queryset = DeviceClusterAssignment.objects.all()
4449
+ filterset = DeviceClusterAssignmentFilterSet
4450
+ generic_filter_tests = [
4451
+ ("device", "device__id"),
4452
+ ("device", "device__name"),
4453
+ ("cluster", "cluster__id"),
4454
+ ("cluster", "cluster__name"),
4455
+ ]
4456
+
4457
+ @classmethod
4458
+ def setUpTestData(cls):
4459
+ manufacturer = Manufacturer.objects.first()
4460
+ device_type = DeviceType.objects.create(
4461
+ manufacturer=manufacturer,
4462
+ model="Test Device Type Filter 1",
4463
+ )
4464
+ device_role = Role.objects.get_for_model(Device).first()
4465
+ device_status = Status.objects.get_for_model(Device).first()
4466
+ location = Location.objects.filter(location_type=LocationType.objects.get(name="Campus")).first()
4467
+ devices = (
4468
+ Device.objects.create(
4469
+ device_type=device_type,
4470
+ role=device_role,
4471
+ name="Test Device Filter 1",
4472
+ location=location,
4473
+ status=device_status,
4474
+ ),
4475
+ Device.objects.create(
4476
+ device_type=device_type,
4477
+ role=device_role,
4478
+ name="Test Device Filter 2",
4479
+ location=location,
4480
+ status=device_status,
4481
+ ),
4482
+ Device.objects.create(
4483
+ device_type=device_type,
4484
+ role=device_role,
4485
+ name="Test Device Filter 3",
4486
+ location=location,
4487
+ status=device_status,
4488
+ ),
4489
+ )
4490
+ cluster_type = ClusterType.objects.create(name="Test Cluster Type Filter")
4491
+ clusters = (
4492
+ Cluster.objects.create(name="Test Cluster Filter 1", cluster_type=cluster_type),
4493
+ Cluster.objects.create(name="Test Cluster Filter 2", cluster_type=cluster_type),
4494
+ Cluster.objects.create(name="Test Cluster Filter 3", cluster_type=cluster_type),
4495
+ )
4496
+ cls.device_cluster_assignments = (
4497
+ DeviceClusterAssignment.objects.create(device=devices[0], cluster=clusters[0]),
4498
+ DeviceClusterAssignment.objects.create(device=devices[1], cluster=clusters[1]),
4499
+ DeviceClusterAssignment.objects.create(device=devices[0], cluster=clusters[1]),
4500
+ DeviceClusterAssignment.objects.create(device=devices[1], cluster=clusters[2]),
4501
+ DeviceClusterAssignment.objects.create(device=devices[2], cluster=clusters[0]),
4502
+ )