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,11 +39,12 @@ class NautobotParallelTestSuite(ParallelTestSuite):
39
39
 
40
40
  class NautobotTestRunner(DiscoverRunner):
41
41
  """
42
- Custom test runner that excludes (slow) integration and migration tests by default.
42
+ Custom test runner that excludes (slow) integration and migration tests by default among others.
43
43
 
44
44
  This test runner is aware of our use of the "integration" tag and only runs integration tests if
45
45
  explicitly passed in with `nautobot-server test --tag integration`.
46
46
  Similarly, it only runs migration tests if explicitly called with `--tag migration_test`.
47
+ Similarly, it only runs tests that require the example app(s) if those are present in settings.PLUGINS.
47
48
 
48
49
  By Nautobot convention, integration tests must be tagged with "integration". The base
49
50
  `nautobot.core.testing.integration.SeleniumTestCase` has this tag, therefore any test cases
@@ -58,6 +59,10 @@ class NautobotTestRunner(DiscoverRunner):
58
59
  parallel_test_suite = NautobotParallelTestSuite
59
60
 
60
61
  exclude_tags = ["integration", "migration_test"]
62
+ if "example_app" not in settings.PLUGINS:
63
+ exclude_tags.append("example_app")
64
+ if "example_app_with_view_override" not in settings.PLUGINS:
65
+ exclude_tags.append("example_app_with_view_override")
61
66
 
62
67
  @classmethod
63
68
  def add_arguments(cls, parser):
@@ -157,6 +162,8 @@ class NautobotTestRunner(DiscoverRunner):
157
162
  db_command = [*command, "--database", alias]
158
163
  call_command(*db_command)
159
164
 
165
+ call_command("post_upgrade")
166
+
160
167
  if self.parallel > 1:
161
168
  for index in range(self.parallel):
162
169
  with time_keeper.timed(f" Cloning '{alias}'"):
@@ -533,10 +533,12 @@ class ModelViewSetMixinTest(testing.APITestCase):
533
533
  self.user.is_superuser = True
534
534
  self.user.save()
535
535
 
536
- # Default behavior - m2m fields included
536
+ # With exclude_m2m query parameter set to False
537
537
  view = self.SimpleIPAddressViewSet()
538
538
  view.action_map = {"get": "list"}
539
- request = APIRequestFactory().get(reverse("ipam-api:ipaddress-list"), headers=self.header)
539
+ request = APIRequestFactory().get(
540
+ reverse("ipam-api:ipaddress-list"), headers=self.header, data={"exclude_m2m": False}
541
+ )
540
542
  force_authenticate(request, user=self.user)
541
543
  request = view.initialize_request(request)
542
544
  view.setup(request)
@@ -561,7 +563,7 @@ class ModelViewSetMixinTest(testing.APITestCase):
561
563
  list(instance.vm_interfaces.all())
562
564
  list(instance.tags.all())
563
565
 
564
- # With exclude_m2m query parameter
566
+ # With exclude_m2m query parameter set to True
565
567
  view = self.SimpleIPAddressViewSet()
566
568
  view.action_map = {"get": "list"}
567
569
  request = APIRequestFactory().get(
@@ -35,7 +35,7 @@ class BreadcrumbItemsTestCase(TestCase):
35
35
  item = ViewNameBreadcrumbItem(view_name="home", label="Home")
36
36
  context = Context({})
37
37
 
38
- url, label = item.as_pair(context)
38
+ url, label = next(item.as_pair(context))
39
39
 
40
40
  self.assertEqual(url, "/")
41
41
  self.assertEqual(label, "Home")
@@ -50,7 +50,7 @@ class BreadcrumbItemsTestCase(TestCase):
50
50
  )
51
51
  context = Context({})
52
52
 
53
- url, label = item.as_pair(context)
53
+ url, label = next(item.as_pair(context))
54
54
 
55
55
  self.assertEqual(url, f"/dcim/location-types/{self.location_type.pk}/?name=test")
56
56
  self.assertEqual(label, "Filtered Locations Types")
@@ -65,7 +65,7 @@ class BreadcrumbItemsTestCase(TestCase):
65
65
  )
66
66
  context = Context({"object": self.location_type})
67
67
 
68
- url, label = item.as_pair(context)
68
+ url, label = next(item.as_pair(context))
69
69
 
70
70
  self.assertEqual(
71
71
  url, f"/dcim/location-types/{self.location_type.pk}/?{urlencode({'name': self.location_type.name})}"
@@ -79,7 +79,7 @@ class BreadcrumbItemsTestCase(TestCase):
79
79
  label=lambda context: f"Hi, {context['user']}!",
80
80
  )
81
81
  context = Context({"user": "Frodo"})
82
- url, label = item.as_pair(context)
82
+ url, label = next(item.as_pair(context))
83
83
  self.assertEqual(url, "/")
84
84
  self.assertEqual(label, "Hi, Frodo!")
85
85
 
@@ -148,7 +148,7 @@ class BreadcrumbItemsTestCase(TestCase):
148
148
  item = ModelBreadcrumbItem(**test_case["kwargs"])
149
149
  context = Context({"object": self.location_type, "model_type": Device, "device_name": "abc"})
150
150
 
151
- url, label = item.as_pair(context)
151
+ url, label = next(item.as_pair(context))
152
152
 
153
153
  self.assertEqual(url, test_case["expected_url"])
154
154
  self.assertEqual(label, test_case["expected_label"])
@@ -157,7 +157,7 @@ class BreadcrumbItemsTestCase(TestCase):
157
157
  item = ModelBreadcrumbItem(model_key="object", label="custom LaBeL")
158
158
  context = Context({"object": self.location_type})
159
159
 
160
- _, label = item.as_pair(context)
160
+ _, label = next(item.as_pair(context))
161
161
  self.assertEqual(label, "custom LaBeL")
162
162
 
163
163
  def test_model_item_from_context(self):
@@ -165,7 +165,7 @@ class BreadcrumbItemsTestCase(TestCase):
165
165
  item = ModelBreadcrumbItem(model_key="object")
166
166
  context = Context({"object": self.location_type})
167
167
 
168
- url, label = item.as_pair(context)
168
+ url, label = next(item.as_pair(context))
169
169
 
170
170
  self.assertEqual(url, "/dcim/location-types/")
171
171
  self.assertEqual(label, "Location Types")
@@ -175,7 +175,7 @@ class BreadcrumbItemsTestCase(TestCase):
175
175
  item = InstanceBreadcrumbItem(instance_key="object")
176
176
  context = Context({"object": self.location_type})
177
177
 
178
- url, label = item.as_pair(context)
178
+ url, label = next(item.as_pair(context))
179
179
 
180
180
  self.assertEqual(url, f"/dcim/location-types/{self.location_type.pk}/")
181
181
  self.assertEqual(label, str(self.location_type))
@@ -192,7 +192,7 @@ class BreadcrumbItemsTestCase(TestCase):
192
192
 
193
193
  for item in items:
194
194
  with self.subTest():
195
- _, label = item.as_pair(context)
195
+ _, label = next(item.as_pair(context))
196
196
  self.assertEqual(label, "Custom Label")
197
197
 
198
198
  def test_no_reverse_match(self):
@@ -200,7 +200,7 @@ class BreadcrumbItemsTestCase(TestCase):
200
200
  item = ViewNameBreadcrumbItem(view_name="nonexistent")
201
201
  context = Context({})
202
202
 
203
- url, label = item.as_pair(context)
203
+ url, label = next(item.as_pair(context))
204
204
 
205
205
  self.assertEqual(url, "")
206
206
  self.assertEqual(label, "")
@@ -210,12 +210,12 @@ class BreadcrumbItemsTestCase(TestCase):
210
210
  context = Context({})
211
211
  item = InstanceBreadcrumbItem(instance_key="missing_key")
212
212
 
213
- url, label = item.as_pair(context)
213
+ url, label = next(item.as_pair(context))
214
214
  self.assertEqual(url, "")
215
215
  self.assertEqual(label, "")
216
216
 
217
217
  item = ModelBreadcrumbItem(model_key="missing_key")
218
- url, label = item.as_pair(context)
218
+ url, label = next(item.as_pair(context))
219
219
  self.assertEqual(url, "")
220
220
  self.assertEqual(label, "")
221
221
 
@@ -223,7 +223,7 @@ class BreadcrumbItemsTestCase(TestCase):
223
223
  context = Context({"object": LocationType.objects.create(name="custom name")})
224
224
  item = InstanceBreadcrumbItem()
225
225
 
226
- _, label = item.as_pair(context)
226
+ _, label = next(item.as_pair(context))
227
227
  self.assertEqual(label, "custom name")
228
228
 
229
229
  def test_instance_item_is_working_with_directly_passed_instance(self):
@@ -231,7 +231,7 @@ class BreadcrumbItemsTestCase(TestCase):
231
231
  instance = LocationType.objects.create(name="cUsToM CoUnTrY")
232
232
  item = InstanceBreadcrumbItem(instance=instance)
233
233
 
234
- url, label = item.as_pair(context)
234
+ url, label = next(item.as_pair(context))
235
235
  self.assertEqual(url, f"/dcim/location-types/{instance.pk}/")
236
236
  self.assertEqual(label, "cUsToM CoUnTrY")
237
237
 
@@ -244,17 +244,17 @@ class BreadcrumbItemsTestCase(TestCase):
244
244
  context = Context({"object": location, "status": status})
245
245
 
246
246
  item = InstanceBreadcrumbItem(instance=context_object_attr("location_type"))
247
- url, label = item.as_pair(context)
247
+ url, label = next(item.as_pair(context))
248
248
  self.assertEqual(url, f"/dcim/location-types/{location_type.pk}/")
249
- self.assertEqual(label, "Country → State")
249
+ self.assertEqual(label, "State")
250
250
 
251
251
  item = BaseBreadcrumbItem(label=context_object_attr("name", context_key="status"))
252
- url, label = item.as_pair(context)
252
+ url, label = next(item.as_pair(context))
253
253
  self.assertEqual(url, "")
254
254
  self.assertEqual(label, status.name)
255
255
 
256
256
  item = InstanceBreadcrumbItem(instance=context_object_attr("location_type.parent"))
257
- url, label = item.as_pair(context)
257
+ url, label = next(item.as_pair(context))
258
258
  self.assertEqual(url, f"/dcim/location-types/{parent_location_type.pk}/")
259
259
  self.assertEqual(label, "Country")
260
260
 
@@ -263,7 +263,7 @@ class BreadcrumbItemsTestCase(TestCase):
263
263
  context = Context({"object": LocationType.objects.create(name="custom name", parent=parent_location_type)})
264
264
  item = InstanceParentBreadcrumbItem()
265
265
 
266
- _, label = item.as_pair(context)
266
+ _, label = next(item.as_pair(context))
267
267
  self.assertEqual(label, "cUsToM CoUnTrY")
268
268
 
269
269
  def test_instance_parent_is_create_proper_url(self):
@@ -273,12 +273,12 @@ class BreadcrumbItemsTestCase(TestCase):
273
273
  context = Context({"object": location_type})
274
274
 
275
275
  item = InstanceParentBreadcrumbItem()
276
- url, label = item.as_pair(context)
276
+ url, label = next(item.as_pair(context))
277
277
  self.assertEqual(url, f"/dcim/location-types/?parent={parent_location_type.pk}")
278
278
  self.assertEqual(label, "Country")
279
279
 
280
280
  item = InstanceParentBreadcrumbItem(parent_query_param="location", parent_lookup_key="name")
281
- url, label = item.as_pair(context)
281
+ url, label = next(item.as_pair(context))
282
282
  self.assertEqual(url, "/dcim/location-types/?location=Country")
283
283
  self.assertEqual(label, "Country")
284
284
 
@@ -296,8 +296,8 @@ class BreadcrumbsTestCase(TestCase):
296
296
  breadcrumbs = Breadcrumbs()
297
297
 
298
298
  # Should have defaults for list and details
299
- self.assertEqual(len(breadcrumbs.items["list"]), 2)
300
- self.assertEqual(len(breadcrumbs.items["detail"]), 3)
299
+ self.assertEqual(len(breadcrumbs.items["list"]), 0)
300
+ self.assertEqual(len(breadcrumbs.items["detail"]), 2)
301
301
 
302
302
  # Verify adding items
303
303
  new_item = BaseBreadcrumbItem()
@@ -306,7 +306,7 @@ class BreadcrumbsTestCase(TestCase):
306
306
  self.assertEqual(len(breadcrumbs.items["list"]), 1)
307
307
  self.assertEqual(breadcrumbs.items["list"][0], new_item)
308
308
 
309
- self.assertEqual(len(breadcrumbs.items["detail"]), 2)
309
+ self.assertEqual(len(breadcrumbs.items["detail"]), 1)
310
310
  self.assertEqual(breadcrumbs.items["detail"][0], new_item)
311
311
 
312
312
  self.assertEqual(len(breadcrumbs.items["custom_action"]), 1)
@@ -325,7 +325,7 @@ class BreadcrumbsTestCase(TestCase):
325
325
 
326
326
  # Other defaults should still exist
327
327
  self.assertIn("detail", breadcrumbs.items)
328
- self.assertEqual(len(breadcrumbs.items["detail"]), 3)
328
+ self.assertEqual(len(breadcrumbs.items["detail"]), 2)
329
329
 
330
330
  def test_get_items_from_action_static_method(self):
331
331
  """Test the _get_items_from_action static method."""
@@ -355,7 +355,6 @@ class BreadcrumbsTestCase(TestCase):
355
355
  breadcrumbs = Breadcrumbs()
356
356
  expected_items = [
357
357
  ("/dcim/location-types/", "Location Types"),
358
- (f"/dcim/location-types/{self.location_type.pk}/", str(self.location_type)),
359
358
  ]
360
359
 
361
360
  # Test with an action that doesn't exist but detail=True
@@ -366,13 +365,13 @@ class BreadcrumbsTestCase(TestCase):
366
365
  items = breadcrumbs.get_breadcrumbs_items(context)
367
366
 
368
367
  # Should get 2 items from detail fallback
369
- self.assertEqual(len(items), 2)
368
+ self.assertEqual(len(items), 1)
370
369
  self.assertEqual(items, expected_items)
371
370
 
372
371
  def test_render_method(self):
373
372
  """Test the render method."""
374
373
  breadcrumbs = Breadcrumbs()
375
- context = Context({"view_action": "list", "model": Device})
374
+ context = Context({"detail": True, "model": Device})
376
375
 
377
376
  html = breadcrumbs.render(context)
378
377
 
@@ -1,6 +1,7 @@
1
1
  from django.test import override_settings, TestCase
2
2
 
3
3
  from nautobot.core import checks
4
+ from nautobot.dcim.choices import DeviceUniquenessChoices
4
5
 
5
6
 
6
7
  class CheckCoreSettingsTest(TestCase):
@@ -42,3 +43,30 @@ class CheckCoreSettingsTest(TestCase):
42
43
  def test_check_maintenance_mode(self):
43
44
  """Error if MAINTENANCE_MODE is set and yet SESSION_ENGINE is still storing sessions in the db."""
44
45
  self.assertEqual(checks.check_maintenance_mode(None), [checks.E005])
46
+
47
+ @override_settings(
48
+ DEVICE_NAME_AS_NATURAL_KEY=True,
49
+ )
50
+ def test_check_deprecated_device_name_as_natural_key(self):
51
+ """Warn if DEVICE_NAME_AS_NATURAL_KEY is defined in settings."""
52
+ self.assertEqual(
53
+ checks.check_deprecated_device_name_as_natural_key(None),
54
+ [checks.W006],
55
+ )
56
+
57
+ @override_settings(
58
+ DEVICE_UNIQUENESS="invalid_value",
59
+ )
60
+ def test_check_invalid_device_uniqueness_value(self):
61
+ """Warn if DEVICE_UNIQUENESS is set to an invalid value."""
62
+ self.assertEqual(
63
+ checks.check_valid_value_for_device_uniqueness(None),
64
+ [checks.W007],
65
+ )
66
+
67
+ @override_settings(
68
+ DEVICE_UNIQUENESS=DeviceUniquenessChoices.NAME,
69
+ )
70
+ def test_check_valid_device_uniqueness_value(self):
71
+ """No warning if DEVICE_UNIQUENESS is set to a valid value."""
72
+ self.assertEqual(checks.check_valid_value_for_device_uniqueness(None), [])
@@ -0,0 +1,40 @@
1
+ from nautobot.core.cli import migrate_deprecated_templates
2
+ from nautobot.core.testing import TestCase
3
+
4
+
5
+ class TestMigrateTemplates(TestCase):
6
+ def test_template_replacements(self):
7
+ """Verify that all old templates are replaced by a single new template."""
8
+ audit_dict = {}
9
+ for new_template, old_templates in migrate_deprecated_templates.TEMPLATE_REPLACEMENTS.items():
10
+ for old_template in old_templates:
11
+ self.assertNotIn(old_template, audit_dict)
12
+ audit_dict[old_template] = new_template
13
+
14
+ def test_replace_template_references_no_change(self):
15
+ content = """
16
+ {% extends "base.html" %}
17
+ {% block content %}
18
+ <h1>Hello, World!</h1>
19
+ {% endblock %}
20
+ """
21
+ replaced_content, was_updated = migrate_deprecated_templates.replace_template_references(content)
22
+ self.assertFalse(was_updated)
23
+ self.assertEqual(replaced_content, content)
24
+
25
+ def test_replace_template_references(self):
26
+ original_content = """
27
+ {% extends "generic/object_bulk_import.html" %}
28
+ {% block content %}
29
+ <h1>Hello, World!</h1>
30
+ {% endblock %}
31
+ """
32
+ new_content = """
33
+ {% extends "generic/object_bulk_create.html" %}
34
+ {% block content %}
35
+ <h1>Hello, World!</h1>
36
+ {% endblock %}
37
+ """
38
+ replaced_content, was_updated = migrate_deprecated_templates.replace_template_references(original_content)
39
+ self.assertTrue(was_updated)
40
+ self.assertEqual(replaced_content, new_content)
@@ -1,7 +1,7 @@
1
1
  """Test cases for nautobot.core.config module."""
2
2
 
3
3
  from constance.test import override_config
4
- from django.test import override_settings, TestCase
4
+ from django.test import override_settings, tag, TestCase
5
5
 
6
6
  from nautobot.apps import config as app_config
7
7
  from nautobot.core.utils import config
@@ -37,6 +37,7 @@ class GetSettingsOrConfigTestCase(TestCase):
37
37
  self.assertRaises(AttributeError, config.get_settings_or_config, "FAKE_SETTING")
38
38
 
39
39
 
40
+ @tag("example_app")
40
41
  class GetAppSettingsOrConfigTestCase(TestCase):
41
42
  """Test the get_app_settings_or_config() helper function."""
42
43
 
@@ -1,21 +1,26 @@
1
+ import inspect
2
+ import sys
1
3
  from unittest import mock
2
4
 
3
5
  from django import forms as django_forms
6
+ from django.apps import apps as django_apps
4
7
  from django.contrib.contenttypes.models import ContentType
5
8
  from django.http import QueryDict
6
- from django.test import TestCase
9
+ from django.test import tag
7
10
  from django.urls import reverse
11
+ from django_filters.filterset import FilterSet
8
12
  from netaddr import IPNetwork
9
13
 
10
14
  from nautobot.core import filters, forms, testing
11
15
  from nautobot.core.utils import requests
16
+ from nautobot.core.utils.filtering import get_filterset_parameter_form_field
12
17
  from nautobot.dcim import filters as dcim_filters, forms as dcim_forms, models as dcim_models
13
18
  from nautobot.dcim.tests import test_views
14
19
  from nautobot.extras import filters as extras_filters, models as extras_models
15
20
  from nautobot.ipam import forms as ipam_forms, models as ipam_models
16
21
 
17
22
 
18
- class ExpandIPAddress(TestCase):
23
+ class ExpandIPAddress(testing.TestCase):
19
24
  """
20
25
  Validate the operation of expand_ipaddress_pattern().
21
26
  """
@@ -198,7 +203,7 @@ class ExpandIPAddress(TestCase):
198
203
  sorted(forms.expand_ipaddress_pattern("1.2.3.[4,,5]/32", 4))
199
204
 
200
205
 
201
- class ExpandAlphanumeric(TestCase):
206
+ class ExpandAlphanumeric(testing.TestCase):
202
207
  """
203
208
  Validate the operation of expand_alphanumeric_pattern().
204
209
  """
@@ -332,7 +337,7 @@ class ExpandAlphanumeric(TestCase):
332
337
  sorted(forms.expand_alphanumeric_pattern("r[a,,b]a"))
333
338
 
334
339
 
335
- class AddFieldToFormClassTest(TestCase):
340
+ class AddFieldToFormClassTest(testing.TestCase):
336
341
  def test_field_added(self):
337
342
  """
338
343
  Test adding of a new field to an existing form.
@@ -361,7 +366,7 @@ class AddFieldToFormClassTest(TestCase):
361
366
  )
362
367
 
363
368
 
364
- class DynamicModelChoiceFieldTest(TestCase):
369
+ class DynamicModelChoiceFieldTest(testing.TestCase):
365
370
  """Tests for DynamicModelChoiceField."""
366
371
 
367
372
  def setUp(self):
@@ -397,7 +402,7 @@ class DynamicModelChoiceFieldTest(TestCase):
397
402
  self.assertEqual(self.field_with_to_field_name.prepare_value(address), address.address)
398
403
 
399
404
 
400
- class DynamicModelMultipleChoiceFieldTest(TestCase):
405
+ class DynamicModelMultipleChoiceFieldTest(testing.TestCase):
401
406
  """Tests for DynamicModelMultipleChoiceField."""
402
407
 
403
408
  def setUp(self):
@@ -434,7 +439,7 @@ class DynamicModelMultipleChoiceFieldTest(TestCase):
434
439
  )
435
440
 
436
441
 
437
- class MultiValueCharFieldTest(TestCase):
442
+ class MultiValueCharFieldTest(testing.TestCase):
438
443
  def setUp(self):
439
444
  self.filter = filters.MultiValueCharFilter()
440
445
  self.field = forms.MultiValueCharField()
@@ -467,7 +472,7 @@ class MultiValueCharFieldTest(TestCase):
467
472
  )
468
473
 
469
474
 
470
- class NumericArrayFieldTest(TestCase):
475
+ class NumericArrayFieldTest(testing.TestCase):
471
476
  def setUp(self):
472
477
  super().setUp()
473
478
  # We need to use a field with required=False so we can test empty/None inputs
@@ -497,7 +502,7 @@ class NumericArrayFieldTest(TestCase):
497
502
  self.field.clean(test)
498
503
 
499
504
 
500
- class AddressFieldMixinTest(TestCase):
505
+ class AddressFieldMixinTest(testing.TestCase):
501
506
  """Test cases for the AddressFieldMixin."""
502
507
 
503
508
  def setUp(self):
@@ -527,7 +532,7 @@ class AddressFieldMixinTest(TestCase):
527
532
  mock_init.assert_called_with(initial=self.initial, instance=self.ip)
528
533
 
529
534
 
530
- class PrefixFieldMixinTest(TestCase):
535
+ class PrefixFieldMixinTest(testing.TestCase):
531
536
  """Test cases for the PrefixFieldMixin."""
532
537
 
533
538
  def setUp(self):
@@ -576,7 +581,7 @@ class JSONFieldTest(testing.TestCase):
576
581
  self.assertEqual('"I am UTF-8! 😀"', forms.JSONField().prepare_value("I am UTF-8! 😀"))
577
582
 
578
583
 
579
- class MultiMatchModelMultipleChoiceFieldTest(TestCase):
584
+ class MultiMatchModelMultipleChoiceFieldTest(testing.TestCase):
580
585
  def test_clean(self):
581
586
  field = forms.MultiMatchModelMultipleChoiceField(
582
587
  queryset=ipam_models.VLANGroup.objects.all(), to_field_name="name"
@@ -604,20 +609,57 @@ class MultiMatchModelMultipleChoiceFieldTest(TestCase):
604
609
  field.clean(value)
605
610
 
606
611
 
607
- class WidgetsTest(TestCase):
612
+ class WidgetsTest(testing.TestCase):
608
613
  def test_api_select_add_query_param_with_utf8(self):
609
614
  widget = forms.APISelect()
610
615
  widget.add_query_param("utf8", "I am UTF-8! 😀")
611
616
  self.assertEqual('["I am UTF-8! 😀"]', widget.attrs["data-query-param-utf8"])
612
617
 
613
618
 
614
- class DynamicFilterFormTest(TestCase):
619
+ class DynamicFilterFormTest(testing.TestCase):
620
+ def test_get_filterset_parameter_form_field_all_filters(self):
621
+ """
622
+ Test every FilterSet to validate that Plural names are correctly mapped in get_filterset_parameter_form_field.
623
+ """
624
+ filterset_classes = set()
625
+ for app_config in django_apps.get_app_configs():
626
+ try:
627
+ filters_mod = sys.modules.get(f"{app_config.name}.filters")
628
+ if not filters_mod:
629
+ continue
630
+ for _name, obj in inspect.getmembers(filters_mod):
631
+ if (
632
+ inspect.isclass(obj) # Check if obj is a class
633
+ and issubclass(obj, FilterSet) # Check if obj is a subclass of FilterSet
634
+ and obj is not FilterSet # Exclude the base FilterSet class itself
635
+ and getattr(getattr(obj, "_meta", None), "model", None)
636
+ is not None # Ensure the FilterSet has a model defined
637
+ ):
638
+ filterset_classes.add(obj)
639
+ except Exception as e:
640
+ # This test might start failing if an app's filters.py gets a design change.
641
+ self.fail(f"Error processing app '{app_config.name}': {e}")
642
+ for filterset_class in filterset_classes:
643
+ filterset = filterset_class()
644
+ model = filterset._meta.model
645
+ for filter_name in filterset.filters.keys():
646
+ try:
647
+ field = get_filterset_parameter_form_field(model, filter_name, filterset=filterset)
648
+ self.assertIsNotNone(field, "Field was unexpectedly None")
649
+ except KeyError as e:
650
+ self.fail(
651
+ f"A filter failed to operate due to mismatched plural name:"
652
+ f" Check MODEL_VERBOSE_NAME_PLURAL_TO_FEATURE_NAME_MAPPING:"
653
+ f" FilterClass: {filterset_class.__name__} name: {filter_name}: {e}"
654
+ )
655
+
615
656
  # TODO(timizuo): investigate why test fails on CI
616
657
  # def test_dynamic_filter_form_with_missing_attr(self):
617
658
  # with self.assertRaises(AttributeError) as err:
618
659
  # DynamicFilterForm()
619
660
  # self.assertEqual("'DynamicFilterForm' object requires `filterset_class` attribute", str(err.exception))
620
661
 
662
+ @tag("example_app")
621
663
  def test_dynamic_filter_form(self):
622
664
  form = forms.DynamicFilterForm(filterset=extras_filters.StatusFilterSet())
623
665
  location_form = forms.DynamicFilterForm(filterset=dcim_filters.LocationFilterSet())