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
@@ -32,7 +32,9 @@ from nautobot.core.ui.object_detail import (
32
32
  SectionChoices,
33
33
  )
34
34
  from nautobot.dcim.models import Device, DeviceRedundancyGroup, Location
35
+ from nautobot.dcim.tables import DeviceModuleInterfaceTable
35
36
  from nautobot.dcim.tables.devices import DeviceTable
37
+ from nautobot.dcim.views import DeviceUIViewSet
36
38
  from nautobot.ipam.models import Prefix
37
39
 
38
40
 
@@ -355,9 +357,80 @@ class EChartsBaseTests(TestCase):
355
357
  self.assertEqual(config["series"][0]["name"], "S1")
356
358
  self.assertEqual(config["series"][1]["name"], "S2")
357
359
 
358
- def test_get_config_with_callable_data(self):
359
- chart = EChartsBase(data=lambda: self.data_normalized)
360
- config = chart.get_config()
360
+ def test_get_config_combined_charts_with_complex_data(self):
361
+ chart2 = EChartsBase(
362
+ chart_type=EChartsTypeChoices.LINE,
363
+ data={
364
+ "Compliant": {"aaa1": 5, "dns1": 12, "ntp1": 8},
365
+ "Non Compliant": {"aaa1": 10, "dns1": 20, "ntp1": 15},
366
+ },
367
+ )
368
+ chart1 = EChartsBase(
369
+ chart_type=EChartsTypeChoices.BAR,
370
+ data={
371
+ "Compliant": {"aaa": 5, "dns": 12, "ntp": 8},
372
+ "Non Compliant": {"aaa": 10, "dns": 20, "ntp": 15},
373
+ },
374
+ combined_with=chart2,
375
+ )
376
+ config = chart1.get_config()
377
+ self.assertEqual(len(config["series"]), 4)
378
+
379
+ self.assertEqual(config["series"][0], {"name": "Compliant", "type": "bar", "data": [5, 12, 8]})
380
+ self.assertEqual(config["series"][1], {"name": "Non Compliant", "type": "bar", "data": [10, 20, 15]})
381
+ self.assertEqual(
382
+ config["series"][2],
383
+ {"name": "Compliant", "type": "line", "data": [5, 12, 8], "smooth": False, "lineStyle": {}},
384
+ )
385
+ self.assertEqual(
386
+ config["series"][3],
387
+ {"name": "Non Compliant", "type": "line", "data": [10, 20, 15], "smooth": False, "lineStyle": {}},
388
+ )
389
+
390
+ def test_get_config_with_context_callable_and_combined_chart(self):
391
+ def main_data(ctx):
392
+ return {
393
+ "Compliant": {"aaa1": ctx.get("aaa1_count", 0), "dns1": 13, "ntp1": 8},
394
+ "Non Compliant": {"aaa1": 10, "dns1": 20, "ntp1": 15},
395
+ }
396
+
397
+ def combined_data(ctx):
398
+ return {
399
+ "Compliant": {"aaa": 5, "dns": ctx.get("dns_count", 0), "ntp1": 8},
400
+ "Non Compliant": {"aaa": 10, "dns": 20, "ntp1": 15},
401
+ }
402
+
403
+ ctx = Context({"aaa1_count": 5, "dns_count": 12})
404
+ combined_chart = EChartsBase(chart_type=EChartsTypeChoices.LINE, data=combined_data)
405
+ main_chart = EChartsBase(
406
+ chart_type=EChartsTypeChoices.BAR,
407
+ data=main_data,
408
+ combined_with=combined_chart,
409
+ )
410
+ config = main_chart.get_config(context=ctx)
411
+ self.assertEqual(len(config["series"]), 4)
412
+
413
+ self.assertEqual(config["series"][0]["data"][0], 5) # aaa1_count
414
+ self.assertEqual(config["series"][2]["data"][1], 12) # dns_count
415
+
416
+ def test_get_config_with_context_callable(self):
417
+ def dynamic_data(ctx):
418
+ return {"data": {"Devices": ctx.get("device_count", 0)}}
419
+
420
+ ctx = Context({"device_count": 42})
421
+ chart = EChartsBase(chart_type=EChartsTypeChoices.PIE, data=dynamic_data)
422
+ config = chart.get_config(context=ctx)
423
+ # check that after getting config data doesn't change it's still dynami_data function
424
+ self.assertEqual(chart.data, dynamic_data)
425
+ self.assertEqual(config["series"][0]["data"][0]["value"], 42)
426
+ ctx = Context({"device_count": 45})
427
+ config = chart.get_config(context=ctx)
428
+ self.assertEqual(config["series"][0]["data"][0]["value"], 45)
429
+
430
+ def test_get_config_with_context_ignored_when_data_is_not_callable(self):
431
+ ctx = Context({"some": "value"})
432
+ chart = EChartsBase(data=self.data_normalized)
433
+ config = chart.get_config(context=ctx)
361
434
  self.assertEqual(config["series"][0]["data"], [1, 2])
362
435
 
363
436
 
@@ -446,7 +519,7 @@ class ObjectDetailContentExtraTabsTest(TestCase):
446
519
  self.factory = RequestFactory()
447
520
  self.request = self.factory.get("/")
448
521
  self.request.user = self.user
449
- self.default_tabs_id = ["main", "advanced", "contacts", "dynamic_groups", "object_metadata"]
522
+ self.default_tabs_id = ["main", "advanced", "contacts", "dynamic_groups", "object_metadata", "data_compliance"]
450
523
 
451
524
  def test_default_extra_tabs_exist(self):
452
525
  """
@@ -492,6 +565,52 @@ class ObjectDetailContentExtraTabsTest(TestCase):
492
565
  self.default_tabs_id.append("services")
493
566
  self.assertListEqual(tab_ids, self.default_tabs_id)
494
567
 
568
+ def test_tab_id_url_as_action(self):
569
+ """
570
+ Test that when you create a panel with a tab_id that matches a viewset action,
571
+ the return_url is constructed correctly.
572
+ """
573
+ self.add_permissions("dcim.add_interface", "dcim.change_interface")
574
+ device_info = Device.objects.first()
575
+
576
+ panel = DeviceUIViewSet.DeviceInterfacesTablePanel(
577
+ weight=100,
578
+ section=SectionChoices.FULL_WIDTH,
579
+ table_title="Interfaces",
580
+ table_class=DeviceModuleInterfaceTable,
581
+ table_attribute="vc_interfaces",
582
+ related_field_name="device",
583
+ tab_id="interfaces",
584
+ )
585
+ context = {"request": self.request, "object": device_info}
586
+ panel_context = panel.get_extra_context(context)
587
+
588
+ return_url = f"/dcim/devices/{device_info.pk}/interfaces/"
589
+ self.assertTrue(panel_context["body_content_table_add_url"].endswith(return_url))
590
+
591
+ def test_tab_id_url_as_param(self):
592
+ """
593
+ Test that when you create a panel with a tab_id that does NOT matches a viewset action,
594
+ the return_url is constructed correctly.
595
+ """
596
+ self.add_permissions("dcim.add_interface", "dcim.change_interface")
597
+ device_info = Device.objects.first()
598
+
599
+ panel = DeviceUIViewSet.DeviceInterfacesTablePanel(
600
+ weight=100,
601
+ section=SectionChoices.FULL_WIDTH,
602
+ table_title="Interfaces",
603
+ table_class=DeviceModuleInterfaceTable,
604
+ table_attribute="vc_interfaces",
605
+ related_field_name="device",
606
+ tab_id="interfaces-not-exist",
607
+ )
608
+ context = {"request": self.request, "object": device_info}
609
+ panel_context = panel.get_extra_context(context)
610
+
611
+ return_url = f"&return_url=/dcim/devices/{device_info.pk}/?tab=interfaces-not-exist"
612
+ self.assertTrue(panel_context["body_content_table_add_url"].endswith(return_url))
613
+
495
614
  def test_extra_tab_panel_context(self):
496
615
  """
497
616
  Confirming that extra tab panels produce the correct context,
@@ -9,7 +9,7 @@ from django.contrib.contenttypes.models import ContentType
9
9
  from django.core.exceptions import ValidationError
10
10
  from django.db.models import Q
11
11
  from django.http import QueryDict
12
- from django.test import override_settings
12
+ from django.test import override_settings, tag
13
13
 
14
14
  from nautobot.circuits import models as circuits_models
15
15
  from nautobot.core import exceptions, forms, settings_funcs
@@ -20,9 +20,15 @@ from nautobot.core.testing import TestCase
20
20
  from nautobot.core.utils import data as data_utils, filtering, lookup, querysets, requests
21
21
  from nautobot.core.utils.cache import construct_cache_key
22
22
  from nautobot.core.utils.migrations import update_object_change_ct_for_replaced_models
23
- from nautobot.core.utils.module_loading import check_name_safe_to_import_privately
23
+ from nautobot.core.utils.module_loading import check_name_safe_to_import_privately, import_string_optional
24
24
  from nautobot.data_validation import models as data_validation_models
25
- from nautobot.dcim import filters as dcim_filters, forms as dcim_forms, models as dcim_models, tables
25
+ from nautobot.dcim import (
26
+ filters as dcim_filters,
27
+ forms as dcim_forms,
28
+ models as dcim_models,
29
+ tables,
30
+ views as dcim_views,
31
+ )
26
32
  from nautobot.extras import models as extras_models, utils as extras_utils
27
33
  from nautobot.extras.choices import ObjectChangeActionChoices, RelationshipTypeChoices
28
34
  from nautobot.extras.filters import StatusFilterSet
@@ -30,8 +36,6 @@ from nautobot.extras.forms import StatusForm
30
36
  from nautobot.extras.models import ObjectChange
31
37
  from nautobot.ipam import models as ipam_models
32
38
 
33
- from example_app.models import ExampleModel
34
-
35
39
 
36
40
  class ConstructCacheKeyTest(TestCase):
37
41
  """
@@ -331,6 +335,26 @@ class GetFooForModelTest(TestCase):
331
335
  instance = extras_models.GraphQLQuery.objects.create(name="FizzBuzz", query="{devices { name }}")
332
336
  self.assertIsNone(lookup.get_user_from_instance(instance))
333
337
 
338
+ def test_get_breadcrumbs_for_model(self):
339
+ breadcrumbs = lookup.get_breadcrumbs_for_model(dcim_models.Device)
340
+ self.assertEqual(breadcrumbs.items, dcim_views.DeviceUIViewSet.get_breadcrumbs(dcim_models.Device).items)
341
+ breadcrumbs = lookup.get_breadcrumbs_for_model(dcim_models.Device, view_type="")
342
+ self.assertEqual(
343
+ breadcrumbs.items, dcim_views.DeviceUIViewSet.get_breadcrumbs(dcim_models.Device, view_type="").items
344
+ )
345
+
346
+ def test_get_detail_view_components_context_for_model(self):
347
+ context = lookup.get_detail_view_components_context_for_model(dcim_models.Device)
348
+ self.assertEqual(
349
+ context["breadcrumbs"].items, lookup.get_breadcrumbs_for_model(dcim_models.Device, view_type="").items
350
+ )
351
+ self.assertEqual(
352
+ context["object_detail_content"], lookup.get_object_detail_content_for_model(dcim_models.Device)
353
+ )
354
+ self.assertEqual(
355
+ context["view_titles"].titles, lookup.get_view_titles_for_model(dcim_models.Device, view_type="").titles
356
+ )
357
+
334
358
  def test_get_filterset_for_model(self):
335
359
  """
336
360
  Test that `get_filterset_for_model` returns the right FilterSet for various inputs.
@@ -353,6 +377,12 @@ class GetFooForModelTest(TestCase):
353
377
  self.assertEqual(lookup.get_form_for_model("dcim.location"), dcim_forms.LocationForm)
354
378
  self.assertEqual(lookup.get_form_for_model(dcim_models.Location), dcim_forms.LocationForm)
355
379
 
380
+ def test_get_object_detail_content_for_model(self):
381
+ self.assertEqual(
382
+ lookup.get_object_detail_content_for_model(dcim_models.Device),
383
+ dcim_views.DeviceUIViewSet.object_detail_content,
384
+ )
385
+
356
386
  def test_get_related_field_for_models(self):
357
387
  """
358
388
  Test that `get_related_field_for_models` returns the appropriate field for various inputs.
@@ -372,10 +402,13 @@ class GetFooForModelTest(TestCase):
372
402
  # both primary_ip4 and primary_ip6 are candidates
373
403
  lookup.get_related_field_for_models(dcim_models.Device, ipam_models.IPAddress)
374
404
 
405
+ @tag("example_app")
375
406
  def test_get_route_for_model(self):
376
407
  """
377
408
  Test that `get_route_for_model` returns the appropriate URL route name for various inputs.
378
409
  """
410
+ from example_app.models import ExampleModel
411
+
379
412
  # UI
380
413
  self.assertEqual(lookup.get_route_for_model("dcim.device", "list"), "dcim:device_list")
381
414
  self.assertEqual(lookup.get_route_for_model(dcim_models.Device, "list"), "dcim:device_list")
@@ -423,10 +456,13 @@ class GetFooForModelTest(TestCase):
423
456
  self.assertEqual(lookup.get_model_from_name("dcim.device"), dcim_models.Device)
424
457
  self.assertEqual(lookup.get_model_from_name("dcim.location"), dcim_models.Location)
425
458
 
459
+ @tag("example_app")
426
460
  def test_get_model_for_view_name(self):
427
461
  """
428
462
  Test that `get_model_for_view_name` returns the appropriate Model, if the colon separated view name provided.
429
463
  """
464
+ from example_app.models import ExampleModel
465
+
430
466
  with self.subTest("Test core UI view."):
431
467
  self.assertEqual(lookup.get_model_for_view_name("dcim:device_list"), dcim_models.Device)
432
468
  self.assertEqual(lookup.get_model_for_view_name("dcim:device"), dcim_models.Device)
@@ -459,6 +495,14 @@ class GetFooForModelTest(TestCase):
459
495
  # Testing unconventional table name
460
496
  self.assertEqual(lookup.get_table_class_string_from_view_name("ipam:prefix_list"), "PrefixDetailTable")
461
497
 
498
+ def test_get_view_titles_for_model(self):
499
+ view_titles = lookup.get_view_titles_for_model(dcim_models.Device)
500
+ self.assertEqual(view_titles.titles, dcim_views.DeviceUIViewSet.get_view_titles(dcim_models.Device).titles)
501
+ view_titles = lookup.get_view_titles_for_model(dcim_models.Device, view_type="")
502
+ self.assertEqual(
503
+ view_titles.titles, dcim_views.DeviceUIViewSet.get_view_titles(dcim_models.Device, view_type="").titles
504
+ )
505
+
462
506
 
463
507
  class IsTaggableTest(TestCase):
464
508
  def test_is_taggable_true(self):
@@ -1128,6 +1172,29 @@ class TestModuleLoadingUtils(TestCase):
1128
1172
  self.assertFalse(permitted)
1129
1173
  self.assertIsInstance(reason, str)
1130
1174
 
1175
+ def test_import_string_optional(self):
1176
+ with self.subTest("Nonexistent module should return None"):
1177
+ self.assertIsNone(import_string_optional("no_such_module.no_such_attribute"))
1178
+ self.assertIsNone(import_string_optional("no_such_module.no_such_submodule.no_such_attribute"))
1179
+ self.assertIsNone(import_string_optional("nautobot.no_such_submodule.no_such_attribute"))
1180
+ self.assertIsNone(import_string_optional("nautobot.core.no_such_submodule.no_such_attribute"))
1181
+
1182
+ with self.subTest("Existing module but nonexistent attribute should return None"):
1183
+ self.assertIsNone(import_string_optional("nautobot.core.no_such_attribute"))
1184
+ self.assertIsNone(import_string_optional("nautobot.core.no_such_attribute"))
1185
+ self.assertIsNone(import_string_optional("sys.no_such_attribute"))
1186
+
1187
+ with self.subTest("Other import errors should propagate upward still"):
1188
+ with self.assertRaises(ImportError):
1189
+ import_string_optional("nautobot.extras.test_jobs.invalid_import.MyJob")
1190
+ with self.assertRaises(ImportError):
1191
+ import_string_optional("nautobot.extras.test_jobs.missing_import.MyJob")
1192
+
1193
+ with self.subTest("Successful imports should succeed"):
1194
+ self.assertEqual(
1195
+ import_string_optional("nautobot.core.tests.test_utils.TestModuleLoadingUtils"), self.__class__
1196
+ )
1197
+
1131
1198
 
1132
1199
  class TestQuerySetUtils(TestCase):
1133
1200
  def test_maybe_select_related(self):
@@ -1,12 +1,12 @@
1
1
  import json
2
2
  import os
3
+ from pathlib import Path
3
4
  import re
4
5
  import tempfile
5
- from unittest import mock, skipIf
6
+ from unittest import mock
6
7
  import urllib.parse
7
8
 
8
9
  from django.apps import apps
9
- from django.conf import settings
10
10
  from django.contrib.contenttypes.models import ContentType
11
11
  from django.core.cache import cache
12
12
  from django.core.files.uploadedfile import SimpleUploadedFile
@@ -110,38 +110,25 @@ class HomeViewTestCase(TestCase):
110
110
 
111
111
  # Search bar in header
112
112
  header_search_bar_pattern = re.compile(
113
- '<header.*<form action="/search/" class="col text-center" method="get" id="navbar_search" role="search">.*</form>.*</header>'
113
+ '<header.*<form action="/search/" class="col-4 text-center" id="header_search" method="get" role="search">.*</form>.*</header>'
114
114
  )
115
115
  header_search_bar_result = header_search_bar_pattern.search(
116
116
  response.content.decode(response.charset).replace("\n", "")
117
117
  )
118
118
 
119
- # Global search bar in body/container-fluid wrapper
120
- body_search_bar_pattern = re.compile(
121
- '<div class="container-fluid wrapper" id="main-content">.*<form action="/search/" method="get" class="form-inline">.*</form>.*</div>',
122
- re.DOTALL,
123
- )
124
-
125
- body_search_bar_result = body_search_bar_pattern.search(
126
- response.content.decode(response.charset).replace("\n", "")
127
- )
128
-
129
- return header_search_bar_result, body_search_bar_result
119
+ return header_search_bar_result
130
120
 
131
121
  def test_search_bar_not_visible_if_user_not_authenticated(self):
132
122
  self.client.logout()
133
123
 
134
- header_search_bar_result, body_search_bar_result = self.make_request()
124
+ header_search_bar_result = self.make_request()
135
125
 
136
126
  self.assertIsNone(header_search_bar_result)
137
- self.assertIsNone(body_search_bar_result)
138
127
 
139
- @tag("fix_in_v3")
140
128
  def test_search_bar_visible_if_user_authenticated(self):
141
- header_search_bar_result, body_search_bar_result = self.make_request()
129
+ header_search_bar_result = self.make_request()
142
130
 
143
131
  self.assertIsNotNone(header_search_bar_result)
144
- self.assertIsNotNone(body_search_bar_result)
145
132
 
146
133
  @override_settings(VERSION="1.2.3")
147
134
  def test_footer_version_visible_authenticated_users_only(self):
@@ -190,6 +177,132 @@ class HomeViewTestCase(TestCase):
190
177
  self.assertNotIn("Welcome to Nautobot!", response.content.decode(response.charset))
191
178
 
192
179
 
180
+ class AppDocsViewTestCase(TestCase):
181
+ def setUp(self):
182
+ super().setUp()
183
+ self.test_app_label = "test_app"
184
+ self.test_base_url = "test-app"
185
+
186
+ # Create temp docs dir
187
+ # I use tearDown to clean up, so this is save
188
+ self.temp_dir = tempfile.TemporaryDirectory() # pylint: disable=consider-using-with
189
+ self.docs_path = Path(self.temp_dir.name) / "docs"
190
+ self.docs_path.mkdir(parents=True)
191
+ (self.docs_path / "index.html").write_text("<html>Test Index</html>")
192
+ (self.docs_path / "css/style.css").parent.mkdir(parents=True, exist_ok=True)
193
+ (self.docs_path / "css/style.css").write_text("body { background: #fff; }")
194
+
195
+ def tearDown(self):
196
+ self.temp_dir.cleanup()
197
+ super().tearDown()
198
+
199
+ def test_docs_index_redirect(self):
200
+ """Ensure /docs/<base_url>/ redirects to /docs/<base_url>/index.html."""
201
+ url = reverse("docs_index_redirect", kwargs={"app_base_url": self.test_base_url})
202
+ response = self.client.get(url, follow=False)
203
+ self.assertEqual(response.status_code, 302)
204
+ self.assertEqual(response["Location"], f"/docs/{self.test_base_url}/index.html")
205
+
206
+ def test_docs_index_redirect_if_not_logged_in(self):
207
+ self.client.logout()
208
+ url = reverse("docs_index_redirect", kwargs={"app_base_url": self.test_base_url})
209
+ response = self.client.get(url, follow=False)
210
+
211
+ # First, the redirect to /docs/<base_url>/index.html
212
+ self.assertEqual(response.status_code, 302)
213
+ redirect_url = f"/docs/{self.test_base_url}/index.html"
214
+ self.assertEqual(response["Location"], redirect_url)
215
+
216
+ # Follow the redirect to AppDocsView, which should require login
217
+ response = self.client.get(redirect_url)
218
+ self.assertRedirects(
219
+ response,
220
+ expected_url=f"{reverse('login')}?next={redirect_url}",
221
+ status_code=302,
222
+ target_status_code=200,
223
+ )
224
+
225
+ def test_docs_file_redirect_if_not_logged_in(self):
226
+ self.client.logout()
227
+ url = reverse("docs_file", kwargs={"app_base_url": self.test_base_url, "path": "css/style.css"})
228
+ response = self.client.get(url)
229
+ # LoginRequiredMixin redirects to /accounts/login/
230
+ self.assertRedirects(
231
+ response,
232
+ expected_url=f"{reverse('login')}?next={url}",
233
+ status_code=302,
234
+ target_status_code=200,
235
+ )
236
+
237
+ @mock.patch.dict("nautobot.core.views.BASE_URL_TO_APP_LABEL", {"test-app": "test_app"})
238
+ @mock.patch("nautobot.core.views.resources.files")
239
+ def test_access_denied_path_traversal_attempts(self, mock_resources_files):
240
+ """Ensure ../ or similar traversal patterns are rejected."""
241
+ mock_resources_files.return_value = Path(self.temp_dir.name)
242
+
243
+ malicious_paths = [
244
+ "../settings.py",
245
+ "../../etc/passwd",
246
+ "docs/../../secret.txt",
247
+ ]
248
+
249
+ for path in malicious_paths:
250
+ with self.subTest(path=path):
251
+ url = reverse("docs_file", kwargs={"app_base_url": self.test_base_url, "path": path})
252
+ response = self.client.get(url)
253
+ self.assertEqual(response.status_code, 403)
254
+
255
+ @mock.patch.dict("nautobot.core.views.BASE_URL_TO_APP_LABEL", {"test-app": "test_app"})
256
+ @mock.patch("nautobot.core.views.resources.files")
257
+ def test_serve_index_html_logged_in(self, mock_resources_files):
258
+ mock_resources_files.return_value = Path(self.temp_dir.name)
259
+ url = reverse("docs_index_redirect", kwargs={"app_base_url": self.test_base_url, "path": "index.html"})
260
+ response = self.client.get(url, follow=True)
261
+ self.assertEqual(response.status_code, 200)
262
+ self.assertContains(response, "Test Index")
263
+ self.assertEqual(response["Content-Type"], "text/html")
264
+
265
+ @mock.patch.dict("nautobot.core.views.BASE_URL_TO_APP_LABEL", {"test-app": "test_app"})
266
+ @mock.patch("nautobot.core.views.resources.files")
267
+ def test_serve_css_logged_in(self, mock_resources_files):
268
+ mock_resources_files.return_value = Path(self.temp_dir.name)
269
+ url = reverse("docs_file", kwargs={"app_base_url": self.test_base_url, "path": "css/style.css"})
270
+ response = self.client.get(url)
271
+ self.assertEqual(response.status_code, 200)
272
+ self.assertContains(response, "background: #fff;")
273
+ self.assertEqual(response["Content-Type"], "text/css")
274
+
275
+ @mock.patch.dict("nautobot.core.views.BASE_URL_TO_APP_LABEL", {"test-app": "test_app"})
276
+ @mock.patch("nautobot.core.views.resources.files")
277
+ def test_docs_index_nonexistent_app(self, mock_resources_files):
278
+ mock_resources_files.return_value = Path(self.temp_dir.name)
279
+ url = reverse("docs_index_redirect", kwargs={"app_base_url": "nonexistent-app"})
280
+ response = self.client.get(url, follow=True)
281
+ self.assertEqual(response.status_code, 404)
282
+ self.assertJSONEqual(response.content, {"detail": "Unknown base_url 'nonexistent-app'."})
283
+
284
+ @mock.patch.dict("nautobot.core.views.BASE_URL_TO_APP_LABEL", {"test-app": "test_app"})
285
+ @mock.patch("nautobot.core.views.resources.files")
286
+ def test_docs_file_nonexistent_app(self, mock_resources_files):
287
+ mock_resources_files.return_value = Path(self.temp_dir.name)
288
+ url = reverse("docs_file", kwargs={"app_base_url": "nonexistent-app", "path": "css/style.css"})
289
+ response = self.client.get(url)
290
+ self.assertEqual(response.status_code, 404)
291
+ self.assertJSONEqual(response.content, {"detail": "Unknown base_url 'nonexistent-app'."})
292
+
293
+ @mock.patch.dict("nautobot.core.views.BASE_URL_TO_APP_LABEL", {"test-app": "test_app"})
294
+ @mock.patch("nautobot.core.views.resources.files")
295
+ def test_nonexistent_file(self, mock_resources_files):
296
+ mock_resources_files.return_value = Path(self.temp_dir.name)
297
+ test_cases = ["/../missing.html", "//../missing.html", "missing.html", "missing_dir/missing.html"]
298
+ for path in test_cases:
299
+ with self.subTest(path=path):
300
+ url = reverse("docs_file", kwargs={"app_base_url": self.test_base_url, "path": path})
301
+ response = self.client.get(url)
302
+ self.assertEqual(response.status_code, 404)
303
+ self.assertIn("File", response.json()["detail"])
304
+
305
+
193
306
  class MediaViewTestCase(TestCase):
194
307
  def test_media_unauthenticated(self):
195
308
  """
@@ -270,29 +383,45 @@ class SearchFieldsTestCase(TestCase):
270
383
  # SearchForm will redirect the user to the login Page
271
384
  self.assertEqual(response.status_code, 302)
272
385
 
273
- @tag("fix_in_v3")
274
- def test_global_and_model_search_bar(self):
386
+ def test_global_search_bar_scoped_to_model(self):
275
387
  self.add_permissions("dcim.view_location", "dcim.view_device")
276
388
 
277
389
  # Assert model search bar present in list UI
278
390
  response = self.client.get(reverse("dcim:location_list"))
279
391
  self.assertBodyContains(
280
392
  response,
281
- '<input type="text" name="q" class="form-control" required placeholder="Search Locations" id="id_q">',
393
+ '<input aria-placeholder="Press Ctrl+K to search" class="form-control nb-text-transparent" name="q" type="search" value="">',
394
+ html=True,
395
+ )
396
+ self.assertBodyContains(
397
+ response,
398
+ """
399
+ <span class="badge border" data-nb-link="/dcim/locations/"><!--
400
+ -->in: Locations<!--
401
+ --><button tabindex="-1" type="button">
402
+ <span aria-hidden="true" class="mdi mdi-close"></span>
403
+ <span class="visually-hidden">Remove</span>
404
+ </button>
405
+ """,
282
406
  html=True,
283
407
  )
284
408
 
285
409
  response = self.client.get(reverse("dcim:device_list"))
286
410
  self.assertBodyContains(
287
411
  response,
288
- '<input type="text" name="q" class="form-control" required placeholder="Search Devices" id="id_q">',
412
+ '<input aria-placeholder="Press Ctrl+K to search" class="form-control nb-text-transparent" name="q" type="search" value="">',
289
413
  html=True,
290
414
  )
291
-
292
- # Assert global search bar present in UI
293
- self.assertContains( # not using assertBodyContains because this is in the nav
415
+ self.assertBodyContains(
294
416
  response,
295
- '<input type="text" name="q" class="form-control" placeholder="Search Nautobot">',
417
+ """
418
+ <span class="badge border" data-nb-link="/dcim/devices/"><!--
419
+ -->in: Devices<!--
420
+ --><button tabindex="-1" type="button">
421
+ <span aria-hidden="true" class="mdi mdi-close"></span>
422
+ <span class="visually-hidden">Remove</span>
423
+ </button>
424
+ """,
296
425
  html=True,
297
426
  )
298
427
 
@@ -560,6 +689,7 @@ class MetricsViewTestCase(TestCase):
560
689
  page_content = response.content.decode(response.charset)
561
690
  return text_string_to_metric_families(page_content)
562
691
 
692
+ @tag("example_app")
563
693
  def test_metrics_extensibility(self):
564
694
  """Assert that the example metric from the Example App shows up _exactly_ when the app is enabled."""
565
695
  test_metric_name = "nautobot_example_metric_count"
@@ -582,6 +712,7 @@ class MetricsViewTestCase(TestCase):
582
712
  self.query_and_parse_metrics()
583
713
  self.assertTrue(mock_generate_latest_with_cache.call_count == 0)
584
714
 
715
+ @tag("example_app")
585
716
  @override_settings(METRICS_EXPERIMENTAL_CACHING_DURATION=30)
586
717
  def test_enabled_metrics_cache_enabled(self):
587
718
  """Assert that multiple calls to metrics with caching returns expected response."""
@@ -762,10 +893,7 @@ class SilkUIAccessTestCase(TestCase):
762
893
 
763
894
 
764
895
  class ExampleViewWithCustomPermissionsTest(TestCase):
765
- @skipIf(
766
- "example_app" not in settings.PLUGINS,
767
- "example_app not in settings.PLUGINS",
768
- )
896
+ @tag("example_app")
769
897
  @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
770
898
  def test_permission_classes_attribute_is_enforced(self):
771
899
  """