nautobot 3.0.0a2__py3-none-any.whl → 3.0.0rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (659) hide show
  1. nautobot/apps/choices.py +4 -2
  2. nautobot/apps/filters.py +7 -9
  3. nautobot/apps/models.py +2 -2
  4. nautobot/apps/ui.py +13 -1
  5. nautobot/apps/utils.py +8 -0
  6. nautobot/circuits/filters.py +3 -2
  7. nautobot/circuits/navigation.py +3 -2
  8. nautobot/circuits/templates/circuits/circuit_create.html +3 -3
  9. nautobot/circuits/templates/circuits/circuittermination_create.html +9 -24
  10. nautobot/circuits/templates/circuits/inc/circuit_termination_cable_fragment.html +6 -6
  11. nautobot/circuits/templates/circuits/inc/speed_widget.html +12 -12
  12. nautobot/circuits/tests/integration/test_circuit.py +10 -13
  13. nautobot/circuits/tests/integration/test_circuits_bulk_operations.py +0 -3
  14. nautobot/circuits/views.py +6 -2
  15. nautobot/cloud/filters.py +1 -1
  16. nautobot/cloud/navigation.py +3 -2
  17. nautobot/core/api/schema.py +1 -1
  18. nautobot/core/api/serializers.py +6 -1
  19. nautobot/core/api/urls.py +2 -0
  20. nautobot/core/api/views.py +12 -0
  21. nautobot/core/apps/__init__.py +11 -10
  22. nautobot/core/celery/__init__.py +3 -5
  23. nautobot/core/checks.py +46 -0
  24. nautobot/core/choices.py +1 -1
  25. nautobot/core/cli/bootstrap_v3_to_v5.py +105 -13
  26. nautobot/core/cli/migrate_deprecated_templates.py +227 -0
  27. nautobot/core/constants.py +3 -0
  28. nautobot/core/context_processors.py +9 -1
  29. nautobot/core/filters.py +4 -0
  30. nautobot/core/forms/__init__.py +2 -0
  31. nautobot/core/forms/forms.py +1 -1
  32. nautobot/core/forms/widgets.py +21 -2
  33. nautobot/core/jobs/__init__.py +62 -3
  34. nautobot/core/jobs/groups.py +31 -1
  35. nautobot/core/management/commands/generate_test_data.py +28 -9
  36. nautobot/core/models/__init__.py +11 -0
  37. nautobot/core/models/generics.py +9 -1
  38. nautobot/core/models/tree_queries.py +10 -5
  39. nautobot/core/models/utils.py +1 -1
  40. nautobot/core/settings.py +35 -19
  41. nautobot/core/settings.yaml +17 -33
  42. nautobot/core/signals.py +12 -1
  43. nautobot/core/tables.py +13 -6
  44. nautobot/core/templates/40x.html +1 -1
  45. nautobot/core/templates/500.html +2 -2
  46. nautobot/core/templates/admin/base.html +1 -2
  47. nautobot/core/templates/admin/change_list.html +9 -12
  48. nautobot/core/templates/admin/config/config.html +12 -12
  49. nautobot/core/templates/admin/index.html +3 -3
  50. nautobot/core/templates/base_django.html +1 -2
  51. nautobot/core/templates/buttons/export.html +1 -1
  52. nautobot/core/templates/components/button/dropdown.html +5 -3
  53. nautobot/core/templates/components/panel/body_wrapper_generic_table.html +1 -1
  54. nautobot/core/templates/components/panel/header_extra_content_table.html +1 -1
  55. nautobot/core/templates/components/panel/panel.html +3 -3
  56. nautobot/core/templates/components/tab/content_wrapper.html +6 -7
  57. nautobot/core/templates/components/tab/label_wrapper_distinct_view.html +1 -1
  58. nautobot/core/templates/echarts/echarts.html +22 -9
  59. nautobot/core/templates/generic/object_bulk_add_component.html +2 -1
  60. nautobot/core/templates/generic/object_bulk_create.html +6 -5
  61. nautobot/core/templates/generic/object_bulk_delete.html +1 -1
  62. nautobot/core/templates/generic/object_bulk_destroy.html +3 -3
  63. nautobot/core/templates/generic/object_bulk_edit.html +1 -1
  64. nautobot/core/templates/generic/object_bulk_import.html +1 -1
  65. nautobot/core/templates/generic/object_bulk_remove.html +2 -2
  66. nautobot/core/templates/generic/object_bulk_update.html +5 -4
  67. nautobot/core/templates/generic/object_create.html +5 -4
  68. nautobot/core/templates/generic/object_delete.html +1 -1
  69. nautobot/core/templates/generic/object_detail.html +1 -1
  70. nautobot/core/templates/generic/object_edit.html +1 -1
  71. nautobot/core/templates/generic/object_import.html +2 -1
  72. nautobot/core/templates/generic/object_list.html +12 -4
  73. nautobot/core/templates/generic/object_notes.html +5 -3
  74. nautobot/core/templates/generic/object_retrieve.html +4 -5
  75. nautobot/core/templates/graphene/graphiql.html +7 -8
  76. nautobot/core/templates/home.html +1 -1
  77. nautobot/core/templates/import_success.html +2 -1
  78. nautobot/core/templates/inc/computed_fields/panel_data.html +1 -1
  79. nautobot/core/templates/inc/created_updated.html +7 -3
  80. nautobot/core/templates/inc/custom_fields/panel_data.html +1 -1
  81. nautobot/core/templates/inc/footer.html +3 -1
  82. nautobot/core/templates/inc/form_static_field.html +6 -0
  83. nautobot/core/templates/inc/header.html +11 -1
  84. nautobot/core/templates/inc/image_attachments.html +2 -1
  85. nautobot/core/templates/inc/media.html +14 -0
  86. nautobot/core/templates/inc/nav_menu.html +3 -9
  87. nautobot/core/templates/inc/object_details_advanced_panel.html +2 -2
  88. nautobot/core/templates/inc/search_panel.html +4 -4
  89. nautobot/core/templates/login.html +4 -2
  90. nautobot/core/templates/nautobot_config.py.j2 +6 -11
  91. nautobot/core/templates/redoc_ui.html +7 -0
  92. nautobot/core/templates/rest_framework/api.html +103 -2
  93. nautobot/core/templates/search.html +1 -1
  94. nautobot/core/templates/swagger_ui.html +17 -3
  95. nautobot/core/templates/system_jobs/import_objects.html +1 -2
  96. nautobot/core/templates/utilities/confirmation_form.html +2 -2
  97. nautobot/core/templates/utilities/obj_table.html +10 -2
  98. nautobot/core/templates/utilities/render_field.html +7 -7
  99. nautobot/core/templates/utilities/render_jinja2.html +2 -2
  100. nautobot/core/templates/utilities/templatetags/filter_form_drawer.html +37 -4
  101. nautobot/core/templates/utilities/theme_preview.html +19 -3
  102. nautobot/core/templates/widgets/number_input_with_choices.html +44 -0
  103. nautobot/core/templates/widgets/selectwithdisabled_option.html +3 -1
  104. nautobot/core/templatetags/helpers.py +76 -18
  105. nautobot/core/testing/api.py +68 -9
  106. nautobot/core/testing/filters.py +0 -23
  107. nautobot/core/testing/integration.py +41 -17
  108. nautobot/core/testing/mixins.py +2 -0
  109. nautobot/core/testing/utils.py +18 -4
  110. nautobot/core/testing/views.py +104 -13
  111. nautobot/core/tests/integration/test_app_home.py +34 -30
  112. nautobot/core/tests/integration/test_app_navbar.py +3 -0
  113. nautobot/core/tests/integration/test_filters.py +48 -11
  114. nautobot/core/tests/integration/test_theme.py +22 -21
  115. nautobot/core/tests/nautobot_config.py +3 -0
  116. nautobot/core/tests/nautobot_config_without_example_apps.py +4 -0
  117. nautobot/core/tests/runner.py +8 -1
  118. nautobot/core/tests/test_api.py +5 -3
  119. nautobot/core/tests/test_breadcrumbs.py +27 -28
  120. nautobot/core/tests/test_checks.py +28 -0
  121. nautobot/core/tests/test_cli.py +40 -0
  122. nautobot/core/tests/test_config.py +2 -1
  123. nautobot/core/tests/test_forms.py +55 -13
  124. nautobot/core/tests/test_jobs.py +144 -3
  125. nautobot/core/tests/test_nautobot_server.py +2 -0
  126. nautobot/core/tests/test_navigations.py +76 -1
  127. nautobot/core/tests/test_patch_social_django.py +42 -0
  128. nautobot/core/tests/test_renderers.py +59 -0
  129. nautobot/core/tests/test_settings_schema.py +1 -0
  130. nautobot/core/tests/test_tables.py +3 -1
  131. nautobot/core/tests/test_templatetags_helpers.py +62 -13
  132. nautobot/core/tests/test_templatetags_ui_framework.py +4 -4
  133. nautobot/core/tests/test_titles.py +0 -16
  134. nautobot/core/tests/test_tree_queries.py +14 -1
  135. nautobot/core/tests/test_ui.py +123 -4
  136. nautobot/core/tests/test_utils.py +72 -5
  137. nautobot/core/tests/test_views.py +159 -31
  138. nautobot/core/ui/breadcrumbs.py +70 -29
  139. nautobot/core/ui/bulk_buttons.py +1 -1
  140. nautobot/core/ui/choices.py +143 -27
  141. nautobot/core/ui/constants.py +76 -12
  142. nautobot/core/ui/echarts.py +15 -20
  143. nautobot/core/ui/object_detail.py +143 -55
  144. nautobot/core/ui/titles.py +3 -6
  145. nautobot/core/urls.py +20 -9
  146. nautobot/core/utils/cache.py +2 -1
  147. nautobot/core/utils/filtering.py +28 -18
  148. nautobot/core/utils/lookup.py +49 -8
  149. nautobot/core/utils/module_loading.py +21 -0
  150. nautobot/core/utils/patch_social_django.py +128 -0
  151. nautobot/core/views/__init__.py +38 -1
  152. nautobot/core/views/generic.py +3 -3
  153. nautobot/core/views/mixins.py +45 -22
  154. nautobot/core/views/renderers.py +4 -3
  155. nautobot/core/views/viewsets.py +2 -1
  156. nautobot/data_validation/apps.py +1 -5
  157. nautobot/data_validation/custom_validators.py +4 -4
  158. nautobot/data_validation/filters.py +1 -1
  159. nautobot/data_validation/forms.py +40 -0
  160. nautobot/data_validation/migrations/0001_initial.py +0 -7
  161. nautobot/data_validation/migrations/0002_data_migration_from_app.py +3 -14
  162. nautobot/data_validation/models.py +16 -7
  163. nautobot/data_validation/navigation.py +8 -1
  164. nautobot/data_validation/tables.py +12 -5
  165. nautobot/data_validation/templates/data_validation/datacompliance_tab.html +1 -0
  166. nautobot/data_validation/templates/data_validation/device_constraints.html +61 -0
  167. nautobot/data_validation/tests/__init__.py +2 -2
  168. nautobot/data_validation/tests/migrations/test_migrations.py +83 -3
  169. nautobot/data_validation/tests/test_data_compliance_rules.py +12 -7
  170. nautobot/data_validation/tests/test_filters.py +8 -6
  171. nautobot/data_validation/tests/test_models.py +15 -0
  172. nautobot/data_validation/tests/test_views.py +190 -32
  173. nautobot/data_validation/urls.py +2 -5
  174. nautobot/data_validation/views.py +73 -40
  175. nautobot/dcim/api/serializers.py +3 -13
  176. nautobot/dcim/apps.py +4 -0
  177. nautobot/dcim/choices.py +65 -0
  178. nautobot/dcim/constants.py +7 -0
  179. nautobot/dcim/custom_validators.py +84 -0
  180. nautobot/dcim/factory.py +1 -1
  181. nautobot/dcim/filter_mixins.py +353 -4
  182. nautobot/dcim/{filters/__init__.py → filters.py} +15 -36
  183. nautobot/dcim/forms.py +90 -4
  184. nautobot/dcim/migrations/0075_interface_duplex_interface_speed_and_more.py +32 -0
  185. nautobot/dcim/migrations/{0075_add_deviceclusterassignment.py → 0076_add_deviceclusterassignment.py} +1 -1
  186. nautobot/dcim/migrations/{0076_device_cluster_to_clusters_data_migration.py → 0077_device_cluster_to_clusters_data_migration.py} +1 -1
  187. nautobot/dcim/migrations/{0077_remove_device_cluster.py → 0078_remove_device_cluster.py} +1 -1
  188. nautobot/dcim/migrations/0079_remove_device_location_tenant_name_uniqueness.py +16 -0
  189. nautobot/dcim/migrations/0080_device_name_data_migration.py +59 -0
  190. nautobot/dcim/migrations/0081_alter_device_device_redundancy_group_priority_and_more.py +25 -0
  191. nautobot/dcim/models/device_component_templates.py +33 -1
  192. nautobot/dcim/models/device_components.py +98 -64
  193. nautobot/dcim/models/devices.py +30 -20
  194. nautobot/dcim/navigation.py +7 -6
  195. nautobot/dcim/tables/devices.py +18 -0
  196. nautobot/dcim/tables/devicetypes.py +8 -1
  197. nautobot/dcim/tables/racks.py +0 -2
  198. nautobot/dcim/tables/template_code.py +15 -15
  199. nautobot/dcim/templates/dcim/cable_connect.html +28 -112
  200. nautobot/dcim/templates/dcim/cable_trace.html +0 -4
  201. nautobot/dcim/templates/dcim/{cable_edit.html → cable_update.html} +1 -1
  202. nautobot/dcim/templates/dcim/consoleport.html +7 -6
  203. nautobot/dcim/templates/dcim/consoleserverport.html +7 -6
  204. nautobot/dcim/templates/dcim/device/config.html +2 -2
  205. nautobot/dcim/templates/dcim/device/lldp_neighbors.html +1 -1
  206. nautobot/dcim/templates/dcim/device/status.html +8 -8
  207. nautobot/dcim/templates/dcim/device.html +1 -1
  208. nautobot/dcim/templates/dcim/device_component_add.html +2 -2
  209. nautobot/dcim/templates/dcim/device_create.html +5 -3
  210. nautobot/dcim/templates/dcim/device_interface_delete.html +1 -1
  211. nautobot/dcim/templates/dcim/device_list.html +73 -10
  212. nautobot/dcim/templates/dcim/devicebay.html +1 -1
  213. nautobot/dcim/templates/dcim/devicebay_populate.html +2 -2
  214. nautobot/dcim/templates/dcim/devicetype_component_add.html +2 -2
  215. nautobot/dcim/templates/dcim/footer_convert_to_contact_or_team_record.html +14 -0
  216. nautobot/dcim/templates/dcim/frontport.html +10 -9
  217. nautobot/dcim/templates/dcim/inc/devicetype_component_table.html +1 -1
  218. nautobot/dcim/templates/dcim/inc/edit_form_softwareversion_js.html +2 -2
  219. nautobot/dcim/templates/dcim/inc/moduletype_component_table.html +1 -1
  220. nautobot/dcim/templates/dcim/inc/rack_elevation.html +1 -1
  221. nautobot/dcim/templates/dcim/interface.html +35 -7
  222. nautobot/dcim/templates/dcim/interface_bulk_delete.html +1 -1
  223. nautobot/dcim/templates/dcim/interface_edit.html +2 -0
  224. nautobot/dcim/templates/dcim/inventoryitem.html +1 -1
  225. nautobot/dcim/templates/dcim/inventoryitem_add.html +3 -1
  226. nautobot/dcim/templates/dcim/inventoryitem_bulk_delete.html +1 -1
  227. nautobot/dcim/templates/dcim/inventoryitem_edit.html +3 -1
  228. nautobot/dcim/templates/dcim/module/base.html +49 -9
  229. nautobot/dcim/templates/dcim/module_consoleports.html +1 -1
  230. nautobot/dcim/templates/dcim/module_consoleserverports.html +1 -1
  231. nautobot/dcim/templates/dcim/module_frontports.html +1 -1
  232. nautobot/dcim/templates/dcim/module_interfaces.html +1 -1
  233. nautobot/dcim/templates/dcim/module_list.html +57 -8
  234. nautobot/dcim/templates/dcim/module_modulebays.html +1 -1
  235. nautobot/dcim/templates/dcim/module_poweroutlets.html +1 -1
  236. nautobot/dcim/templates/dcim/module_powerports.html +1 -1
  237. nautobot/dcim/templates/dcim/module_rearports.html +1 -1
  238. nautobot/dcim/templates/dcim/modulefamily_retrieve.html +1 -1
  239. nautobot/dcim/templates/dcim/moduletype_list.html +2 -2
  240. nautobot/dcim/templates/dcim/moduletype_retrieve.html +49 -9
  241. nautobot/dcim/templates/dcim/platform_create.html +1 -1
  242. nautobot/dcim/templates/dcim/poweroutlet.html +1 -1
  243. nautobot/dcim/templates/dcim/powerport.html +6 -5
  244. nautobot/dcim/templates/dcim/rack_elevation_list.html +17 -5
  245. nautobot/dcim/templates/dcim/rack_retrieve.html +22 -15
  246. nautobot/dcim/templates/dcim/rearport.html +8 -7
  247. nautobot/dcim/templates/dcim/trace/cable.html +1 -1
  248. nautobot/dcim/templates/dcim/virtualchassis_add_member.html +16 -14
  249. nautobot/dcim/templates/dcim/virtualchassis_update.html +15 -7
  250. nautobot/dcim/tests/integration/test_controller.py +4 -6
  251. nautobot/dcim/tests/integration/test_controller_managed_device_group.py +1 -5
  252. nautobot/dcim/tests/integration/test_create_device.py +0 -2
  253. nautobot/dcim/tests/integration/test_device_bulk_operations.py +1 -3
  254. nautobot/dcim/tests/integration/test_fileinputpicker.py +6 -10
  255. nautobot/dcim/tests/integration/test_location_bulk_operations.py +0 -2
  256. nautobot/dcim/tests/integration/test_module_bay_position.py +3 -4
  257. nautobot/dcim/tests/test_api.py +194 -6
  258. nautobot/dcim/tests/test_custom_validators.py +229 -0
  259. nautobot/dcim/tests/test_filters.py +55 -7
  260. nautobot/dcim/tests/test_forms.py +110 -8
  261. nautobot/dcim/tests/test_graphql.py +44 -1
  262. nautobot/dcim/tests/test_models.py +328 -4
  263. nautobot/dcim/tests/test_tables.py +160 -0
  264. nautobot/dcim/tests/test_views.py +132 -29
  265. nautobot/dcim/urls.py +64 -21
  266. nautobot/dcim/utils.py +3 -3
  267. nautobot/dcim/views.py +777 -397
  268. nautobot/extras/api/views.py +60 -45
  269. nautobot/extras/choices.py +2 -13
  270. nautobot/extras/datasources/git.py +3 -1
  271. nautobot/extras/{filters/mixins.py → filter_mixins.py} +1 -1
  272. nautobot/extras/{filters/customfields.py → filter_mixins_customfields.py} +42 -6
  273. nautobot/extras/{filters/__init__.py → filters.py} +33 -48
  274. nautobot/extras/forms/forms.py +14 -15
  275. nautobot/extras/forms/mixins.py +0 -41
  276. nautobot/extras/jobs.py +2 -0
  277. nautobot/extras/jobs_ui.py +4 -3
  278. nautobot/extras/management/__init__.py +11 -0
  279. nautobot/extras/management/commands/refresh_dynamic_group_member_caches.py +4 -1
  280. nautobot/extras/migrations/0127_approval_workflow_models.py +6 -6
  281. nautobot/extras/migrations/0129_jobresult_debug_log_count_jobresult_error_log_count_and_more.py +37 -0
  282. nautobot/extras/migrations/0130_jobresult_generate_log_entry_counts.py +42 -0
  283. nautobot/extras/migrations/0131_configcontext_device_families.py +18 -0
  284. nautobot/extras/models/__init__.py +1 -2
  285. nautobot/extras/models/approvals.py +33 -14
  286. nautobot/extras/models/change_logging.py +4 -0
  287. nautobot/extras/models/contacts.py +2 -0
  288. nautobot/extras/models/groups.py +44 -5
  289. nautobot/extras/models/jobs.py +60 -4
  290. nautobot/extras/models/mixins.py +28 -0
  291. nautobot/extras/models/models.py +23 -2
  292. nautobot/extras/models/secrets.py +1 -0
  293. nautobot/extras/models/statuses.py +0 -15
  294. nautobot/extras/navigation.py +13 -9
  295. nautobot/extras/plugins/__init__.py +33 -55
  296. nautobot/extras/plugins/marketplace_manifest.yml +49 -1
  297. nautobot/extras/plugins/tables.py +3 -3
  298. nautobot/extras/plugins/urls.py +2 -21
  299. nautobot/extras/plugins/utils.py +1 -33
  300. nautobot/extras/plugins/views.py +0 -9
  301. nautobot/extras/querysets.py +8 -0
  302. nautobot/extras/signals.py +20 -19
  303. nautobot/extras/tables.py +64 -68
  304. nautobot/extras/templates/django_ajax_tables/ajax_wrapper.html +2 -0
  305. nautobot/extras/templates/extras/approval_dashboard.html +7 -5
  306. nautobot/extras/templates/extras/approvalworkflowdefinition_update.html +4 -2
  307. nautobot/extras/templates/extras/approvalworkflowstage_retrieve.html +20 -12
  308. nautobot/extras/templates/extras/configcontext_update.html +1 -0
  309. nautobot/extras/templates/extras/configcontextschema_validation.html +2 -2
  310. nautobot/extras/templates/extras/dynamicgroup_retrieve.html +11 -5
  311. nautobot/extras/templates/extras/dynamicgroup_update.html +1 -1
  312. nautobot/extras/templates/extras/gitrepository_result.html +0 -2
  313. nautobot/extras/templates/extras/inc/approval_buttons_column.html +20 -6
  314. nautobot/extras/templates/extras/inc/bulk_edit_overridable_field.html +8 -7
  315. nautobot/extras/templates/extras/inc/configcontext_format.html +10 -3
  316. nautobot/extras/templates/extras/inc/graphqlquery_execute.html +71 -0
  317. nautobot/extras/templates/extras/inc/job_tiles.html +15 -3
  318. nautobot/extras/templates/extras/inc/json_format.html +10 -3
  319. nautobot/extras/templates/extras/inc/overridable_field.html +13 -12
  320. nautobot/extras/templates/extras/job.html +29 -12
  321. nautobot/extras/templates/extras/job_bulk_edit.html +18 -0
  322. nautobot/extras/templates/extras/job_edit.html +52 -46
  323. nautobot/extras/templates/extras/job_list.html +29 -25
  324. nautobot/extras/templates/extras/marketplace.html +5 -9
  325. nautobot/extras/templates/extras/object_configcontext.html +1 -1
  326. nautobot/extras/templates/extras/object_dynamicgroups.html +2 -2
  327. nautobot/extras/templates/extras/objectchange_retrieve.html +19 -39
  328. nautobot/extras/templates/extras/plugin_detail.html +29 -24
  329. nautobot/extras/templates/extras/plugins_list.html +16 -26
  330. nautobot/extras/templates/extras/role_retrieve.html +64 -0
  331. nautobot/extras/templates/extras/scheduledjob.html +4 -2
  332. nautobot/extras/templates/extras/secret_create.html +1 -1
  333. nautobot/extras/templatetags/custom_links.py +12 -12
  334. nautobot/extras/templatetags/job_buttons.py +14 -12
  335. nautobot/extras/test_jobs/invalid_import.py +9 -0
  336. nautobot/extras/test_jobs/log_counts_by_level.py +23 -0
  337. nautobot/extras/test_jobs/missing_import.py +11 -0
  338. nautobot/extras/tests/integration/test_computedfields.py +8 -9
  339. nautobot/extras/tests/integration/test_configcontextschema.py +27 -26
  340. nautobot/extras/tests/integration/test_customfields.py +9 -10
  341. nautobot/extras/tests/integration/test_dynamicgroups.py +12 -9
  342. nautobot/extras/tests/integration/test_plugin_banner.py +3 -0
  343. nautobot/extras/tests/integration/test_plugins.py +18 -6
  344. nautobot/extras/tests/integration/test_relationships.py +0 -2
  345. nautobot/extras/tests/test_api.py +90 -18
  346. nautobot/extras/tests/test_approvals.py +38 -38
  347. nautobot/extras/tests/test_changelog.py +59 -5
  348. nautobot/extras/tests/test_customfields.py +22 -13
  349. nautobot/extras/tests/test_customfields_filters.py +479 -0
  350. nautobot/extras/tests/test_dynamicgroups.py +39 -1
  351. nautobot/extras/tests/test_filters.py +57 -22
  352. nautobot/extras/tests/test_forms.py +18 -21
  353. nautobot/extras/tests/test_jobs.py +25 -4
  354. nautobot/extras/tests/test_migrations.py +1 -0
  355. nautobot/extras/tests/test_models.py +51 -33
  356. nautobot/extras/tests/test_plugins.py +36 -10
  357. nautobot/extras/tests/test_utils.py +3 -4
  358. nautobot/extras/tests/test_views.py +52 -112
  359. nautobot/extras/urls.py +0 -14
  360. nautobot/extras/views.py +164 -71
  361. nautobot/ipam/factory.py +7 -0
  362. nautobot/ipam/filter_mixins.py +38 -0
  363. nautobot/ipam/filters.py +53 -38
  364. nautobot/ipam/formfields.py +1 -1
  365. nautobot/ipam/forms.py +6 -3
  366. nautobot/ipam/migrations/0030_ipam__namespaces.py +13 -0
  367. nautobot/ipam/migrations/0031_ipam___data_migrations.py +4 -1
  368. nautobot/ipam/migrations/0054_namespace_tenant.py +25 -0
  369. nautobot/ipam/models.py +29 -2
  370. nautobot/ipam/navigation.py +3 -2
  371. nautobot/ipam/signals.py +71 -0
  372. nautobot/ipam/tables.py +19 -6
  373. nautobot/ipam/templates/ipam/inc/toggle_available.html +10 -10
  374. nautobot/ipam/templates/ipam/inc/vlangroup_header.html +1 -0
  375. nautobot/ipam/templates/ipam/ipaddress.html +14 -0
  376. nautobot/ipam/templates/ipam/ipaddress_merge.html +3 -3
  377. nautobot/ipam/templates/ipam/ipaddresstointerface_retrieve.html +1 -0
  378. nautobot/ipam/templates/ipam/namespace_ip_addresses.html +1 -1
  379. nautobot/ipam/templates/ipam/namespace_prefixes.html +1 -1
  380. nautobot/ipam/templates/ipam/namespace_update.html +15 -0
  381. nautobot/ipam/templates/ipam/namespace_vrfs.html +1 -1
  382. nautobot/ipam/templates/ipam/prefix_delete.html +1 -1
  383. nautobot/ipam/templates/ipam/prefix_list.html +14 -13
  384. nautobot/ipam/templates/ipam/vlan_interfaces.html +1 -1
  385. nautobot/ipam/templates/ipam/vlan_vminterfaces.html +1 -1
  386. nautobot/ipam/tests/migration/test_migrations.py +89 -0
  387. nautobot/ipam/tests/test_api.py +13 -6
  388. nautobot/ipam/tests/test_filters.py +36 -1
  389. nautobot/ipam/tests/test_forms.py +1 -1
  390. nautobot/ipam/tests/test_models.py +44 -2
  391. nautobot/ipam/tests/test_tables.py +1 -2
  392. nautobot/ipam/tests/test_utils.py +1 -1
  393. nautobot/ipam/tests/test_views.py +13 -14
  394. nautobot/ipam/ui.py +0 -17
  395. nautobot/ipam/utils/migrations.py +16 -2
  396. nautobot/ipam/utils/testing.py +9 -3
  397. nautobot/ipam/views.py +53 -11
  398. nautobot/load_balancers/__init__.py +0 -0
  399. nautobot/load_balancers/api/__init__.py +1 -0
  400. nautobot/load_balancers/api/serializers.py +75 -0
  401. nautobot/load_balancers/api/urls.py +23 -0
  402. nautobot/load_balancers/api/views.py +61 -0
  403. nautobot/load_balancers/apps.py +17 -0
  404. nautobot/load_balancers/choices.py +167 -0
  405. nautobot/load_balancers/filters.py +225 -0
  406. nautobot/load_balancers/forms.py +532 -0
  407. nautobot/load_balancers/management/commands/__init__.py +0 -0
  408. nautobot/load_balancers/management/commands/generate_load_balancer_models_test_data.py +38 -0
  409. nautobot/load_balancers/migrations/0001_initial.py +465 -0
  410. nautobot/load_balancers/migrations/0002_create_default_statuses_pool_members.py +31 -0
  411. nautobot/load_balancers/migrations/__init__.py +0 -0
  412. nautobot/load_balancers/models.py +423 -0
  413. nautobot/load_balancers/navigation.py +80 -0
  414. nautobot/load_balancers/tables.py +255 -0
  415. nautobot/load_balancers/tests/__init__.py +474 -0
  416. nautobot/load_balancers/tests/test_api.py +353 -0
  417. nautobot/load_balancers/tests/test_filters.py +134 -0
  418. nautobot/load_balancers/tests/test_forms.py +266 -0
  419. nautobot/load_balancers/tests/test_models.py +195 -0
  420. nautobot/load_balancers/tests/test_views.py +229 -0
  421. nautobot/load_balancers/urls.py +17 -0
  422. nautobot/load_balancers/views.py +248 -0
  423. nautobot/project-static/dist/css/github-dark.min.css +10 -0
  424. nautobot/project-static/dist/css/github.min.css +10 -0
  425. nautobot/project-static/dist/css/nautobot.css +1 -11
  426. nautobot/project-static/dist/css/nautobot.css.map +1 -1
  427. nautobot/project-static/dist/js/libraries.js +1 -1
  428. nautobot/project-static/dist/js/libraries.js.map +1 -1
  429. nautobot/project-static/dist/js/nautobot.js +1 -1
  430. nautobot/project-static/dist/js/nautobot.js.map +1 -1
  431. nautobot/project-static/js/cabletrace.js +1 -1
  432. nautobot/project-static/js/forms.js +13 -0
  433. nautobot/project-static/js/interface_filtering.js +20 -16
  434. nautobot/project-static/nautobot-icons/battery-3.svg +3 -0
  435. nautobot/project-static/nautobot-icons/bus-globe.svg +3 -0
  436. nautobot/project-static/nautobot-icons/bus-shield-check.svg +3 -0
  437. nautobot/project-static/nautobot-icons/bus-shield.svg +3 -0
  438. nautobot/project-static/nautobot-icons/cloud.svg +1 -1
  439. nautobot/project-static/nautobot-icons/control-panel.svg +1 -1
  440. nautobot/project-static/nautobot-icons/device-lifecycle.svg +1 -1
  441. nautobot/project-static/nautobot-icons/elements.svg +1 -1
  442. nautobot/project-static/nautobot-icons/extensibility.svg +3 -0
  443. nautobot/project-static/nautobot-icons/hammer.svg +1 -1
  444. nautobot/project-static/nautobot-icons/organization.svg +3 -0
  445. nautobot/project-static/nautobot-icons/secrets.svg +1 -1
  446. nautobot/project-static/nautobot-icons/security.svg +3 -0
  447. nautobot/project-static/nautobot-icons/server.svg +1 -1
  448. nautobot/project-static/nautobot-icons/star-filled.svg +1 -1
  449. nautobot/project-static/nautobot-icons/star.svg +1 -1
  450. nautobot/tenancy/api/serializers.py +1 -0
  451. nautobot/tenancy/api/views.py +2 -1
  452. nautobot/tenancy/{filters/__init__.py → filters.py} +2 -10
  453. nautobot/tenancy/navigation.py +3 -1
  454. nautobot/tenancy/tests/test_filters.py +0 -2
  455. nautobot/tenancy/views.py +2 -1
  456. nautobot/ui/package-lock.json +87 -4
  457. nautobot/ui/package.json +2 -1
  458. nautobot/ui/src/js/collapse.js +3 -3
  459. nautobot/ui/src/js/nautobot.js +16 -1
  460. nautobot/ui/src/js/select2.js +53 -2
  461. nautobot/ui/src/scss/colors.scss +1 -1
  462. nautobot/ui/src/scss/nautobot.scss +112 -30
  463. nautobot/ui/webpack.config.js +13 -0
  464. nautobot/users/templates/users/preferences.html +11 -2
  465. nautobot/users/templates/users/profile.html +45 -12
  466. nautobot/users/templates/users/sessionkey_delete.html +1 -1
  467. nautobot/users/tests/test_api.py +4 -0
  468. nautobot/users/views.py +4 -2
  469. nautobot/virtualization/filters.py +6 -1
  470. nautobot/virtualization/models.py +1 -68
  471. nautobot/virtualization/navigation.py +3 -2
  472. nautobot/virtualization/templates/virtualization/virtual_machine_vminterface_delete.html +1 -1
  473. nautobot/virtualization/templates/virtualization/virtualmachine_list.html +2 -2
  474. nautobot/virtualization/templates/virtualization/virtualmachine_update.html +3 -1
  475. nautobot/virtualization/tests/test_api.py +3 -0
  476. nautobot/virtualization/tests/test_filters.py +10 -1
  477. nautobot/virtualization/tests/test_models.py +45 -4
  478. nautobot/virtualization/views.py +4 -1
  479. nautobot/vpn/__init__.py +0 -0
  480. nautobot/vpn/api/serializers.py +113 -0
  481. nautobot/vpn/api/urls.py +19 -0
  482. nautobot/vpn/api/views.py +70 -0
  483. nautobot/vpn/apps.py +8 -0
  484. nautobot/vpn/choices.py +171 -0
  485. nautobot/vpn/factory.py +219 -0
  486. nautobot/vpn/filters.py +234 -0
  487. nautobot/vpn/forms.py +487 -0
  488. nautobot/vpn/homepage.py +19 -0
  489. nautobot/vpn/migrations/0001_initial.py +541 -0
  490. nautobot/vpn/migrations/0002_populate_defaults.py +199 -0
  491. nautobot/vpn/migrations/__init__.py +0 -0
  492. nautobot/vpn/models.py +535 -0
  493. nautobot/vpn/navigation.py +98 -0
  494. nautobot/vpn/tables.py +383 -0
  495. nautobot/vpn/templates/vpn/vpnprofile_create.html +150 -0
  496. nautobot/vpn/tests/__init__.py +0 -0
  497. nautobot/vpn/tests/test_api.py +336 -0
  498. nautobot/vpn/tests/test_filters.py +139 -0
  499. nautobot/vpn/tests/test_forms.py +293 -0
  500. nautobot/vpn/tests/test_models.py +147 -0
  501. nautobot/vpn/tests/test_views.py +300 -0
  502. nautobot/vpn/urls.py +16 -0
  503. nautobot/vpn/views.py +495 -0
  504. nautobot/wireless/navigation.py +3 -2
  505. nautobot/wireless/tests/integration/test_radio_profile.py +1 -5
  506. nautobot/wireless/tests/test_api.py +1 -1
  507. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/METADATA +15 -15
  508. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/RECORD +514 -572
  509. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/entry_points.txt +1 -0
  510. nautobot/circuits/templates/circuits/circuit.html +0 -2
  511. nautobot/circuits/templates/circuits/circuit_edit.html +0 -2
  512. nautobot/circuits/templates/circuits/circuit_retrieve.html +0 -2
  513. nautobot/circuits/templates/circuits/circuit_update.html +0 -1
  514. nautobot/circuits/templates/circuits/circuittermination.html +0 -2
  515. nautobot/circuits/templates/circuits/circuittermination_edit.html +0 -2
  516. nautobot/circuits/templates/circuits/circuittermination_retrieve.html +0 -2
  517. nautobot/circuits/templates/circuits/circuittermination_update.html +0 -1
  518. nautobot/circuits/templates/circuits/circuittype.html +0 -2
  519. nautobot/circuits/templates/circuits/circuittype_retrieve.html +0 -2
  520. nautobot/circuits/templates/circuits/inc/circuit_termination.html +0 -85
  521. nautobot/circuits/templates/circuits/provider.html +0 -2
  522. nautobot/circuits/templates/circuits/provider_edit.html +0 -2
  523. nautobot/circuits/templates/circuits/provider_retrieve.html +0 -1
  524. nautobot/circuits/templates/circuits/provider_update.html +0 -1
  525. nautobot/circuits/templates/circuits/providernetwork.html +0 -2
  526. nautobot/circuits/templates/circuits/providernetwork_retrieve.html +0 -2
  527. nautobot/cloud/templates/cloud/cloudaccount_retrieve.html +0 -2
  528. nautobot/cloud/templates/cloud/cloudnetwork_retrieve.html +0 -2
  529. nautobot/cloud/templates/cloud/cloudresourcetype_retrieve.html +0 -2
  530. nautobot/cloud/templates/cloud/cloudservice_retrieve.html +0 -2
  531. nautobot/core/templates/buttons/import.html +0 -9
  532. nautobot/data_validation/template_content.py +0 -42
  533. nautobot/data_validation/templates/data_validation/datacompliance_retrieve.html +0 -1
  534. nautobot/dcim/filters/mixins.py +0 -354
  535. nautobot/dcim/templates/dcim/controller/base.html +0 -2
  536. nautobot/dcim/templates/dcim/controller_retrieve.html +0 -2
  537. nautobot/dcim/templates/dcim/controller_wirelessnetworks.html +0 -2
  538. nautobot/dcim/templates/dcim/controllermanageddevicegroup_retrieve.html +0 -2
  539. nautobot/dcim/templates/dcim/device/base.html +0 -2
  540. nautobot/dcim/templates/dcim/device/consoleports.html +0 -2
  541. nautobot/dcim/templates/dcim/device/consoleserverports.html +0 -2
  542. nautobot/dcim/templates/dcim/device/devicebays.html +0 -2
  543. nautobot/dcim/templates/dcim/device/frontports.html +0 -2
  544. nautobot/dcim/templates/dcim/device/interfaces.html +0 -2
  545. nautobot/dcim/templates/dcim/device/inventory.html +0 -2
  546. nautobot/dcim/templates/dcim/device/modulebays.html +0 -2
  547. nautobot/dcim/templates/dcim/device/poweroutlets.html +0 -2
  548. nautobot/dcim/templates/dcim/device/powerports.html +0 -2
  549. nautobot/dcim/templates/dcim/device/rearports.html +0 -2
  550. nautobot/dcim/templates/dcim/device/wireless.html +0 -2
  551. nautobot/dcim/templates/dcim/device_component.html +0 -2
  552. nautobot/dcim/templates/dcim/device_edit.html +0 -2
  553. nautobot/dcim/templates/dcim/devicefamily_retrieve.html +0 -2
  554. nautobot/dcim/templates/dcim/deviceredundancygroup_retrieve.html +0 -2
  555. nautobot/dcim/templates/dcim/devicetype.html +0 -2
  556. nautobot/dcim/templates/dcim/devicetype_edit.html +0 -2
  557. nautobot/dcim/templates/dcim/devicetype_retrieve.html +0 -2
  558. nautobot/dcim/templates/dcim/inc/device_napalm_tabs.html +0 -1
  559. nautobot/dcim/templates/dcim/interfaceredundancygroup_retrieve.html +0 -2
  560. nautobot/dcim/templates/dcim/location.html +0 -2
  561. nautobot/dcim/templates/dcim/location_edit.html +0 -2
  562. nautobot/dcim/templates/dcim/location_retrieve.html +0 -243
  563. nautobot/dcim/templates/dcim/locationtype.html +0 -2
  564. nautobot/dcim/templates/dcim/locationtype_retrieve.html +0 -2
  565. nautobot/dcim/templates/dcim/manufacturer.html +0 -2
  566. nautobot/dcim/templates/dcim/modulebay_retrieve.html +0 -1
  567. nautobot/dcim/templates/dcim/platform.html +0 -2
  568. nautobot/dcim/templates/dcim/powerfeed.html +0 -2
  569. nautobot/dcim/templates/dcim/powerfeed_retrieve.html +0 -2
  570. nautobot/dcim/templates/dcim/powerpanel.html +0 -2
  571. nautobot/dcim/templates/dcim/powerpanel_edit.html +0 -2
  572. nautobot/dcim/templates/dcim/powerpanel_retrieve.html +0 -2
  573. nautobot/dcim/templates/dcim/rack.html +0 -2
  574. nautobot/dcim/templates/dcim/rack_edit.html +0 -2
  575. nautobot/dcim/templates/dcim/rackgroup.html +0 -2
  576. nautobot/dcim/templates/dcim/rackreservation.html +0 -2
  577. nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +0 -2
  578. nautobot/dcim/templates/dcim/softwareversion_retrieve.html +0 -2
  579. nautobot/dcim/templates/dcim/virtualchassis.html +0 -2
  580. nautobot/dcim/templates/dcim/virtualchassis_add.html +0 -2
  581. nautobot/dcim/templates/dcim/virtualchassis_edit.html +0 -2
  582. nautobot/dcim/templates/dcim/virtualchassis_retrieve.html +0 -2
  583. nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +0 -2
  584. nautobot/dcim/ui.py +0 -29
  585. nautobot/extras/templates/extras/computedfield.html +0 -2
  586. nautobot/extras/templates/extras/computedfield_retrieve.html +0 -2
  587. nautobot/extras/templates/extras/configcontext.html +0 -2
  588. nautobot/extras/templates/extras/configcontext_edit.html +0 -2
  589. nautobot/extras/templates/extras/configcontext_retrieve.html +0 -2
  590. nautobot/extras/templates/extras/configcontextschema.html +0 -2
  591. nautobot/extras/templates/extras/configcontextschema_edit.html +0 -2
  592. nautobot/extras/templates/extras/contact_retrieve.html +0 -2
  593. nautobot/extras/templates/extras/customfield.html +0 -2
  594. nautobot/extras/templates/extras/customfield_edit.html +0 -2
  595. nautobot/extras/templates/extras/customfield_retrieve.html +0 -2
  596. nautobot/extras/templates/extras/customlink.html +0 -2
  597. nautobot/extras/templates/extras/dynamicgroup.html +0 -2
  598. nautobot/extras/templates/extras/dynamicgroup_edit.html +0 -2
  599. nautobot/extras/templates/extras/exporttemplate.html +0 -2
  600. nautobot/extras/templates/extras/gitrepository.html +0 -2
  601. nautobot/extras/templates/extras/gitrepository_object_edit.html +0 -2
  602. nautobot/extras/templates/extras/graphqlquery.html +0 -2
  603. nautobot/extras/templates/extras/graphqlquery_list.html +0 -1
  604. nautobot/extras/templates/extras/graphqlquery_retrieve.html +0 -97
  605. nautobot/extras/templates/extras/job_detail.html +0 -2
  606. nautobot/extras/templates/extras/jobbutton_retrieve.html +0 -2
  607. nautobot/extras/templates/extras/jobhook.html +0 -2
  608. nautobot/extras/templates/extras/jobqueue_retrieve.html +0 -2
  609. nautobot/extras/templates/extras/jobresult.html +0 -2
  610. nautobot/extras/templates/extras/metadatatype_retrieve.html +0 -2
  611. nautobot/extras/templates/extras/note.html +0 -2
  612. nautobot/extras/templates/extras/note_retrieve.html +0 -1
  613. nautobot/extras/templates/extras/object_changelog.html +0 -2
  614. nautobot/extras/templates/extras/object_notes.html +0 -2
  615. nautobot/extras/templates/extras/objectchange.html +0 -2
  616. nautobot/extras/templates/extras/objectchange_list.html +0 -3
  617. nautobot/extras/templates/extras/relationship.html +0 -1
  618. nautobot/extras/templates/extras/secret.html +0 -1
  619. nautobot/extras/templates/extras/secret_edit.html +0 -1
  620. nautobot/extras/templates/extras/secretsgroup.html +0 -2
  621. nautobot/extras/templates/extras/secretsgroup_edit.html +0 -2
  622. nautobot/extras/templates/extras/secretsgroup_retrieve.html +0 -2
  623. nautobot/extras/templates/extras/status.html +0 -2
  624. nautobot/extras/templates/extras/tag.html +0 -2
  625. nautobot/extras/templates/extras/tag_edit.html +0 -2
  626. nautobot/extras/templates/extras/tag_retrieve.html +0 -2
  627. nautobot/extras/templates/extras/team_retrieve.html +0 -2
  628. nautobot/ipam/templates/ipam/inc/prefix_header_extra_content_table.html +0 -4
  629. nautobot/ipam/templates/ipam/namespace_retrieve.html +0 -1
  630. nautobot/ipam/templates/ipam/prefix.html +0 -2
  631. nautobot/ipam/templates/ipam/prefix_edit.html +0 -1
  632. nautobot/ipam/templates/ipam/prefix_retrieve.html +0 -2
  633. nautobot/ipam/templates/ipam/rir.html +0 -2
  634. nautobot/ipam/templates/ipam/routetarget.html +0 -1
  635. nautobot/ipam/templates/ipam/service.html +0 -2
  636. nautobot/ipam/templates/ipam/service_edit.html +0 -2
  637. nautobot/ipam/templates/ipam/service_retrieve.html +0 -2
  638. nautobot/ipam/templates/ipam/vlan.html +0 -2
  639. nautobot/ipam/templates/ipam/vlan_edit.html +0 -2
  640. nautobot/ipam/templates/ipam/vlan_retrieve.html +0 -2
  641. nautobot/ipam/templates/ipam/vlangroup.html +0 -2
  642. nautobot/ipam/templates/ipam/vrf.html +0 -1
  643. nautobot/tenancy/templates/tenancy/tenant.html +0 -2
  644. nautobot/tenancy/templates/tenancy/tenant_edit.html +0 -2
  645. nautobot/tenancy/templates/tenancy/tenantgroup.html +0 -2
  646. nautobot/tenancy/templates/tenancy/tenantgroup_retrieve.html +0 -1
  647. nautobot/virtualization/templates/virtualization/clustergroup.html +0 -2
  648. nautobot/virtualization/templates/virtualization/clustertype.html +0 -2
  649. nautobot/virtualization/templates/virtualization/virtualmachine.html +0 -2
  650. nautobot/virtualization/templates/virtualization/virtualmachine_edit.html +0 -2
  651. nautobot/virtualization/templates/virtualization/virtualmachine_retrieve.html +0 -2
  652. nautobot/wireless/templates/wireless/radioprofile_retrieve.html +0 -2
  653. nautobot/wireless/templates/wireless/supporteddatarate_retrieve.html +0 -2
  654. nautobot/wireless/templates/wireless/wirelessnetwork_retrieve.html +0 -2
  655. /nautobot/dcim/templates/dcim/{cable.html → cable_retrieve.html} +0 -0
  656. /nautobot/tenancy/{filters/mixins.py → filter_mixins.py} +0 -0
  657. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/LICENSE.txt +0 -0
  658. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/NOTICE +0 -0
  659. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/WHEEL +0 -0
@@ -39,31 +39,31 @@ class ApprovalWorkflowTestMixin:
39
39
  cls.approval_workflow_def_1 = models.ApprovalWorkflowDefinition.objects.create(
40
40
  name="Test Approval Workflow Definition 1",
41
41
  model_content_type=scheduled_job_ct,
42
- priority=0,
42
+ weight=0,
43
43
  )
44
44
  cls.approval_workflow_def_2 = models.ApprovalWorkflowDefinition.objects.create(
45
45
  name="Test Approval Workflow Definition 2",
46
46
  model_content_type=scheduled_job_ct,
47
47
  model_constraints={"name": "Bulk Delete Objects"},
48
- priority=1,
48
+ weight=1,
49
49
  )
50
50
  cls.approval_workflow_def_3 = models.ApprovalWorkflowDefinition.objects.create(
51
51
  name="Test Approval Workflow Definition 3",
52
52
  model_content_type=scheduled_job_ct,
53
53
  model_constraints={"name": "Bulk Delete Objects"},
54
- priority=2,
54
+ weight=2,
55
55
  )
56
56
  cls.approval_workflow_def_4 = models.ApprovalWorkflowDefinition.objects.create(
57
57
  name="Test Approval Workflow Definition 4",
58
58
  model_content_type=scheduled_job_ct,
59
59
  model_constraints={"name": "Bulk Delete Objects"},
60
- priority=3,
60
+ weight=3,
61
61
  )
62
62
  cls.approval_workflow_def_5 = models.ApprovalWorkflowDefinition.objects.create(
63
63
  name="Test Approval Workflow Definition 5",
64
64
  model_content_type=scheduled_job_ct,
65
65
  model_constraints={"name": "Bulk Delete Objects"},
66
- priority=4,
66
+ weight=4,
67
67
  )
68
68
  cls.approval_workflow_1_instance_1 = models.ApprovalWorkflow.objects.create(
69
69
  approval_workflow_definition=cls.approval_workflow_def_1,
@@ -97,7 +97,7 @@ class ApprovalWorkflowTestMixin:
97
97
  )
98
98
  cls.approval_workflow_1_stage_def_1 = models.ApprovalWorkflowStageDefinition.objects.create(
99
99
  approval_workflow_definition=cls.approval_workflow_def_1,
100
- weight=100,
100
+ sequence=100,
101
101
  name="Test Approval Workflow 1 Stage 1",
102
102
  min_approvers=2,
103
103
  denial_message="Stage 1 Denial Message",
@@ -105,7 +105,7 @@ class ApprovalWorkflowTestMixin:
105
105
  )
106
106
  cls.approval_workflow_1_stage_def_2 = models.ApprovalWorkflowStageDefinition.objects.create(
107
107
  approval_workflow_definition=cls.approval_workflow_def_1,
108
- weight=200,
108
+ sequence=200,
109
109
  name="Test Approval Workflow 1 Stage 2",
110
110
  min_approvers=2,
111
111
  denial_message="Stage 2 Denial Message",
@@ -113,7 +113,7 @@ class ApprovalWorkflowTestMixin:
113
113
  )
114
114
  cls.approval_workflow_1_stage_def_3 = models.ApprovalWorkflowStageDefinition.objects.create(
115
115
  approval_workflow_definition=cls.approval_workflow_def_1,
116
- weight=300,
116
+ sequence=300,
117
117
  name="Test Approval Workflow 1 Stage 3",
118
118
  min_approvers=2,
119
119
  denial_message="Stage 3 Denial Message",
@@ -121,7 +121,7 @@ class ApprovalWorkflowTestMixin:
121
121
  )
122
122
  cls.approval_workflow_1_stage_def_4 = models.ApprovalWorkflowStageDefinition.objects.create(
123
123
  approval_workflow_definition=cls.approval_workflow_def_1,
124
- weight=400,
124
+ sequence=400,
125
125
  name="Test Approval Workflow 1 Stage 4",
126
126
  min_approvers=2,
127
127
  denial_message="Stage 4 Denial Message",
@@ -129,7 +129,7 @@ class ApprovalWorkflowTestMixin:
129
129
  )
130
130
  cls.approval_workflow_1_stage_def_5 = models.ApprovalWorkflowStageDefinition.objects.create(
131
131
  approval_workflow_definition=cls.approval_workflow_def_1,
132
- weight=500,
132
+ sequence=500,
133
133
  name="Test Approval Workflow 1 Stage 5",
134
134
  min_approvers=2,
135
135
  denial_message="Stage 5 Denial Message",
@@ -137,7 +137,7 @@ class ApprovalWorkflowTestMixin:
137
137
  )
138
138
  cls.approval_workflow_1_stage_def_6 = models.ApprovalWorkflowStageDefinition.objects.create(
139
139
  approval_workflow_definition=cls.approval_workflow_def_1,
140
- weight=600,
140
+ sequence=600,
141
141
  name="Test Approval Workflow 1 Stage 6",
142
142
  min_approvers=2,
143
143
  denial_message="Stage 6 Denial Message",
@@ -145,7 +145,7 @@ class ApprovalWorkflowTestMixin:
145
145
  )
146
146
  cls.approval_workflow_2_stage_def_1 = models.ApprovalWorkflowStageDefinition.objects.create(
147
147
  approval_workflow_definition=cls.approval_workflow_def_2,
148
- weight=100,
148
+ sequence=100,
149
149
  name="Test Approval Workflow 2 Stage 1",
150
150
  min_approvers=2,
151
151
  denial_message="Stage 1 Denial Message",
@@ -153,7 +153,7 @@ class ApprovalWorkflowTestMixin:
153
153
  )
154
154
  cls.approval_workflow_2_stage_def_2 = models.ApprovalWorkflowStageDefinition.objects.create(
155
155
  approval_workflow_definition=cls.approval_workflow_def_2,
156
- weight=200,
156
+ sequence=200,
157
157
  name="Test Approval Workflow 2 Stage 2",
158
158
  min_approvers=2,
159
159
  denial_message="Stage 2 Denial Message",
@@ -161,7 +161,7 @@ class ApprovalWorkflowTestMixin:
161
161
  )
162
162
  cls.approval_workflow_2_stage_def_3 = models.ApprovalWorkflowStageDefinition.objects.create(
163
163
  approval_workflow_definition=cls.approval_workflow_def_2,
164
- weight=300,
164
+ sequence=300,
165
165
  name="Test Approval Workflow 2 Stage 3",
166
166
  min_approvers=2,
167
167
  denial_message="Stage 3 Denial Message",
@@ -232,19 +232,19 @@ class ApprovalWorkflowDefinitionAPITest(ApprovalWorkflowTestMixin, APIViewTestCa
232
232
  "name": "Approval Workflow Definition 1",
233
233
  "model_content_type": "extras.scheduledjob",
234
234
  "model_constraints": {"name": "Bulk Delete Objects"},
235
- "priority": 5,
235
+ "weight": 5,
236
236
  },
237
237
  {
238
238
  "name": "Approval Workflow Definition 2",
239
239
  "model_content_type": "extras.scheduledjob",
240
240
  "model_constraints": {},
241
- "priority": 6,
241
+ "weight": 6,
242
242
  },
243
243
  {
244
244
  "name": "Approval Workflow Definition 3",
245
245
  "model_content_type": "extras.scheduledjob",
246
246
  "model_constraints": {"name": "Bulk Delete Objects"},
247
- "priority": 7,
247
+ "weight": 7,
248
248
  },
249
249
  ]
250
250
 
@@ -271,18 +271,18 @@ class ApprovalWorkflowDefinitionManagerTest(TestCase):
271
271
  models.ApprovalWorkflowDefinition.objects.create(
272
272
  name=f"Test Approval Workflow Definition {i}",
273
273
  model_content_type=scheduled_job_ct,
274
- priority=i,
274
+ weight=len(range(4)) - 1 - i, # first with highest weight
275
275
  )
276
276
  for i in range(4)
277
277
  ]
278
278
 
279
279
  def test_find_for_model(self):
280
- """Test that the workflow definition with the highest priority and no constraints is returned."""
280
+ """Test that the workflow definition with the highest weight and no constraints is returned."""
281
281
  self.assertEqual(
282
282
  models.ApprovalWorkflowDefinition.objects.find_for_model(self.scheduled_job),
283
283
  self.approval_workflow_defs[0],
284
284
  )
285
- self.assertEqual(self.approval_workflow_defs[0].priority, 0)
285
+ self.assertEqual(self.approval_workflow_defs[0].weight, 3)
286
286
 
287
287
  def test_find_for_model_with_filter_match_constraints(self):
288
288
  """Test that a workflow definition with filter matching constraints is correctly returned."""
@@ -294,7 +294,7 @@ class ApprovalWorkflowDefinitionManagerTest(TestCase):
294
294
  models.ApprovalWorkflowDefinition.objects.find_for_model(self.scheduled_job),
295
295
  self.approval_workflow_defs[0],
296
296
  )
297
- self.assertEqual(self.approval_workflow_defs[0].priority, 0)
297
+ self.assertEqual(self.approval_workflow_defs[0].weight, 3)
298
298
 
299
299
  export_job_model = models.Job.objects.get_for_class_path(ExportObjectList.class_path)
300
300
  export_scheduled_job = models.ScheduledJob.objects.create(
@@ -309,7 +309,7 @@ class ApprovalWorkflowDefinitionManagerTest(TestCase):
309
309
  models.ApprovalWorkflowDefinition.objects.find_for_model(export_scheduled_job),
310
310
  self.approval_workflow_defs[0],
311
311
  )
312
- self.assertEqual(self.approval_workflow_defs[0].priority, 0)
312
+ self.assertEqual(self.approval_workflow_defs[0].weight, 3)
313
313
 
314
314
  def test_find_for_model_with_exact_match_constraints(self):
315
315
  """Test that a workflow definition with exact matching constraints is correctly returned."""
@@ -319,12 +319,12 @@ class ApprovalWorkflowDefinitionManagerTest(TestCase):
319
319
  models.ApprovalWorkflowDefinition.objects.find_for_model(self.scheduled_job),
320
320
  self.approval_workflow_defs[0],
321
321
  )
322
- self.assertEqual(self.approval_workflow_defs[0].priority, 0)
322
+ self.assertEqual(self.approval_workflow_defs[0].weight, 3)
323
323
 
324
- def test_find_for_model_returns_highest_priority_when_all_match(self):
324
+ def test_find_for_model_returns_highest_weight_when_all_match(self):
325
325
  """
326
326
  Test that when all workflow definitions match the model instance,
327
- the one with the highest priority (lowest number) is returned.
327
+ the one with the highest weight is returned.
328
328
  """
329
329
  # Set all definitions to have matching constraints
330
330
  for definition in self.approval_workflow_defs:
@@ -335,14 +335,14 @@ class ApprovalWorkflowDefinitionManagerTest(TestCase):
335
335
  models.ApprovalWorkflowDefinition.objects.find_for_model(self.scheduled_job),
336
336
  self.approval_workflow_defs[0],
337
337
  )
338
- self.assertEqual(self.approval_workflow_defs[0].priority, 0)
338
+ self.assertEqual(self.approval_workflow_defs[0].weight, 3)
339
339
 
340
340
  def test_find_for_model_skips_unmatched_constraints_and_returns_next_without_constraints(self):
341
341
  """
342
- Test that if the highest priority workflow definition has unmatched constraints,
342
+ Test that if the highest weight workflow definition has unmatched constraints,
343
343
  the method skips it and returns the next one with no constraints.
344
344
  """
345
- # Set constraints on the highest priority definition that do not match the instance
345
+ # Set constraints on the highest weight definition that do not match the instance
346
346
  self.approval_workflow_defs[0].model_constraints = {"name": "Non Matching Name"}
347
347
  self.approval_workflow_defs[0].save()
348
348
 
@@ -354,12 +354,12 @@ class ApprovalWorkflowDefinitionManagerTest(TestCase):
354
354
  models.ApprovalWorkflowDefinition.objects.find_for_model(self.scheduled_job),
355
355
  self.approval_workflow_defs[1],
356
356
  )
357
- self.assertEqual(self.approval_workflow_defs[1].priority, 1)
357
+ self.assertEqual(self.approval_workflow_defs[1].weight, 2)
358
358
 
359
- def test_find_for_model_matches_lower_priority_if_higher_fails(self):
359
+ def test_find_for_model_matches_lower_weight_if_higher_fails(self):
360
360
  """
361
- Test that if the highest priority workflow definition's constraints don't match,
362
- the next matching lower-priority definition is returned.
361
+ Test that if the highest weight workflow definition's constraints don't match,
362
+ the next matching lower-weight definition is returned.
363
363
  """
364
364
  self.approval_workflow_defs[0].model_constraints = {"name": "Non Matching Name"}
365
365
  self.approval_workflow_defs[0].save()
@@ -371,7 +371,7 @@ class ApprovalWorkflowDefinitionManagerTest(TestCase):
371
371
  models.ApprovalWorkflowDefinition.objects.find_for_model(self.scheduled_job),
372
372
  self.approval_workflow_defs[1],
373
373
  )
374
- self.assertEqual(self.approval_workflow_defs[1].priority, 1)
374
+ self.assertEqual(self.approval_workflow_defs[1].weight, 2)
375
375
 
376
376
  def test_find_for_model_ignores_constraints_matching_different_instance(self):
377
377
  """
@@ -412,7 +412,7 @@ class ApprovalWorkflowStageDefinitionAPITest(ApprovalWorkflowTestMixin, APIViewT
412
412
  cls.create_data = [
413
413
  {
414
414
  "approval_workflow_definition": cls.approval_workflow_def_2.pk,
415
- "weight": 400,
415
+ "sequence": 400,
416
416
  "name": "Test Approval Workflow 2 Stage 4 Definition",
417
417
  "min_approvers": 3,
418
418
  "denial_message": "Stage 4 Denial Message",
@@ -420,7 +420,7 @@ class ApprovalWorkflowStageDefinitionAPITest(ApprovalWorkflowTestMixin, APIViewT
420
420
  },
421
421
  {
422
422
  "approval_workflow_definition": cls.approval_workflow_def_2.pk,
423
- "weight": 500,
423
+ "sequence": 500,
424
424
  "name": "Test Approval Workflow 2 Stage 5 Definition",
425
425
  "min_approvers": 2,
426
426
  "denial_message": "Stage 5 Denial Message",
@@ -428,7 +428,7 @@ class ApprovalWorkflowStageDefinitionAPITest(ApprovalWorkflowTestMixin, APIViewT
428
428
  },
429
429
  {
430
430
  "approval_workflow_definition": cls.approval_workflow_def_2.pk,
431
- "weight": 600,
431
+ "sequence": 600,
432
432
  "name": "Test Approval Workflow 2 Stage 6 Definition",
433
433
  "min_approvers": 1,
434
434
  "denial_message": "Stage 6 Denial Message",
@@ -437,7 +437,7 @@ class ApprovalWorkflowStageDefinitionAPITest(ApprovalWorkflowTestMixin, APIViewT
437
437
  ]
438
438
 
439
439
  cls.update_data = {
440
- "weight": 700,
440
+ "sequence": 700,
441
441
  "approval_workflow_definition": cls.approval_workflow_def_2.pk,
442
442
  "min_approvers": 4,
443
443
  "denial_message": "Updated Denial Message",
@@ -505,7 +505,7 @@ class ApprovalWorkflowTriggerAPITest(APITestCase):
505
505
  )
506
506
  models.ApprovalWorkflowStageDefinition.objects.create(
507
507
  approval_workflow_definition=self.approval_workflow_def,
508
- weight=100,
508
+ sequence=100,
509
509
  name="Test Approval Workflow Stage 1",
510
510
  min_approvers=2,
511
511
  denial_message="Stage 1 Denial Message",
@@ -1,5 +1,7 @@
1
+ import uuid
2
+
1
3
  from django.contrib.contenttypes.models import ContentType
2
- from django.test import override_settings
4
+ from django.test import override_settings, tag
3
5
  from django.urls import reverse
4
6
  from django.utils.html import escape
5
7
  from rest_framework import status
@@ -22,8 +24,6 @@ from nautobot.extras.models import CustomField, CustomFieldChoice, DynamicGroup,
22
24
  from nautobot.ipam.models import VLAN, VLANGroup
23
25
  from nautobot.virtualization.models import Cluster, ClusterType, VirtualMachine, VMInterface
24
26
 
25
- from example_app.signals import EXAMPLE_APP_CUSTOM_FIELD_DEFAULT, EXAMPLE_APP_CUSTOM_FIELD_NAME
26
-
27
27
 
28
28
  class ChangeLogViewTest(ModelViewTestCase):
29
29
  model = Location
@@ -234,6 +234,32 @@ class ChangeLogViewTest(ModelViewTestCase):
234
234
  self.assertContains(resp, escape('"description": "changed description2"'))
235
235
  self.assertContains(resp, escape('"description": "changed description3"'))
236
236
 
237
+ def test_objectchange_skips_add_conditional_prefetch(self):
238
+ """
239
+ Test that ObjectChange.objects.all() skips prefetch_related on ContentTypes without a model class.
240
+ """
241
+ self.add_permissions("extras.view_objectchange")
242
+
243
+ ct = ContentType.objects.create(app_label="nonexistent_app", model="nonexistentmodel")
244
+ oc = ObjectChange.objects.create(
245
+ changed_object_type=ct,
246
+ changed_object_id=1,
247
+ object_repr="nonexistentobject",
248
+ action=ObjectChangeActionChoices.ACTION_CREATE,
249
+ user=self.user,
250
+ object_data={},
251
+ request_id=uuid.uuid4(),
252
+ )
253
+ url = reverse("extras:objectchange_list")
254
+ with self.assertLogs(level="WARNING") as cm:
255
+ response = self.client.get(url)
256
+ self.assertHttpStatus(response, 200)
257
+ self.assertContains(response, oc.object_repr)
258
+ self.assertIn(
259
+ ("One or more ContentType entries in the database are invalid."),
260
+ cm.output[0],
261
+ )
262
+
237
263
 
238
264
  class ChangeLogAPITest(APITestCase):
239
265
  def setUp(self):
@@ -260,7 +286,10 @@ class ChangeLogAPITest(APITestCase):
260
286
  self.tags = Tag.objects.get_for_model(Location)
261
287
  self.statuses = Status.objects.get_for_model(Location)
262
288
 
289
+ @tag("example_app")
263
290
  def test_create_object(self):
291
+ from example_app.signals import EXAMPLE_APP_CUSTOM_FIELD_DEFAULT, EXAMPLE_APP_CUSTOM_FIELD_NAME
292
+
264
293
  location_type = LocationType.objects.get(name="Campus")
265
294
  data = {
266
295
  "name": "Test Location 1",
@@ -290,8 +319,11 @@ class ChangeLogAPITest(APITestCase):
290
319
  self.assertEqual(oc.object_data["tags"], sorted([self.tags[0].name, self.tags[1].name]))
291
320
  self.assertEqual(oc.user_id, self.user.pk)
292
321
 
322
+ @tag("example_app")
293
323
  def test_update_object(self):
294
324
  """Test PUT with changelogs."""
325
+ from example_app.signals import EXAMPLE_APP_CUSTOM_FIELD_DEFAULT, EXAMPLE_APP_CUSTOM_FIELD_NAME
326
+
295
327
  location_type = LocationType.objects.get(name="Campus")
296
328
  location = Location.objects.create(
297
329
  name="Test Location 1",
@@ -510,13 +542,35 @@ class ObjectChangeModelTest(TestCase): # TODO: change to BaseModelTestCase once
510
542
  def setUpTestData(cls):
511
543
  cls.location_status = Status.objects.get_for_model(Location).first()
512
544
 
545
+ def test_m2m_fields_not_excluded(self):
546
+ """Ensure that m2m fields are included in object changes, even if exclude_m2m is the default in the REST API."""
547
+ with context_managers.web_request_context(self.user):
548
+ location_type = LocationType.objects.create(name="Test m2m locationtype")
549
+
550
+ with context_managers.web_request_context(self.user):
551
+ location_type.content_types.set(ContentType.objects.filter(app_label="dcim"))
552
+
553
+ object_changes = get_changes_for_model(location_type)
554
+ self.assertEqual(object_changes.count(), 2)
555
+
556
+ snapshots = object_changes.first().get_snapshots()
557
+ self.assertIsNotNone(snapshots["differences"]["removed"])
558
+ self.assertIsNotNone(snapshots["differences"]["added"])
559
+ self.assertIn("content_types", snapshots["differences"]["removed"])
560
+ self.assertIn("content_types", snapshots["differences"]["added"])
561
+ self.assertEqual(
562
+ len(snapshots["differences"]["added"]["content_types"]),
563
+ ContentType.objects.filter(app_label="dcim").count(),
564
+ )
565
+
513
566
  def test_opt_out(self):
514
567
  """Hidden static group associations can "opt out" of change logging."""
515
568
  dg = DynamicGroup.objects.exclude(group_type=DynamicGroupTypeChoices.TYPE_STATIC).first()
516
569
  # Force reassignment of all cached memberships:
517
570
  members = list(dg.members)
518
- dg._set_members([])
519
- dg._set_members(members)
571
+ with context_managers.web_request_context(self.user):
572
+ dg._set_members([])
573
+ dg._set_members(members)
520
574
 
521
575
  for sga in dg.static_group_associations(manager="all_objects").all():
522
576
  self.assertIsNone(get_changes_for_model(sga).first())
@@ -1,11 +1,11 @@
1
1
  import json
2
2
  import logging
3
3
 
4
- from django.conf import settings
5
4
  from django.contrib.contenttypes.models import ContentType
6
5
  from django.core.exceptions import ValidationError
7
6
  from django.db.models import ProtectedError, QuerySet
8
7
  from django.forms import ChoiceField, IntegerField, NumberInput
8
+ from django.test import tag
9
9
  from django.urls import reverse
10
10
  from rest_framework import status
11
11
 
@@ -31,6 +31,7 @@ from nautobot.virtualization.models import VirtualMachine
31
31
  # TODO: this needs to be both a BaseModelTestCase (as it tests the model class) and a (views) TestCase,
32
32
  # (due to the test_multi_select_field_value_after_bulk_update() test).
33
33
  # At some point we should probably split this into separate classes.
34
+ @tag("example_app")
34
35
  class CustomFieldTest(ModelTestCases.BaseModelTestCase, TestCase):
35
36
  model = CustomField
36
37
 
@@ -397,6 +398,7 @@ class CustomFieldTest(ModelTestCases.BaseModelTestCase, TestCase):
397
398
  self.assertIsInstance(filter_field.widget, NumberInput)
398
399
 
399
400
 
401
+ @tag("example_app")
400
402
  class CustomFieldManagerTest(TestCase):
401
403
  def setUp(self):
402
404
  self.content_type = ContentType.objects.get_for_model(Location)
@@ -569,6 +571,7 @@ class ComputedFieldManagerTestCase(TestCase):
569
571
  self.assertQuerysetEqualAndNotEmpty(qs, listing)
570
572
 
571
573
 
574
+ @tag("example_app")
572
575
  class CustomFieldDataAPITest(APITestCase):
573
576
  """
574
577
  Check that object representations in the REST API include their custom field data.
@@ -693,9 +696,8 @@ class CustomFieldDataAPITest(APITestCase):
693
696
  self.cf_json,
694
697
  ]
695
698
 
696
- if "example_app" in settings.PLUGINS:
697
- self.cf_plugin_field = CustomField.objects.get(key="example_app_auto_custom_field")
698
- self.all_cfs.append(self.cf_plugin_field)
699
+ self.cf_plugin_field = CustomField.objects.get(key="example_app_auto_custom_field")
700
+ self.all_cfs.append(self.cf_plugin_field)
699
701
  self.statuses = Status.objects.get_for_model(Location)
700
702
 
701
703
  # Create some locations
@@ -717,8 +719,7 @@ class CustomFieldDataAPITest(APITestCase):
717
719
  self.cf_markdown.key: "### Hello world!\n\n- Item 1\n- Item 2\n- Item 3",
718
720
  self.cf_json.key: {"hello": "world"},
719
721
  }
720
- if "example_app" in settings.PLUGINS:
721
- self.locations[1]._custom_field_data[self.cf_plugin_field.key] = "Custom value"
722
+ self.locations[1]._custom_field_data[self.cf_plugin_field.key] = "Custom value"
722
723
  self.locations[1].validated_save()
723
724
  self.list_url = reverse("dcim-api:location-list")
724
725
  self.detail_url = reverse("dcim-api:location-detail", kwargs={"pk": self.locations[1].pk})
@@ -792,8 +793,7 @@ class CustomFieldDataAPITest(APITestCase):
792
793
  self.cf_json.key: {"foo": "bar"},
793
794
  },
794
795
  }
795
- if "example_app" in settings.PLUGINS:
796
- data["custom_fields"]["example_app_auto_custom_field"] = "Custom value"
796
+ data["custom_fields"]["example_app_auto_custom_field"] = "Custom value"
797
797
  response = self.client.post(self.list_url, data, format="json", **self.header)
798
798
  self.assertHttpStatus(response, status.HTTP_201_CREATED)
799
799
 
@@ -865,9 +865,8 @@ class CustomFieldDataAPITest(APITestCase):
865
865
  self.cf_markdown.key: "### Heading",
866
866
  self.cf_json.key: {"dict1": {"dict2": {}}},
867
867
  }
868
- if "example_app" in settings.PLUGINS:
869
- self.cf_plugin_field = CustomField.objects.get(key="example_app_auto_custom_field")
870
- custom_field_data[self.cf_plugin_field.key] = "Custom Value"
868
+ self.cf_plugin_field = CustomField.objects.get(key="example_app_auto_custom_field")
869
+ custom_field_data[self.cf_plugin_field.key] = "Custom Value"
871
870
  data = (
872
871
  {
873
872
  "name": "Location 3",
@@ -1169,6 +1168,7 @@ class CustomFieldDataAPITest(APITestCase):
1169
1168
  self.assertContains(response, "Invalid choice", status_code=status.HTTP_400_BAD_REQUEST)
1170
1169
 
1171
1170
 
1171
+ @tag("example_app")
1172
1172
  class CustomFieldImportTest(TestCase):
1173
1173
  """
1174
1174
  Test importing object custom field data along with the object itself.
@@ -2009,20 +2009,29 @@ class CustomFieldFilterTest(TestCase):
2009
2009
  )
2010
2010
 
2011
2011
  def test_filter_multi_select(self):
2012
- self.assertQuerysetEqual(
2012
+ self.assertQuerysetEqualAndNotEmpty(
2013
2013
  self.filterset({"cf_cf9": "Foo"}, self.queryset).qs,
2014
2014
  self.queryset.filter(_custom_field_data__cf9__contains="Foo"),
2015
2015
  )
2016
- self.assertQuerysetEqual(
2016
+ self.assertQuerysetEqualAndNotEmpty(
2017
2017
  self.filterset({"cf_cf9": "Bar"}, self.queryset).qs,
2018
2018
  self.queryset.filter(_custom_field_data__cf9__contains="Bar"),
2019
2019
  )
2020
+ self.assertQuerysetEqualAndNotEmpty(
2021
+ self.filterset({"cf_cf9": ["Foo"]}, self.queryset).qs,
2022
+ self.queryset.filter(_custom_field_data__cf9__contains="Foo"),
2023
+ )
2024
+ self.assertQuerysetEqualAndNotEmpty(
2025
+ self.filterset({"cf_cf9": ["Bar"]}, self.queryset).qs,
2026
+ self.queryset.filter(_custom_field_data__cf9__contains="Bar"),
2027
+ )
2020
2028
  self.assertQuerysetEqualAndNotEmpty( # https://github.com/nautobot/nautobot/issues/5009
2021
2029
  self.filterset({"cf_cf9": str(self.multiselect_choices[0].pk)}, self.queryset).qs,
2022
2030
  self.queryset.filter(_custom_field_data__cf9__contains=self.multiselect_choices[0].value),
2023
2031
  )
2024
2032
 
2025
2033
 
2034
+ @tag("example_app")
2026
2035
  class CustomFieldChoiceTest(ModelTestCases.BaseModelTestCase):
2027
2036
  model = CustomFieldChoice
2028
2037