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
@@ -16,12 +16,13 @@ from nautobot.core.jobs import ExportObjectList
16
16
  from nautobot.core.jobs.cleanup import CleanupTypes
17
17
  from nautobot.core.testing import create_job_result_and_run_job, TransactionTestCase
18
18
  from nautobot.core.testing.context import load_event_broker_override_settings
19
- from nautobot.dcim.models import Device, DeviceType, Location, LocationType, Manufacturer
20
- from nautobot.extras.choices import JobResultStatusChoices, LogLevelChoices
19
+ from nautobot.dcim.models import Device, DeviceType, FrontPortTemplate, Location, LocationType, Manufacturer
20
+ from nautobot.extras.choices import DynamicGroupTypeChoices, JobResultStatusChoices, LogLevelChoices
21
21
  from nautobot.extras.factory import JobResultFactory, ObjectChangeFactory
22
22
  from nautobot.extras.models import (
23
23
  Contact,
24
24
  ContactAssociation,
25
+ DynamicGroup,
25
26
  ExportTemplate,
26
27
  FileProxy,
27
28
  JobLogEntry,
@@ -34,7 +35,7 @@ from nautobot.extras.models import (
34
35
  )
35
36
  from nautobot.extras.models.metadata import ObjectMetadata
36
37
  from nautobot.ipam.models import IPAddress, Namespace, Prefix
37
- from nautobot.users.models import ObjectPermission
38
+ from nautobot.users.models import ObjectPermission, User
38
39
 
39
40
 
40
41
  class ExportObjectListTest(TransactionTestCase):
@@ -1248,3 +1249,143 @@ class BulkDeleteTestCase(TransactionTestCase):
1248
1249
  saved_view_id=None,
1249
1250
  )
1250
1251
  self._common_no_error_test_assertion(Role, job_result, name__istartswith="Example Status")
1252
+
1253
+
1254
+ class RefreshDynamicGroupCacheJobButtonReceiverTestCase(TransactionTestCase):
1255
+ job_module = "nautobot.core.jobs.groups"
1256
+ job_name = "RefreshDynamicGroupCacheJobButtonReceiver"
1257
+
1258
+ def test_successful_cache_refresh(self):
1259
+ LocationType.objects.create(name="DG Test LT 1")
1260
+ LocationType.objects.create(name="DG Test LT 2")
1261
+ LocationType.objects.create(name="DG Test LT 3")
1262
+ dg = DynamicGroup(
1263
+ name="Location Types",
1264
+ content_type=ContentType.objects.get_for_model(LocationType),
1265
+ group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER,
1266
+ filter={"name__isw": ["DG Test"]},
1267
+ )
1268
+ dg.clean()
1269
+ dg.save(update_cached_members=False)
1270
+ self.assertEqual(0, dg.count)
1271
+
1272
+ job_result = create_job_result_and_run_job(
1273
+ self.job_module,
1274
+ self.job_name,
1275
+ object_model_name="extras.dynamicgroup",
1276
+ object_pk=dg.pk,
1277
+ )
1278
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_SUCCESS)
1279
+ self.assertEqual(3, dg.count)
1280
+
1281
+ dg.filter = {"name__iew": ["DG Test"]}
1282
+ dg.clean()
1283
+ dg.save(update_cached_members=False)
1284
+ self.assertEqual(3, dg.count)
1285
+ job_result = create_job_result_and_run_job(
1286
+ self.job_module,
1287
+ self.job_name,
1288
+ object_model_name="extras.dynamicgroup",
1289
+ object_pk=dg.pk,
1290
+ )
1291
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_SUCCESS)
1292
+ self.assertEqual(0, dg.count)
1293
+
1294
+ def test_failure_on_non_dg(self):
1295
+ job_result = create_job_result_and_run_job(
1296
+ self.job_module,
1297
+ self.job_name,
1298
+ object_model_name="extras.status",
1299
+ object_pk=Status.objects.first().pk,
1300
+ )
1301
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
1302
+ log_fail = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_FAILURE)
1303
+ self.assertEqual(log_fail.message, "This job button should only be used with Dynamic Group records.")
1304
+
1305
+ def test_failure_on_static_dg(self):
1306
+ dg = DynamicGroup.objects.create(
1307
+ name="Location Types",
1308
+ content_type=ContentType.objects.get_for_model(LocationType),
1309
+ group_type=DynamicGroupTypeChoices.TYPE_STATIC,
1310
+ )
1311
+ job_result = create_job_result_and_run_job(
1312
+ self.job_module,
1313
+ self.job_name,
1314
+ object_model_name="extras.dynamicgroup",
1315
+ object_pk=dg.pk,
1316
+ )
1317
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
1318
+ log_fail = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_FAILURE)
1319
+ self.assertEqual(
1320
+ log_fail.message,
1321
+ "The members of this Dynamic Group are statically defined and do not need to be recalculated.",
1322
+ )
1323
+
1324
+
1325
+ class ValidateModelDataTestCase(TransactionTestCase):
1326
+ job_module = "nautobot.core.jobs"
1327
+ job_name = "ValidateModelData"
1328
+
1329
+ def test_successful_validation(self):
1330
+ job_result = create_job_result_and_run_job(
1331
+ self.job_module,
1332
+ self.job_name,
1333
+ content_types=[ContentType.objects.get_for_model(Status).pk],
1334
+ verbose=True,
1335
+ )
1336
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_SUCCESS)
1337
+
1338
+ def test_failure_on_invalid_dg(self):
1339
+ dg = DynamicGroup(
1340
+ name="Legacy rear_port_template filter",
1341
+ filter={"rear_port_template": "74aac78c-fabb-468c-a036-26c46c56f27a"},
1342
+ content_type=ContentType.objects.get_for_model(FrontPortTemplate),
1343
+ )
1344
+ dg.save(update_cached_members=False)
1345
+ job_result = create_job_result_and_run_job(
1346
+ self.job_module,
1347
+ self.job_name,
1348
+ content_types=[ContentType.objects.get_for_model(DynamicGroup).pk],
1349
+ verbose=False,
1350
+ )
1351
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
1352
+ log_fail = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_FAILURE)
1353
+ self.assertIn("Enter a list of values", log_fail.message)
1354
+
1355
+ def test_warning_without_permission(self):
1356
+ job_result = create_job_result_and_run_job(
1357
+ self.job_module,
1358
+ self.job_name,
1359
+ username=self.user.username, # otherwise run_job_for_testing defaults to a superuser account
1360
+ content_types=[ContentType.objects.get_for_model(Status).pk],
1361
+ verbose=True,
1362
+ )
1363
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_SUCCESS)
1364
+ log_warn = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_WARNING)
1365
+ self.assertEqual("No statuses found", log_warn.message)
1366
+ self.assertIsNone(
1367
+ JobLogEntry.objects.filter(
1368
+ job_result=job_result, log_level=LogLevelChoices.LOG_SUCCESS, message="Validated successfully"
1369
+ ).first()
1370
+ )
1371
+
1372
+ def test_no_restrict_superuser(self):
1373
+ job_result = create_job_result_and_run_job(
1374
+ self.job_module,
1375
+ self.job_name,
1376
+ content_types=[ContentType.objects.get_for_model(User).pk],
1377
+ verbose=True,
1378
+ )
1379
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_SUCCESS)
1380
+
1381
+ def test_no_restrict_non_superuser(self):
1382
+ job_result = create_job_result_and_run_job(
1383
+ self.job_module,
1384
+ self.job_name,
1385
+ username=self.user.username, # otherwise run_job_for_testing defaults to a superuser account
1386
+ content_types=[ContentType.objects.get_for_model(User).pk],
1387
+ verbose=True,
1388
+ )
1389
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
1390
+ log_fail = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_FAILURE)
1391
+ self.assertIn("Unable to apply access permissions to users.user", log_fail.message)
@@ -13,6 +13,7 @@ from unittest import mock, TestCase
13
13
 
14
14
  from django import __version__ as django_version
15
15
  from django.conf import settings
16
+ from django.test import tag
16
17
 
17
18
  from nautobot import __version__ as nautobot_version
18
19
 
@@ -101,6 +102,7 @@ class NautobotServerTestCase(TestCase):
101
102
 
102
103
  self.assertNotEqual(secret_key_1, secret_key_2)
103
104
 
105
+ @tag("example_app")
104
106
  def test_settings_processing(self):
105
107
  result = subprocess.run(
106
108
  ["nautobot-server", "--config", settings.SETTINGS_PATH, "print_settings"],
@@ -1,9 +1,15 @@
1
+ import os
2
+
3
+ from django.apps import apps
1
4
  from django.test import tag, TestCase
2
5
  from django.urls import resolve
3
6
 
7
+ from nautobot.core.apps import NavMenuTab
4
8
  from nautobot.core.choices import ButtonActionColorChoices, ButtonActionIconChoices
5
9
  from nautobot.core.testing.utils import get_expected_menu_item_name
10
+ from nautobot.core.ui.choices import NavigationIconChoices, NavigationWeightChoices
6
11
  from nautobot.core.utils.lookup import get_route_for_model
12
+ from nautobot.core.utils.module_loading import import_string_optional
7
13
  from nautobot.core.utils.permissions import get_permission_for_model
8
14
  from nautobot.extras.registry import registry
9
15
 
@@ -45,7 +51,8 @@ class NavMenuTestCase(TestCase):
45
51
  except AttributeError:
46
52
  # Not a model view?
47
53
  self.assertIn(
48
- item_details["name"], {"Apps Marketplace", "Installed Apps", "Interface Connections"}
54
+ item_details["name"],
55
+ {"Apps Marketplace", "Installed Apps", "Interface Connections", "Device Constraints"},
49
56
  )
50
57
 
51
58
  for button, button_details in item_details["buttons"].items():
@@ -81,3 +88,71 @@ class NavMenuTestCase(TestCase):
81
88
  else:
82
89
  expected_perms[tab_name] |= group_perms
83
90
  self.assertEqual(expected_perms[tab_name], tab_details["permissions"])
91
+
92
+ def test_nav_menu_tabs_have_icon_and_weight(self):
93
+ """Ensure each NavMenuTab in every navigation.py has an icon and weight set, and any duplicates by name match."""
94
+ tabs_by_name = {}
95
+ for app in apps.get_app_configs():
96
+ if not app.name.startswith("nautobot."):
97
+ continue
98
+ nav_path = f"{app.name}.navigation.menu_items"
99
+ menu_items = import_string_optional(nav_path)
100
+ if menu_items is None:
101
+ continue
102
+ for tab in menu_items:
103
+ if not isinstance(tab, NavMenuTab):
104
+ raise TypeError(f"Expected NavMenuTab instance in {nav_path}, got {type(tab)}")
105
+ tab_name = tab.name
106
+ icon = tab.icon
107
+ weight = tab.weight
108
+ with self.subTest(tab_name=tab_name, nav_path=nav_path):
109
+ self.assertIsNotNone(tab_name, f"Tab in {nav_path} missing 'name'")
110
+ self.assertIsNotNone(icon, f"Tab '{tab_name}' in {nav_path} missing 'icon'")
111
+ self.assertIsNotNone(weight, f"Tab '{tab_name}' in {nav_path} missing 'weight'")
112
+ if tab_name in tabs_by_name:
113
+ prev_icon, prev_weight, prev_path = tabs_by_name[tab_name]
114
+ self.assertEqual(
115
+ icon,
116
+ prev_icon,
117
+ f"Tab '{tab_name}' has inconsistent icons: '{icon}' in {nav_path} vs '{prev_icon}' in {prev_path}",
118
+ )
119
+ self.assertEqual(
120
+ weight,
121
+ prev_weight,
122
+ f"Tab '{tab_name}' has inconsistent weights: '{weight}' in {nav_path} vs '{prev_weight}' in {prev_path}",
123
+ )
124
+ else:
125
+ tabs_by_name[tab_name] = (icon, weight, nav_path)
126
+
127
+ def test_icon_and_weight_class_attributes_match(self):
128
+ """
129
+ Ensure every class attribute in NavigationIconChoices is also in NavigationWeightChoices and vice versa.
130
+ If not, print the missing/extra attributes for easier debugging.
131
+ """
132
+ icon_attrs = {attr for attr in dir(NavigationIconChoices) if attr.isupper()}
133
+ weight_attrs = {attr for attr in dir(NavigationWeightChoices) if attr.isupper()}
134
+
135
+ only_in_icons = sorted(icon_attrs - weight_attrs)
136
+ only_in_weights = sorted(weight_attrs - icon_attrs)
137
+
138
+ if only_in_icons or only_in_weights:
139
+ msg = []
140
+ if only_in_icons:
141
+ msg.append(f"Class attributes only in NavigationIconChoices: {only_in_icons}")
142
+ if only_in_weights:
143
+ msg.append(f"Class attributes only in NavigationWeightChoices: {only_in_weights}")
144
+ self.fail("\n".join(msg))
145
+
146
+ def test_navigation_icons_have_svg(self):
147
+ """Ensure every NavigationIconChoices icon has a corresponding SVG file."""
148
+ missing = []
149
+ svg_dir = os.path.abspath(
150
+ os.path.join(os.path.dirname(__file__), "..", "..", "project-static", "nautobot-icons")
151
+ )
152
+ icon_attrs = [attr for attr in dir(NavigationIconChoices) if attr.isupper() and not attr == "CHOICES"]
153
+ for icon_attr in icon_attrs:
154
+ icon_name = getattr(NavigationIconChoices, icon_attr)
155
+ svg_path = os.path.join(svg_dir, f"{icon_name}.svg")
156
+ if not os.path.isfile(svg_path):
157
+ missing.append(svg_path)
158
+ self.assertFalse(missing, f"Missing SVG files for NavigationIconChoices: {missing}")
@@ -0,0 +1,42 @@
1
+ """
2
+ Test suite for social_django storage patch.
3
+
4
+ This tests that the monkeypatch correctly replaces the vulnerable create_user
5
+ method with the secure version that raises AuthAlreadyAssociated instead of
6
+ silently returning an existing user.
7
+
8
+ Please see nautobot/core/utils/patch_social_django.py for details on the patch.
9
+ """
10
+
11
+ from unittest.mock import MagicMock, patch
12
+
13
+ from nautobot.core.testing import TestCase
14
+
15
+
16
+ class PatchSocialDjangoTestCase(TestCase):
17
+ def test_django_storage_has_patch_at_import_time(self):
18
+ """
19
+ Test that importing DjangoStorage gives us the patched version.
20
+
21
+ This verifies that the patch applied in CoreConfig.ready() persists
22
+ and affects all imports of DjangoStorage throughout the application.
23
+ """
24
+ from django.db.utils import IntegrityError
25
+ from social_core.exceptions import AuthAlreadyAssociated
26
+ from social_django.models import DjangoStorage
27
+
28
+ # Mock user model to trigger IntegrityError
29
+ mock_user_model = MagicMock()
30
+ mock_manager = MagicMock()
31
+ mock_manager.create_user.side_effect = IntegrityError("duplicate key")
32
+ mock_user_model._default_manager = mock_manager
33
+
34
+ # Patch username_field and user_model methods to return our mock user model
35
+ with patch.object(DjangoStorage.user, "username_field", return_value="username"):
36
+ with patch.object(DjangoStorage.user, "user_model", return_value=mock_user_model):
37
+ # Should raise AuthAlreadyAssociated (patched behavior)
38
+ with self.assertRaises(AuthAlreadyAssociated):
39
+ DjangoStorage.user.create_user(username="test", email="test@example.com")
40
+
41
+ # Verify vulnerable get() not called
42
+ mock_manager.get.assert_not_called()
@@ -0,0 +1,59 @@
1
+ from unittest.mock import patch
2
+
3
+ from django.test import override_settings, RequestFactory
4
+ from django.urls import reverse
5
+ from rest_framework.response import Response
6
+
7
+ from nautobot.cloud.views import CloudResourceTypeUIViewSet
8
+ from nautobot.core.testing import TestCase
9
+ from nautobot.core.ui.titles import Titles
10
+ from nautobot.core.views.renderers import NautobotHTMLRenderer
11
+
12
+
13
+ class ObjectListViewTitlesTest(TestCase):
14
+ """
15
+ Test suite for verifying that Titles are correctly set in ObjectListView rendering.
16
+ """
17
+
18
+ user_permissions = ["cloud.view_cloudresourcetype"]
19
+
20
+ def setUp(self):
21
+ super().setUp()
22
+ self.factory = RequestFactory()
23
+
24
+ @override_settings(ALLOWED_HOSTS=["*"], PAGINATE_COUNT=5, MAX_PAGE_SIZE=10)
25
+ def test_uiviewset_list_view_title(self):
26
+ """
27
+ Test that the list view title is correctly set from the Titles configuration.
28
+ """
29
+
30
+ path = reverse("cloud:cloudresourcetype_list")
31
+ request = self.factory.get(path)
32
+ request.user = self.user
33
+ viewset_class = CloudResourceTypeUIViewSet
34
+ with patch.object(CloudResourceTypeUIViewSet, "view_titles", Titles(titles={"list": "Burritos"})):
35
+ view = viewset_class()
36
+ view.action_map = {"get": "list"}
37
+
38
+ request = view.initialize_request(request)
39
+
40
+ view.setup(request)
41
+ view.initial(request)
42
+
43
+ renderer = NautobotHTMLRenderer()
44
+ context = renderer.get_context(
45
+ data={},
46
+ accepted_media_type="text/html",
47
+ renderer_context={"view": view, "request": request, "response": Response({})},
48
+ )
49
+
50
+ # Verify that the title is set in the context
51
+ self.assertIn("view_titles", context)
52
+ self.assertNotIn(
53
+ "title", context
54
+ ) # title is used within the render path but should not be directly in context
55
+ self.assertEqual(context["view_titles"], viewset_class.view_titles)
56
+
57
+ # Finally, render the view and verify the title appears in the response
58
+ response = self.client.get(path)
59
+ self.assertContains(response, "Burritos")
@@ -132,6 +132,7 @@ class SettingsJSONSchemaTestCase(TestCase):
132
132
  "CSRF_FAILURE_VIEW",
133
133
  "DATA_UPLOAD_MAX_NUMBER_FIELDS",
134
134
  "DEFAULT_AUTO_FIELD",
135
+ "DJANGO_TABLES2_TEMPLATE",
135
136
  "DRF_REACT_TEMPLATE_TYPE_MAP",
136
137
  "EXEMPT_EXCLUDE_MODELS",
137
138
  "FILTERS_NULL_CHOICE_LABEL",
@@ -1,4 +1,4 @@
1
- from django.test import TestCase
1
+ from django.test import tag, TestCase
2
2
 
3
3
  from nautobot.circuits.models import Circuit
4
4
  from nautobot.circuits.tables import CircuitTable
@@ -32,6 +32,7 @@ class TableTestCase(TestCase):
32
32
  )
33
33
  self.assertEqual(list(table_queryset_data), list(sorted_queryset))
34
34
 
35
+ @tag("example_app")
35
36
  def test_tree_model_table_orderable(self):
36
37
  """Assert TreeNode model table are orderable."""
37
38
  location_type = LocationType.objects.get(name="Campus")
@@ -107,6 +108,7 @@ class TableTestCase(TestCase):
107
108
  )
108
109
  self.assertEqual(list(table_queryset_data), list(sorted_queryset))
109
110
 
111
+ @tag("example_app")
110
112
  def test_base_table_apis(self):
111
113
  """
112
114
  Test BaseTable APIs, specifically visible_columns and configurable_columns.
@@ -1,14 +1,16 @@
1
+ from unittest import mock
2
+
1
3
  from constance.test import override_config
2
4
  from django.conf import settings
5
+ from django.contrib.staticfiles.testing import StaticLiveServerTestCase
3
6
  from django.templatetags.static import static
4
- from django.test import override_settings, TestCase
7
+ from django.test import override_settings, tag
5
8
 
6
9
  from nautobot.core.templatetags import helpers
10
+ from nautobot.core.testing import TestCase
7
11
  from nautobot.dcim import models
8
12
  from nautobot.ipam.models import VLAN
9
13
 
10
- from example_app.models import AnotherExampleModel, ExampleModel
11
-
12
14
 
13
15
  class NautobotTemplatetagsHelperTest(TestCase):
14
16
  def test_hyperlinked_object(self):
@@ -61,6 +63,9 @@ class NautobotTemplatetagsHelperTest(TestCase):
61
63
  self.assertEqual(helpers.pre_tag(None), '<span class="text-secondary">&mdash;</span>')
62
64
  self.assertEqual(helpers.pre_tag([]), "<pre>[]</pre>")
63
65
  self.assertEqual(helpers.pre_tag("something"), "<pre>something</pre>")
66
+ self.assertEqual(helpers.pre_tag("", format_empty_value=False), '<span class="text-secondary">&mdash;</span>')
67
+ self.assertEqual(helpers.pre_tag([], format_empty_value=False), '<span class="text-secondary">&mdash;</span>')
68
+ self.assertEqual(helpers.pre_tag("something", format_empty_value=False), "<pre>something</pre>")
64
69
 
65
70
  def test_add_html_id(self):
66
71
  # Case where what we have isn't actually a HTML element but just a bare string
@@ -132,6 +137,7 @@ class NautobotTemplatetagsHelperTest(TestCase):
132
137
  )
133
138
  self.assertEqual("utf8:\n- 😀😀\n- 😀\n", helpers.render_yaml({"utf8": ["😀😀", "😀"]}, False))
134
139
 
140
+ @tag("example_app")
135
141
  def test_meta(self):
136
142
  location = models.Location.objects.first()
137
143
 
@@ -139,31 +145,42 @@ class NautobotTemplatetagsHelperTest(TestCase):
139
145
  self.assertEqual(helpers.meta(models.Location, "app_label"), "dcim")
140
146
  self.assertEqual(helpers.meta(location, "not_present"), "")
141
147
 
148
+ from example_app.models import ExampleModel
149
+
142
150
  self.assertEqual(helpers.meta(ExampleModel, "app_label"), "example_app")
143
151
 
152
+ @tag("example_app")
144
153
  def test_viewname(self):
145
154
  location = models.Location.objects.first()
146
155
 
147
156
  self.assertEqual(helpers.viewname(location, "edit"), "dcim:location_edit")
148
157
  self.assertEqual(helpers.viewname(models.Location, "test"), "dcim:location_test")
149
158
 
159
+ from example_app.models import ExampleModel
160
+
150
161
  self.assertEqual(helpers.viewname(ExampleModel, "edit"), "plugins:example_app:examplemodel_edit")
151
162
 
163
+ @tag("example_app")
152
164
  def test_validated_viewname(self):
153
165
  location = models.Location.objects.first()
154
166
 
155
167
  self.assertEqual(helpers.validated_viewname(location, "list"), "dcim:location_list")
156
168
  self.assertIsNone(helpers.validated_viewname(models.Location, "notvalid"))
157
169
 
170
+ from example_app.models import ExampleModel
171
+
158
172
  self.assertEqual(helpers.validated_viewname(ExampleModel, "list"), "plugins:example_app:examplemodel_list")
159
173
  self.assertIsNone(helpers.validated_viewname(ExampleModel, "notvalid"))
160
174
 
175
+ @tag("example_app")
161
176
  def test_validated_api_viewname(self):
162
177
  location = models.Location.objects.first()
163
178
 
164
179
  self.assertEqual(helpers.validated_api_viewname(location, "list"), "dcim-api:location-list")
165
180
  self.assertIsNone(helpers.validated_api_viewname(models.Location, "notvalid"))
166
181
 
182
+ from example_app.models import ExampleModel
183
+
167
184
  self.assertEqual(
168
185
  helpers.validated_api_viewname(ExampleModel, "list"), "plugins-api:example_app-api:examplemodel-list"
169
186
  )
@@ -180,7 +197,13 @@ class NautobotTemplatetagsHelperTest(TestCase):
180
197
  def test_humanize_speed(self):
181
198
  self.assertEqual(helpers.humanize_speed(1544), "1.544 Mbps")
182
199
  self.assertEqual(helpers.humanize_speed(100000), "100 Mbps")
200
+ self.assertEqual(helpers.humanize_speed(2500000), "2.5 Gbps")
183
201
  self.assertEqual(helpers.humanize_speed(10000000), "10 Gbps")
202
+ self.assertEqual(helpers.humanize_speed(100000000), "100 Gbps")
203
+ self.assertEqual(helpers.humanize_speed(1000000000), "1 Tbps")
204
+ self.assertEqual(helpers.humanize_speed(1600000000), "1.6 Tbps")
205
+ self.assertEqual(helpers.humanize_speed(10000000000), "10 Tbps")
206
+ self.assertEqual(helpers.humanize_speed(100000000000), "100 Tbps")
184
207
 
185
208
  def test_tzoffset(self):
186
209
  self.assertTrue(callable(helpers.tzoffset))
@@ -201,16 +224,6 @@ class NautobotTemplatetagsHelperTest(TestCase):
201
224
  self.assertEqual(helpers.percentage(2, 10), 20)
202
225
  self.assertEqual(helpers.percentage(10, 3), 333)
203
226
 
204
- def test_get_docs_url(self):
205
- self.assertTrue(callable(helpers.get_docs_url))
206
- location = models.Location.objects.first()
207
- self.assertEqual(helpers.get_docs_url(location), static("docs/user-guide/core-data-model/dcim/location.html"))
208
- example_model = ExampleModel.objects.create(name="test", number=1)
209
- self.assertEqual(helpers.get_docs_url(example_model), static("example_app/docs/models/examplemodel.html"))
210
- # AnotherExampleModel does not have documentation.
211
- another_model = AnotherExampleModel.objects.create(name="test", number=1)
212
- self.assertIsNone(helpers.get_docs_url(another_model))
213
-
214
227
  def test_has_perms(self):
215
228
  self.assertTrue(callable(helpers.has_perms))
216
229
  # TODO add unit tests for has_perms
@@ -265,6 +278,7 @@ class NautobotTemplatetagsHelperTest(TestCase):
265
278
  # Assert when obj is None
266
279
  self.assertEqual(helpers.hyperlinked_object_with_color(obj=None), '<span class="text-secondary">&mdash;</span>')
267
280
 
281
+ @tag("example_app")
268
282
  @override_settings(BANNER_TOP="¡Hola, mundo!")
269
283
  @override_config(example_app__SAMPLE_VARIABLE="Testing")
270
284
  def test_settings_or_config(self):
@@ -338,3 +352,38 @@ class NautobotTemplatetagsHelperTest(TestCase):
338
352
  "-85 dBm",
339
353
  )
340
354
  self.assertEqual(helpers.dbm(None), helpers.placeholder(None))
355
+
356
+
357
+ @tag("test")
358
+ class NautobotStaticDocsTestCase(StaticLiveServerTestCase):
359
+ @tag("example_app")
360
+ def test_get_docs_url(self):
361
+ self.assertTrue(callable(helpers.get_docs_url))
362
+ location_type = models.LocationType.objects.create(name="Some Location Type")
363
+ self.assertEqual(
364
+ helpers.get_docs_url(location_type), static("docs/user-guide/core-data-model/dcim/locationtype.html")
365
+ )
366
+
367
+ from example_app.models import AnotherExampleModel, ExampleModel
368
+
369
+ example_model = ExampleModel.objects.create(name="test", number=1)
370
+ self.assertEqual(helpers.get_docs_url(example_model), "/docs/example-app/models/examplemodel.html")
371
+ # AnotherExampleModel does not have documentation.
372
+ another_model = AnotherExampleModel.objects.create(name="test", number=1)
373
+ self.assertIsNone(helpers.get_docs_url(another_model))
374
+
375
+ @tag("example_app")
376
+ @mock.patch("nautobot.core.templatetags.helpers.find", return_value=False)
377
+ @mock.patch("nautobot.core.templatetags.helpers.resources.files", side_effect=ModuleNotFoundError)
378
+ def test_get_docs_url_module_not_found_and_no_static_file(self, mock_files, mock_find):
379
+ # Force `resources.files()` to raise ModuleNotFoundError to simulate a plugin
380
+ # that is listed in settings.PLUGINS but doesn't actually exist on disk.
381
+ # This ensures the `except ModuleNotFoundError` branch is covered.
382
+ from example_app.models import ExampleModel
383
+
384
+ example_model = ExampleModel.objects.create(name="test", number=1)
385
+ result = helpers.get_docs_url(example_model)
386
+ self.assertIsNone(result)
387
+
388
+ mock_files.assert_called_once()
389
+ mock_find.assert_called_once()
@@ -65,7 +65,7 @@ class NautobotTemplatetagsUIComponentsTest(TestCase):
65
65
  {
66
66
  "list_url": "home",
67
67
  "title": "New Home",
68
- "view_action": "list",
68
+ "detail": True,
69
69
  "breadcrumbs": Breadcrumbs(),
70
70
  }
71
71
  )
@@ -98,7 +98,7 @@ class NautobotTemplatetagsUIComponentsTest(TestCase):
98
98
  {
99
99
  "list_url": "home",
100
100
  "title": "New Home",
101
- "view_action": "list",
101
+ "detail": True,
102
102
  "breadcrumbs": Breadcrumbs(),
103
103
  }
104
104
  )
@@ -126,7 +126,7 @@ class NautobotTemplatetagsUIComponentsTest(TestCase):
126
126
  {
127
127
  "list_url": "home",
128
128
  "title": "New Home",
129
- "view_action": "list",
129
+ "detail": True,
130
130
  "breadcrumbs": Breadcrumbs(),
131
131
  }
132
132
  )
@@ -152,7 +152,7 @@ class NautobotTemplatetagsUIComponentsTest(TestCase):
152
152
  {
153
153
  "list_url": "home",
154
154
  "title": "New Home",
155
- "view_action": "list",
155
+ "detail": True,
156
156
  "breadcrumbs": Breadcrumbs(),
157
157
  }
158
158
  )
@@ -107,22 +107,6 @@ class TitlesTestCase(TestCase):
107
107
  "context": {"view_action": "bulk_update", "objs_count": 10, "verbose_name_plural": "devices"},
108
108
  "expected": "Editing 10 Devices",
109
109
  },
110
- {
111
- "name": "changelog_action",
112
- "context": {
113
- "view_action": "changelog",
114
- "object": location_type,
115
- },
116
- "expected": "Test Location Type Title - Change Log",
117
- },
118
- {
119
- "name": "notes_action",
120
- "context": {
121
- "view_action": "notes",
122
- "object": location_type,
123
- },
124
- "expected": "Test Location Type Title - Notes",
125
- },
126
110
  {
127
111
  "name": "approve_action",
128
112
  "context": {"view_action": "approve", "verbose_name": "device"},
@@ -13,8 +13,21 @@ class TestInvalidateMaxTreeDepthSignal(TestCase):
13
13
  # Ensure that the max_depth hasn't already been cached
14
14
  Location.objects.__dict__.pop("max_depth", None)
15
15
  location = Location.objects.first()
16
- with self.assertNumQueries(1):
16
+
17
+ with CaptureQueriesContext(connection) as ctx:
17
18
  location.save()
19
+ captured_tree_cte_queries = [
20
+ query["sql"] for query in ctx.captured_queries if "WITH RECURSIVE" in query["sql"]
21
+ ]
22
+ allowed_number_of_tree_queries = 0 # We don't expect any tree queries to be run
23
+ _query_separator = "\n" + ("-" * 10) + "\n" + "NEXT QUERY" + "\n" + ("-" * 10)
24
+ self.assertEqual(
25
+ len(captured_tree_cte_queries),
26
+ allowed_number_of_tree_queries,
27
+ f"The CTE tree was calculated a different number of times ({len(captured_tree_cte_queries)})"
28
+ f" than allowed ({allowed_number_of_tree_queries})."
29
+ f" The following queries were used:\n{_query_separator.join(captured_tree_cte_queries)}",
30
+ )
18
31
 
19
32
 
20
33
  class QuerySetAncestorTests(TestCase):