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

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

Potentially problematic release.


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

Files changed (420) hide show
  1. nautobot/apps/choices.py +0 -2
  2. nautobot/apps/filters.py +7 -9
  3. nautobot/apps/models.py +2 -2
  4. nautobot/apps/ui.py +9 -1
  5. nautobot/circuits/filters.py +3 -2
  6. nautobot/circuits/navigation.py +3 -2
  7. nautobot/circuits/templates/circuits/circuit.html +1 -1
  8. nautobot/circuits/templates/circuits/circuit_create.html +3 -3
  9. nautobot/circuits/templates/circuits/circuittermination.html +1 -1
  10. nautobot/circuits/templates/circuits/circuittermination_create.html +9 -24
  11. nautobot/circuits/templates/circuits/circuittype.html +1 -1
  12. nautobot/circuits/templates/circuits/inc/circuit_termination_cable_fragment.html +6 -6
  13. nautobot/circuits/templates/circuits/inc/speed_widget.html +12 -12
  14. nautobot/circuits/templates/circuits/providernetwork.html +1 -1
  15. nautobot/circuits/tests/integration/test_circuit.py +10 -13
  16. nautobot/cloud/filters.py +1 -1
  17. nautobot/cloud/navigation.py +3 -2
  18. nautobot/core/api/schema.py +1 -1
  19. nautobot/core/api/serializers.py +6 -1
  20. nautobot/core/api/urls.py +1 -0
  21. nautobot/core/api/views.py +8 -0
  22. nautobot/core/apps/__init__.py +11 -10
  23. nautobot/core/celery/__init__.py +3 -5
  24. nautobot/core/checks.py +46 -0
  25. nautobot/core/cli/bootstrap_v3_to_v5.py +70 -1
  26. nautobot/core/cli/migrate_deprecated_templates.py +200 -0
  27. nautobot/core/constants.py +3 -0
  28. nautobot/core/context_processors.py +9 -1
  29. nautobot/core/forms/forms.py +1 -1
  30. nautobot/core/jobs/__init__.py +6 -3
  31. nautobot/core/jobs/groups.py +31 -1
  32. nautobot/core/management/commands/generate_test_data.py +28 -9
  33. nautobot/core/models/generics.py +9 -1
  34. nautobot/core/models/tree_queries.py +10 -5
  35. nautobot/core/settings.py +18 -12
  36. nautobot/core/settings.yaml +13 -7
  37. nautobot/core/signals.py +12 -1
  38. nautobot/core/tables.py +13 -6
  39. nautobot/core/templates/40x.html +1 -1
  40. nautobot/core/templates/500.html +2 -2
  41. nautobot/core/templates/admin/config/config.html +12 -12
  42. nautobot/core/templates/admin/index.html +3 -3
  43. nautobot/core/templates/buttons/export.html +1 -1
  44. nautobot/core/templates/components/button/dropdown.html +5 -3
  45. nautobot/core/templates/components/panel/body_wrapper_generic_table.html +1 -1
  46. nautobot/core/templates/components/panel/panel.html +3 -3
  47. nautobot/core/templates/components/tab/content_wrapper.html +2 -3
  48. nautobot/core/templates/components/tab/label_wrapper_distinct_view.html +1 -1
  49. nautobot/core/templates/echarts/echarts.html +1 -1
  50. nautobot/core/templates/generic/object_bulk_add_component.html +2 -1
  51. nautobot/core/templates/generic/object_bulk_create.html +4 -3
  52. nautobot/core/templates/generic/object_bulk_destroy.html +3 -3
  53. nautobot/core/templates/generic/object_bulk_remove.html +2 -2
  54. nautobot/core/templates/generic/object_bulk_update.html +5 -4
  55. nautobot/core/templates/generic/object_create.html +5 -4
  56. nautobot/core/templates/generic/object_import.html +2 -1
  57. nautobot/core/templates/generic/object_list.html +12 -4
  58. nautobot/core/templates/generic/object_notes.html +5 -3
  59. nautobot/core/templates/generic/object_retrieve.html +2 -3
  60. nautobot/core/templates/graphene/graphiql.html +7 -7
  61. nautobot/core/templates/home.html +1 -1
  62. nautobot/core/templates/import_success.html +2 -1
  63. nautobot/core/templates/inc/computed_fields/panel_data.html +1 -1
  64. nautobot/core/templates/inc/created_updated.html +7 -3
  65. nautobot/core/templates/inc/custom_fields/panel_data.html +1 -1
  66. nautobot/core/templates/inc/form_static_field.html +6 -0
  67. nautobot/core/templates/inc/header.html +1 -1
  68. nautobot/core/templates/inc/image_attachments.html +2 -1
  69. nautobot/core/templates/inc/nav_menu.html +2 -1
  70. nautobot/core/templates/inc/search_panel.html +4 -4
  71. nautobot/core/templates/login.html +4 -2
  72. nautobot/core/templates/nautobot_config.py.j2 +6 -5
  73. nautobot/core/templates/redoc_ui.html +7 -0
  74. nautobot/core/templates/search.html +1 -1
  75. nautobot/core/templates/swagger_ui.html +17 -3
  76. nautobot/core/templates/system_jobs/import_objects.html +1 -2
  77. nautobot/core/templates/utilities/confirmation_form.html +2 -2
  78. nautobot/core/templates/utilities/obj_table.html +10 -2
  79. nautobot/core/templates/utilities/render_field.html +7 -7
  80. nautobot/core/templates/utilities/render_jinja2.html +2 -2
  81. nautobot/core/templates/utilities/templatetags/filter_form_drawer.html +4 -4
  82. nautobot/core/templates/utilities/theme_preview.html +16 -3
  83. nautobot/core/templates/widgets/selectwithdisabled_option.html +3 -1
  84. nautobot/core/templatetags/helpers.py +52 -6
  85. nautobot/core/testing/api.py +68 -9
  86. nautobot/core/testing/filters.py +0 -23
  87. nautobot/core/testing/integration.py +23 -10
  88. nautobot/core/testing/mixins.py +2 -0
  89. nautobot/core/testing/views.py +4 -0
  90. nautobot/core/tests/integration/test_app_home.py +34 -30
  91. nautobot/core/tests/integration/test_app_navbar.py +3 -0
  92. nautobot/core/tests/nautobot_config_without_example_apps.py +4 -0
  93. nautobot/core/tests/runner.py +9 -1
  94. nautobot/core/tests/test_api.py +5 -3
  95. nautobot/core/tests/test_breadcrumbs.py +6 -7
  96. nautobot/core/tests/test_checks.py +28 -0
  97. nautobot/core/tests/test_cli.py +40 -0
  98. nautobot/core/tests/test_config.py +2 -1
  99. nautobot/core/tests/test_forms.py +55 -13
  100. nautobot/core/tests/test_jobs.py +75 -1
  101. nautobot/core/tests/test_nautobot_server.py +2 -0
  102. nautobot/core/tests/test_navigations.py +76 -1
  103. nautobot/core/tests/test_patch_social_django.py +42 -0
  104. nautobot/core/tests/test_tables.py +3 -1
  105. nautobot/core/tests/test_templatetags_helpers.py +53 -13
  106. nautobot/core/tests/test_templatetags_ui_framework.py +4 -4
  107. nautobot/core/tests/test_tree_queries.py +14 -1
  108. nautobot/core/tests/test_ui.py +1 -1
  109. nautobot/core/tests/test_utils.py +31 -4
  110. nautobot/core/tests/test_views.py +159 -31
  111. nautobot/core/ui/breadcrumbs.py +2 -12
  112. nautobot/core/ui/choices.py +142 -10
  113. nautobot/core/ui/constants.py +76 -12
  114. nautobot/core/ui/object_detail.py +92 -12
  115. nautobot/core/urls.py +12 -1
  116. nautobot/core/utils/cache.py +2 -1
  117. nautobot/core/utils/filtering.py +17 -17
  118. nautobot/core/utils/lookup.py +3 -8
  119. nautobot/core/utils/module_loading.py +21 -0
  120. nautobot/core/utils/patch_social_django.py +128 -0
  121. nautobot/core/views/__init__.py +38 -1
  122. nautobot/core/views/generic.py +3 -3
  123. nautobot/core/views/mixins.py +15 -3
  124. nautobot/core/views/renderers.py +2 -0
  125. nautobot/core/views/viewsets.py +2 -1
  126. nautobot/data_validation/apps.py +1 -5
  127. nautobot/data_validation/custom_validators.py +4 -4
  128. nautobot/data_validation/filters.py +1 -1
  129. nautobot/data_validation/forms.py +40 -0
  130. nautobot/data_validation/migrations/0001_initial.py +0 -7
  131. nautobot/data_validation/migrations/0002_data_migration_from_app.py +0 -12
  132. nautobot/data_validation/models.py +16 -7
  133. nautobot/data_validation/navigation.py +8 -1
  134. nautobot/data_validation/tables.py +12 -5
  135. nautobot/data_validation/templates/data_validation/datacompliance_tab.html +1 -0
  136. nautobot/data_validation/templates/data_validation/device_constraints.html +61 -0
  137. nautobot/data_validation/tests/__init__.py +2 -2
  138. nautobot/data_validation/tests/migrations/test_migrations.py +83 -3
  139. nautobot/data_validation/tests/test_data_compliance_rules.py +12 -7
  140. nautobot/data_validation/tests/test_filters.py +8 -6
  141. nautobot/data_validation/tests/test_models.py +15 -0
  142. nautobot/data_validation/tests/test_views.py +190 -32
  143. nautobot/data_validation/urls.py +2 -5
  144. nautobot/data_validation/views.py +73 -40
  145. nautobot/dcim/api/serializers.py +0 -13
  146. nautobot/dcim/apps.py +4 -0
  147. nautobot/dcim/choices.py +16 -0
  148. nautobot/dcim/custom_validators.py +84 -0
  149. nautobot/dcim/filter_mixins.py +353 -4
  150. nautobot/dcim/{filters/__init__.py → filters.py} +2 -35
  151. nautobot/dcim/forms.py +1 -1
  152. nautobot/dcim/migrations/0078_remove_device_location_tenant_name_uniqueness.py +16 -0
  153. nautobot/dcim/migrations/0079_device_name_data_migration.py +59 -0
  154. nautobot/dcim/models/device_components.py +81 -68
  155. nautobot/dcim/models/devices.py +13 -16
  156. nautobot/dcim/navigation.py +7 -6
  157. nautobot/dcim/tables/devices.py +3 -0
  158. nautobot/dcim/tables/template_code.py +14 -14
  159. nautobot/dcim/templates/dcim/cable.html +2 -61
  160. nautobot/dcim/templates/dcim/cable_connect.html +28 -112
  161. nautobot/dcim/templates/dcim/cable_edit.html +2 -5
  162. nautobot/dcim/templates/dcim/cable_retrieve.html +61 -0
  163. nautobot/dcim/templates/dcim/cable_trace.html +1 -3
  164. nautobot/dcim/templates/dcim/cable_update.html +5 -0
  165. nautobot/dcim/templates/dcim/consoleport.html +6 -5
  166. nautobot/dcim/templates/dcim/consoleserverport.html +6 -5
  167. nautobot/dcim/templates/dcim/device/config.html +2 -2
  168. nautobot/dcim/templates/dcim/device/consoleports.html +1 -1
  169. nautobot/dcim/templates/dcim/device/consoleserverports.html +1 -1
  170. nautobot/dcim/templates/dcim/device/devicebays.html +1 -1
  171. nautobot/dcim/templates/dcim/device/frontports.html +1 -1
  172. nautobot/dcim/templates/dcim/device/interfaces.html +1 -1
  173. nautobot/dcim/templates/dcim/device/inventory.html +1 -1
  174. nautobot/dcim/templates/dcim/device/lldp_neighbors.html +1 -1
  175. nautobot/dcim/templates/dcim/device/modulebays.html +1 -1
  176. nautobot/dcim/templates/dcim/device/poweroutlets.html +1 -1
  177. nautobot/dcim/templates/dcim/device/powerports.html +1 -1
  178. nautobot/dcim/templates/dcim/device/rearports.html +1 -1
  179. nautobot/dcim/templates/dcim/device/status.html +8 -8
  180. nautobot/dcim/templates/dcim/device/wireless.html +1 -1
  181. nautobot/dcim/templates/dcim/device.html +1 -1
  182. nautobot/dcim/templates/dcim/device_component_add.html +2 -2
  183. nautobot/dcim/templates/dcim/device_create.html +5 -3
  184. nautobot/dcim/templates/dcim/device_interface_delete.html +1 -1
  185. nautobot/dcim/templates/dcim/device_list.html +73 -10
  186. nautobot/dcim/templates/dcim/devicebay_populate.html +2 -2
  187. nautobot/dcim/templates/dcim/devicetype.html +1 -1
  188. nautobot/dcim/templates/dcim/devicetype_component_add.html +2 -2
  189. nautobot/dcim/templates/dcim/footer_convert_to_contact_or_team_record.html +14 -0
  190. nautobot/dcim/templates/dcim/frontport.html +9 -8
  191. nautobot/dcim/templates/dcim/inc/edit_form_softwareversion_js.html +2 -2
  192. nautobot/dcim/templates/dcim/interface.html +26 -6
  193. nautobot/dcim/templates/dcim/interface_bulk_delete.html +1 -1
  194. nautobot/dcim/templates/dcim/inventoryitem_add.html +3 -1
  195. nautobot/dcim/templates/dcim/inventoryitem_bulk_delete.html +1 -1
  196. nautobot/dcim/templates/dcim/inventoryitem_edit.html +3 -1
  197. nautobot/dcim/templates/dcim/location_retrieve.html +1 -242
  198. nautobot/dcim/templates/dcim/module/base.html +49 -9
  199. nautobot/dcim/templates/dcim/module_list.html +57 -8
  200. nautobot/dcim/templates/dcim/modulefamily_retrieve.html +1 -1
  201. nautobot/dcim/templates/dcim/moduletype_retrieve.html +49 -9
  202. nautobot/dcim/templates/dcim/platform_create.html +1 -1
  203. nautobot/dcim/templates/dcim/powerfeed.html +1 -1
  204. nautobot/dcim/templates/dcim/powerpanel.html +1 -1
  205. nautobot/dcim/templates/dcim/powerport.html +5 -4
  206. nautobot/dcim/templates/dcim/rack_elevation_list.html +16 -4
  207. nautobot/dcim/templates/dcim/rack_retrieve.html +33 -15
  208. nautobot/dcim/templates/dcim/rearport.html +7 -6
  209. nautobot/dcim/templates/dcim/virtualchassis.html +1 -1
  210. nautobot/dcim/templates/dcim/virtualchassis_add_member.html +16 -14
  211. nautobot/dcim/templates/dcim/virtualchassis_update.html +14 -6
  212. nautobot/dcim/tests/integration/test_controller.py +1 -0
  213. nautobot/dcim/tests/test_api.py +8 -0
  214. nautobot/dcim/tests/test_custom_validators.py +229 -0
  215. nautobot/dcim/tests/test_filters.py +12 -6
  216. nautobot/dcim/tests/test_models.py +63 -4
  217. nautobot/dcim/tests/test_views.py +63 -22
  218. nautobot/dcim/urls.py +64 -21
  219. nautobot/dcim/utils.py +3 -3
  220. nautobot/dcim/views.py +547 -273
  221. nautobot/extras/api/views.py +9 -1
  222. nautobot/extras/choices.py +2 -13
  223. nautobot/extras/{filters/mixins.py → filter_mixins.py} +1 -1
  224. nautobot/extras/{filters/customfields.py → filter_mixins_customfields.py} +42 -6
  225. nautobot/extras/{filters/__init__.py → filters.py} +14 -46
  226. nautobot/extras/forms/forms.py +5 -13
  227. nautobot/extras/forms/mixins.py +0 -41
  228. nautobot/extras/management/__init__.py +9 -0
  229. nautobot/extras/migrations/0127_approval_workflow_models.py +6 -6
  230. nautobot/extras/migrations/0129_jobresult_debug_log_count_jobresult_error_log_count_and_more.py +37 -0
  231. nautobot/extras/migrations/0130_jobresult_generate_log_entry_counts.py +42 -0
  232. nautobot/extras/models/__init__.py +1 -2
  233. nautobot/extras/models/approvals.py +22 -13
  234. nautobot/extras/models/contacts.py +2 -0
  235. nautobot/extras/models/groups.py +44 -5
  236. nautobot/extras/models/jobs.py +59 -1
  237. nautobot/extras/models/mixins.py +28 -0
  238. nautobot/extras/models/models.py +13 -0
  239. nautobot/extras/models/secrets.py +1 -0
  240. nautobot/extras/models/statuses.py +0 -15
  241. nautobot/extras/navigation.py +13 -9
  242. nautobot/extras/plugins/__init__.py +33 -55
  243. nautobot/extras/plugins/tables.py +3 -3
  244. nautobot/extras/plugins/urls.py +2 -21
  245. nautobot/extras/plugins/utils.py +1 -33
  246. nautobot/extras/plugins/views.py +0 -4
  247. nautobot/extras/signals.py +20 -19
  248. nautobot/extras/tables.py +52 -68
  249. nautobot/extras/templates/extras/approval_dashboard.html +7 -5
  250. nautobot/extras/templates/extras/approvalworkflowdefinition_update.html +4 -2
  251. nautobot/extras/templates/extras/approvalworkflowstage_retrieve.html +20 -12
  252. nautobot/extras/templates/extras/computedfield.html +1 -1
  253. nautobot/extras/templates/extras/configcontext.html +1 -1
  254. nautobot/extras/templates/extras/configcontextschema_validation.html +2 -2
  255. nautobot/extras/templates/extras/customfield.html +1 -1
  256. nautobot/extras/templates/extras/dynamicgroup_retrieve.html +11 -5
  257. nautobot/extras/templates/extras/dynamicgroup_update.html +1 -1
  258. nautobot/extras/templates/extras/gitrepository_result.html +0 -2
  259. nautobot/extras/templates/extras/graphqlquery_retrieve.html +1 -96
  260. nautobot/extras/templates/extras/inc/approval_buttons_column.html +20 -6
  261. nautobot/extras/templates/extras/inc/bulk_edit_overridable_field.html +8 -7
  262. nautobot/extras/templates/extras/inc/configcontext_format.html +10 -3
  263. nautobot/extras/templates/extras/inc/graphqlquery_execute.html +71 -0
  264. nautobot/extras/templates/extras/inc/job_tiles.html +15 -3
  265. nautobot/extras/templates/extras/inc/json_format.html +10 -3
  266. nautobot/extras/templates/extras/inc/overridable_field.html +13 -12
  267. nautobot/extras/templates/extras/job.html +29 -12
  268. nautobot/extras/templates/extras/job_bulk_edit.html +18 -0
  269. nautobot/extras/templates/extras/job_edit.html +52 -46
  270. nautobot/extras/templates/extras/job_list.html +29 -25
  271. nautobot/extras/templates/extras/marketplace.html +5 -9
  272. nautobot/extras/templates/extras/object_configcontext.html +1 -1
  273. nautobot/extras/templates/extras/object_dynamicgroups.html +2 -2
  274. nautobot/extras/templates/extras/objectchange_retrieve.html +19 -37
  275. nautobot/extras/templates/extras/plugin_detail.html +26 -21
  276. nautobot/extras/templates/extras/plugins_list.html +16 -26
  277. nautobot/extras/templates/extras/role_retrieve.html +64 -0
  278. nautobot/extras/templates/extras/scheduledjob.html +4 -2
  279. nautobot/extras/templates/extras/secretsgroup.html +1 -1
  280. nautobot/extras/templates/extras/tag.html +1 -1
  281. nautobot/extras/templatetags/custom_links.py +12 -12
  282. nautobot/extras/templatetags/job_buttons.py +14 -12
  283. nautobot/extras/test_jobs/invalid_import.py +9 -0
  284. nautobot/extras/test_jobs/log_counts_by_level.py +23 -0
  285. nautobot/extras/test_jobs/missing_import.py +11 -0
  286. nautobot/extras/tests/integration/test_configcontextschema.py +27 -26
  287. nautobot/extras/tests/integration/test_customfields.py +8 -7
  288. nautobot/extras/tests/integration/test_dynamicgroups.py +5 -1
  289. nautobot/extras/tests/integration/test_plugin_banner.py +3 -0
  290. nautobot/extras/tests/integration/test_plugins.py +18 -6
  291. nautobot/extras/tests/test_api.py +27 -18
  292. nautobot/extras/tests/test_approvals.py +38 -38
  293. nautobot/extras/tests/test_changelog.py +35 -3
  294. nautobot/extras/tests/test_customfields.py +22 -13
  295. nautobot/extras/tests/test_customfields_filters.py +479 -0
  296. nautobot/extras/tests/test_dynamicgroups.py +39 -1
  297. nautobot/extras/tests/test_filters.py +21 -19
  298. nautobot/extras/tests/test_forms.py +18 -21
  299. nautobot/extras/tests/test_jobs.py +25 -4
  300. nautobot/extras/tests/test_migrations.py +1 -0
  301. nautobot/extras/tests/test_models.py +13 -31
  302. nautobot/extras/tests/test_plugins.py +36 -10
  303. nautobot/extras/tests/test_views.py +31 -30
  304. nautobot/extras/views.py +81 -19
  305. nautobot/ipam/factory.py +7 -0
  306. nautobot/ipam/filter_mixins.py +38 -0
  307. nautobot/ipam/filters.py +27 -38
  308. nautobot/ipam/formfields.py +1 -1
  309. nautobot/ipam/forms.py +6 -3
  310. nautobot/ipam/migrations/0030_ipam__namespaces.py +13 -0
  311. nautobot/ipam/migrations/0031_ipam___data_migrations.py +4 -1
  312. nautobot/ipam/migrations/0054_namespace_tenant.py +25 -0
  313. nautobot/ipam/models.py +29 -2
  314. nautobot/ipam/navigation.py +3 -2
  315. nautobot/ipam/signals.py +71 -0
  316. nautobot/ipam/tables.py +13 -6
  317. nautobot/ipam/templates/ipam/inc/toggle_available.html +10 -10
  318. nautobot/ipam/templates/ipam/inc/vlangroup_header.html +1 -0
  319. nautobot/ipam/templates/ipam/ipaddress.html +14 -0
  320. nautobot/ipam/templates/ipam/ipaddress_merge.html +3 -3
  321. nautobot/ipam/templates/ipam/ipaddresstointerface_retrieve.html +1 -0
  322. nautobot/ipam/templates/ipam/namespace_update.html +15 -0
  323. nautobot/ipam/templates/ipam/prefix_delete.html +1 -1
  324. nautobot/ipam/templates/ipam/prefix_list.html +14 -13
  325. nautobot/ipam/templates/ipam/service.html +1 -1
  326. nautobot/ipam/templates/ipam/vlan.html +1 -1
  327. nautobot/ipam/templates/ipam/vlan_interfaces.html +1 -1
  328. nautobot/ipam/templates/ipam/vlan_vminterfaces.html +1 -1
  329. nautobot/ipam/tests/migration/test_migrations.py +89 -0
  330. nautobot/ipam/tests/test_api.py +13 -6
  331. nautobot/ipam/tests/test_filters.py +10 -0
  332. nautobot/ipam/tests/test_forms.py +1 -1
  333. nautobot/ipam/tests/test_models.py +43 -1
  334. nautobot/ipam/tests/test_tables.py +1 -2
  335. nautobot/ipam/tests/test_utils.py +1 -1
  336. nautobot/ipam/tests/test_views.py +13 -14
  337. nautobot/ipam/ui.py +0 -17
  338. nautobot/ipam/utils/migrations.py +16 -2
  339. nautobot/ipam/utils/testing.py +9 -3
  340. nautobot/ipam/views.py +46 -6
  341. nautobot/project-static/dist/css/nautobot.css +1 -1
  342. nautobot/project-static/dist/css/nautobot.css.map +1 -1
  343. nautobot/project-static/dist/js/nautobot.js +1 -1
  344. nautobot/project-static/dist/js/nautobot.js.map +1 -1
  345. nautobot/project-static/js/cabletrace.js +1 -1
  346. nautobot/project-static/js/interface_filtering.js +20 -16
  347. nautobot/project-static/nautobot-icons/battery-3.svg +3 -0
  348. nautobot/project-static/nautobot-icons/cloud.svg +1 -1
  349. nautobot/project-static/nautobot-icons/control-panel.svg +1 -1
  350. nautobot/project-static/nautobot-icons/device-lifecycle.svg +1 -1
  351. nautobot/project-static/nautobot-icons/elements.svg +1 -1
  352. nautobot/project-static/nautobot-icons/extensibility.svg +3 -0
  353. nautobot/project-static/nautobot-icons/hammer.svg +1 -1
  354. nautobot/project-static/nautobot-icons/organization.svg +3 -0
  355. nautobot/project-static/nautobot-icons/secrets.svg +1 -1
  356. nautobot/project-static/nautobot-icons/security.svg +3 -0
  357. nautobot/project-static/nautobot-icons/server.svg +1 -1
  358. nautobot/project-static/nautobot-icons/star-filled.svg +1 -1
  359. nautobot/project-static/nautobot-icons/star.svg +1 -1
  360. nautobot/tenancy/api/serializers.py +1 -0
  361. nautobot/tenancy/api/views.py +2 -1
  362. nautobot/tenancy/{filters/__init__.py → filters.py} +2 -10
  363. nautobot/tenancy/navigation.py +3 -1
  364. nautobot/tenancy/tests/test_filters.py +0 -2
  365. nautobot/tenancy/views.py +2 -1
  366. nautobot/ui/src/js/collapse.js +3 -3
  367. nautobot/ui/src/js/nautobot.js +16 -0
  368. nautobot/ui/src/scss/colors.scss +1 -1
  369. nautobot/ui/src/scss/nautobot.scss +61 -28
  370. nautobot/users/templates/users/profile.html +45 -12
  371. nautobot/users/templates/users/sessionkey_delete.html +1 -1
  372. nautobot/users/tests/test_api.py +4 -0
  373. nautobot/users/views.py +4 -2
  374. nautobot/virtualization/models.py +1 -68
  375. nautobot/virtualization/navigation.py +3 -2
  376. nautobot/virtualization/templates/virtualization/virtual_machine_vminterface_delete.html +1 -1
  377. nautobot/virtualization/templates/virtualization/virtualmachine.html +1 -1
  378. nautobot/virtualization/templates/virtualization/virtualmachine_list.html +2 -2
  379. nautobot/virtualization/templates/virtualization/virtualmachine_update.html +3 -1
  380. nautobot/virtualization/tests/test_api.py +3 -0
  381. nautobot/virtualization/tests/test_models.py +44 -4
  382. nautobot/vpn/__init__.py +0 -0
  383. nautobot/vpn/api/serializers.py +113 -0
  384. nautobot/vpn/api/urls.py +19 -0
  385. nautobot/vpn/api/views.py +70 -0
  386. nautobot/vpn/apps.py +8 -0
  387. nautobot/vpn/choices.py +171 -0
  388. nautobot/vpn/factory.py +209 -0
  389. nautobot/vpn/filters.py +233 -0
  390. nautobot/vpn/forms.py +486 -0
  391. nautobot/vpn/homepage.py +19 -0
  392. nautobot/vpn/migrations/0001_initial.py +541 -0
  393. nautobot/vpn/migrations/0002_populate_defaults.py +199 -0
  394. nautobot/vpn/migrations/__init__.py +0 -0
  395. nautobot/vpn/models.py +527 -0
  396. nautobot/vpn/navigation.py +98 -0
  397. nautobot/vpn/tables.py +380 -0
  398. nautobot/vpn/templates/vpn/vpnprofile.html +2 -0
  399. nautobot/vpn/templates/vpn/vpnprofile_create.html +150 -0
  400. nautobot/vpn/tests/__init__.py +0 -0
  401. nautobot/vpn/tests/test_api.py +341 -0
  402. nautobot/vpn/tests/test_filters.py +139 -0
  403. nautobot/vpn/tests/test_forms.py +294 -0
  404. nautobot/vpn/tests/test_models.py +97 -0
  405. nautobot/vpn/tests/test_views.py +281 -0
  406. nautobot/vpn/urls.py +16 -0
  407. nautobot/vpn/views.py +437 -0
  408. nautobot/wireless/navigation.py +3 -2
  409. nautobot/wireless/tests/integration/test_radio_profile.py +1 -5
  410. nautobot/wireless/tests/test_api.py +1 -1
  411. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/METADATA +14 -14
  412. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/RECORD +417 -366
  413. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/entry_points.txt +1 -0
  414. nautobot/data_validation/template_content.py +0 -42
  415. nautobot/dcim/filters/mixins.py +0 -354
  416. nautobot/ipam/templates/ipam/inc/prefix_header_extra_content_table.html +0 -4
  417. /nautobot/tenancy/{filters/mixins.py → filter_mixins.py} +0 -0
  418. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/LICENSE.txt +0 -0
  419. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/NOTICE +0 -0
  420. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/WHEEL +0 -0
@@ -10,7 +10,7 @@ from django.contrib.auth import get_user_model
10
10
  from django.contrib.auth.models import Group
11
11
  from django.contrib.contenttypes.models import ContentType
12
12
  from django.core.files.uploadedfile import SimpleUploadedFile
13
- from django.test import override_settings
13
+ from django.test import override_settings, tag
14
14
  from django.urls import reverse
15
15
  from django.utils.timezone import make_aware, now
16
16
  from rest_framework import status
@@ -144,7 +144,7 @@ class ApprovalWorkflowStageTest(
144
144
 
145
145
  cls.approval_workflow_definitions = [
146
146
  ApprovalWorkflowDefinition.objects.create(
147
- name=f"Test Approval Workflow {i}", model_content_type=cls.scheduledjob_ct, priority=i
147
+ name=f"Test Approval Workflow {i}", model_content_type=cls.scheduledjob_ct, weight=i
148
148
  )
149
149
  for i in range(4)
150
150
  ]
@@ -160,7 +160,7 @@ class ApprovalWorkflowStageTest(
160
160
  cls.approval_workflow_stage_definitions = [
161
161
  ApprovalWorkflowStageDefinition.objects.create(
162
162
  approval_workflow_definition=cls.approval_workflow_definitions[i],
163
- weight=i * 100,
163
+ sequence=i * 100,
164
164
  name=f"Test Approval Workflow Stage {i} Definition",
165
165
  min_approvers=1,
166
166
  denial_message="Stage Denial Message",
@@ -403,7 +403,7 @@ class ApprovalWorkflowStageTest(
403
403
 
404
404
  approval_workflow_stage_definition_2 = ApprovalWorkflowStageDefinition.objects.create(
405
405
  approval_workflow_definition=approval_workflow.approval_workflow_definition,
406
- weight=200,
406
+ sequence=200,
407
407
  name="Approval Workflow Stage Definition 2",
408
408
  min_approvers=1,
409
409
  denial_message="Stage 2 Denial Message",
@@ -496,7 +496,7 @@ class ApprovalWorkflowStageTest(
496
496
 
497
497
  approval_workflow_stage_definition_2 = ApprovalWorkflowStageDefinition.objects.create(
498
498
  approval_workflow_definition=approval_workflow.approval_workflow_definition,
499
- weight=200,
499
+ sequence=200,
500
500
  name="Approval Workflow Stage Definition 2",
501
501
  min_approvers=1,
502
502
  denial_message="Stage 2 Denial Message",
@@ -645,7 +645,7 @@ class ApprovalWorkflowStageTest(
645
645
  approver_group_2 = Group.objects.create(name="Approver Group 2")
646
646
  approval_workflow_stage_definition_approver_group_2 = ApprovalWorkflowStageDefinition.objects.create(
647
647
  approval_workflow_definition=self.approval_workflow_definitions[3],
648
- weight=100,
648
+ sequence=100,
649
649
  name="Test Approval Workflow Stage 1 Definition",
650
650
  min_approvers=1,
651
651
  denial_message="Stage Denial Message",
@@ -986,6 +986,9 @@ class ContactTest(APIViewTestCases.APIViewTestCase):
986
986
  bulk_update_data = {
987
987
  "address": "Carnegie Hall, New York, NY",
988
988
  }
989
+ validation_excluded_fields = [
990
+ "teams", # M2M field, excluded by default
991
+ ]
989
992
 
990
993
  @classmethod
991
994
  def setUpTestData(cls):
@@ -1793,6 +1796,7 @@ class GitRepositoryTest(APIViewTestCases.APIViewTestCase):
1793
1796
  self.assertEqual(response.data["message"], f"Repository {self.repos[0].name} sync job added to queue.")
1794
1797
  self.assertIsInstance(response.data["job_result"], dict)
1795
1798
 
1799
+ @tag("example_app")
1796
1800
  def test_create_with_app_provided_contents(self):
1797
1801
  """Test that `provided_contents` published by an App works."""
1798
1802
  self.add_permissions("extras.add_gitrepository")
@@ -2215,7 +2219,7 @@ class JobTest(
2215
2219
  ApprovalWorkflowDefinition.objects.create(
2216
2220
  name="Test Approval Workflow Definition 1",
2217
2221
  model_content_type=ContentType.objects.get_for_model(ScheduledJob),
2218
- priority=0,
2222
+ weight=0,
2219
2223
  )
2220
2224
 
2221
2225
  # Do the stuff.
@@ -2481,13 +2485,14 @@ class JobTest(
2481
2485
  "Unable to schedule job: Job may have sensitive input variables",
2482
2486
  )
2483
2487
 
2488
+ @tag("example_app")
2484
2489
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2485
2490
  @mock.patch("nautobot.extras.api.views.get_worker_count")
2486
2491
  def test_run_a_job_with_sensitive_variables_when_approval_workflow_defined(self, mock_get_worker_count):
2487
2492
  ApprovalWorkflowDefinition.objects.create(
2488
2493
  name="Test Approval Workflow Definition 1",
2489
2494
  model_content_type=ContentType.objects.get_for_model(ScheduledJob),
2490
- priority=0,
2495
+ weight=0,
2491
2496
  )
2492
2497
 
2493
2498
  mock_get_worker_count.return_value = 1
@@ -2702,7 +2707,7 @@ class JobTest(
2702
2707
  ApprovalWorkflowDefinition.objects.create(
2703
2708
  name="Test Approval Workflow Definition 1",
2704
2709
  model_content_type=ContentType.objects.get_for_model(ScheduledJob),
2705
- priority=0,
2710
+ weight=0,
2706
2711
  )
2707
2712
 
2708
2713
  mock_get_worker_count.return_value = 1
@@ -2802,7 +2807,7 @@ class JobTest(
2802
2807
  ApprovalWorkflowDefinition.objects.create(
2803
2808
  name="Approval Definition",
2804
2809
  model_content_type=ContentType.objects.get_for_model(ScheduledJob),
2805
- priority=0,
2810
+ weight=0,
2806
2811
  )
2807
2812
 
2808
2813
  start_time = now() + timedelta(minutes=1)
@@ -3274,6 +3279,7 @@ class UserSavedViewAssociationTest(APIViewTestCases.APIViewTestCase):
3274
3279
  class ScheduledJobTest(
3275
3280
  APIViewTestCases.GetObjectViewTestCase,
3276
3281
  APIViewTestCases.ListObjectsViewTestCase,
3282
+ APIViewTestCases.DeleteObjectViewTestCase,
3277
3283
  ):
3278
3284
  model = ScheduledJob
3279
3285
  choices_fields = []
@@ -4744,9 +4750,9 @@ class TagTest(APIViewTestCases.APIViewTestCase):
4744
4750
  data = {**self.create_data[0], "content_types": [Manufacturer._meta.label_lower]}
4745
4751
  response = self.client.post(self._get_list_url(), data, format="json", **self.header)
4746
4752
 
4747
- tag = Tag.objects.filter(name=data["name"])
4753
+ tags = Tag.objects.filter(name=data["name"])
4748
4754
  self.assertHttpStatus(response, 400)
4749
- self.assertFalse(tag.exists())
4755
+ self.assertFalse(tags.exists())
4750
4756
  self.assertIn(f"Invalid content type: {Manufacturer._meta.label_lower}", response.data["content_types"])
4751
4757
 
4752
4758
  def test_create_tags_without_content_types(self):
@@ -4783,9 +4789,9 @@ class TagTest(APIViewTestCases.APIViewTestCase):
4783
4789
  """Test updating a tag without changing its content-types."""
4784
4790
  self.add_permissions("extras.change_tag")
4785
4791
 
4786
- tag = Tag.objects.exclude(content_types=ContentType.objects.get_for_model(Location)).first()
4787
- tag_content_types = list(tag.content_types.all())
4788
- url = self._get_detail_url(tag)
4792
+ tag_instance = Tag.objects.exclude(content_types=ContentType.objects.get_for_model(Location)).first()
4793
+ tag_content_types = list(tag_instance.content_types.all())
4794
+ url = self._get_detail_url(tag_instance)
4789
4795
  data = {"color": ColorChoices.COLOR_LIME}
4790
4796
 
4791
4797
  response = self.client.patch(url, data, format="json", **self.header)
@@ -4795,9 +4801,9 @@ class TagTest(APIViewTestCases.APIViewTestCase):
4795
4801
  sorted(response.data["content_types"]), sorted([f"{ct.app_label}.{ct.model}" for ct in tag_content_types])
4796
4802
  )
4797
4803
 
4798
- tag.refresh_from_db()
4799
- self.assertEqual(tag.color, ColorChoices.COLOR_LIME)
4800
- self.assertEqual(list(tag.content_types.all()), tag_content_types)
4804
+ tag_instance.refresh_from_db()
4805
+ self.assertEqual(tag_instance.color, ColorChoices.COLOR_LIME)
4806
+ self.assertEqual(list(tag_instance.content_types.all()), tag_content_types)
4801
4807
 
4802
4808
 
4803
4809
  #
@@ -4810,6 +4816,9 @@ class TeamTest(APIViewTestCases.APIViewTestCase):
4810
4816
  bulk_update_data = {
4811
4817
  "address": "Carnegie Hall, New York, NY",
4812
4818
  }
4819
+ validation_excluded_fields = [
4820
+ "contacts", # M2M field, excluded by default
4821
+ ]
4813
4822
 
4814
4823
  @classmethod
4815
4824
  def setUpTestData(cls):
@@ -39,31 +39,31 @@ class ApprovalWorkflowTestMixin:
39
39
  cls.approval_workflow_def_1 = models.ApprovalWorkflowDefinition.objects.create(
40
40
  name="Test Approval Workflow Definition 1",
41
41
  model_content_type=scheduled_job_ct,
42
- priority=0,
42
+ weight=0,
43
43
  )
44
44
  cls.approval_workflow_def_2 = models.ApprovalWorkflowDefinition.objects.create(
45
45
  name="Test Approval Workflow Definition 2",
46
46
  model_content_type=scheduled_job_ct,
47
47
  model_constraints={"name": "Bulk Delete Objects"},
48
- priority=1,
48
+ weight=1,
49
49
  )
50
50
  cls.approval_workflow_def_3 = models.ApprovalWorkflowDefinition.objects.create(
51
51
  name="Test Approval Workflow Definition 3",
52
52
  model_content_type=scheduled_job_ct,
53
53
  model_constraints={"name": "Bulk Delete Objects"},
54
- priority=2,
54
+ weight=2,
55
55
  )
56
56
  cls.approval_workflow_def_4 = models.ApprovalWorkflowDefinition.objects.create(
57
57
  name="Test Approval Workflow Definition 4",
58
58
  model_content_type=scheduled_job_ct,
59
59
  model_constraints={"name": "Bulk Delete Objects"},
60
- priority=3,
60
+ weight=3,
61
61
  )
62
62
  cls.approval_workflow_def_5 = models.ApprovalWorkflowDefinition.objects.create(
63
63
  name="Test Approval Workflow Definition 5",
64
64
  model_content_type=scheduled_job_ct,
65
65
  model_constraints={"name": "Bulk Delete Objects"},
66
- priority=4,
66
+ weight=4,
67
67
  )
68
68
  cls.approval_workflow_1_instance_1 = models.ApprovalWorkflow.objects.create(
69
69
  approval_workflow_definition=cls.approval_workflow_def_1,
@@ -97,7 +97,7 @@ class ApprovalWorkflowTestMixin:
97
97
  )
98
98
  cls.approval_workflow_1_stage_def_1 = models.ApprovalWorkflowStageDefinition.objects.create(
99
99
  approval_workflow_definition=cls.approval_workflow_def_1,
100
- weight=100,
100
+ sequence=100,
101
101
  name="Test Approval Workflow 1 Stage 1",
102
102
  min_approvers=2,
103
103
  denial_message="Stage 1 Denial Message",
@@ -105,7 +105,7 @@ class ApprovalWorkflowTestMixin:
105
105
  )
106
106
  cls.approval_workflow_1_stage_def_2 = models.ApprovalWorkflowStageDefinition.objects.create(
107
107
  approval_workflow_definition=cls.approval_workflow_def_1,
108
- weight=200,
108
+ sequence=200,
109
109
  name="Test Approval Workflow 1 Stage 2",
110
110
  min_approvers=2,
111
111
  denial_message="Stage 2 Denial Message",
@@ -113,7 +113,7 @@ class ApprovalWorkflowTestMixin:
113
113
  )
114
114
  cls.approval_workflow_1_stage_def_3 = models.ApprovalWorkflowStageDefinition.objects.create(
115
115
  approval_workflow_definition=cls.approval_workflow_def_1,
116
- weight=300,
116
+ sequence=300,
117
117
  name="Test Approval Workflow 1 Stage 3",
118
118
  min_approvers=2,
119
119
  denial_message="Stage 3 Denial Message",
@@ -121,7 +121,7 @@ class ApprovalWorkflowTestMixin:
121
121
  )
122
122
  cls.approval_workflow_1_stage_def_4 = models.ApprovalWorkflowStageDefinition.objects.create(
123
123
  approval_workflow_definition=cls.approval_workflow_def_1,
124
- weight=400,
124
+ sequence=400,
125
125
  name="Test Approval Workflow 1 Stage 4",
126
126
  min_approvers=2,
127
127
  denial_message="Stage 4 Denial Message",
@@ -129,7 +129,7 @@ class ApprovalWorkflowTestMixin:
129
129
  )
130
130
  cls.approval_workflow_1_stage_def_5 = models.ApprovalWorkflowStageDefinition.objects.create(
131
131
  approval_workflow_definition=cls.approval_workflow_def_1,
132
- weight=500,
132
+ sequence=500,
133
133
  name="Test Approval Workflow 1 Stage 5",
134
134
  min_approvers=2,
135
135
  denial_message="Stage 5 Denial Message",
@@ -137,7 +137,7 @@ class ApprovalWorkflowTestMixin:
137
137
  )
138
138
  cls.approval_workflow_1_stage_def_6 = models.ApprovalWorkflowStageDefinition.objects.create(
139
139
  approval_workflow_definition=cls.approval_workflow_def_1,
140
- weight=600,
140
+ sequence=600,
141
141
  name="Test Approval Workflow 1 Stage 6",
142
142
  min_approvers=2,
143
143
  denial_message="Stage 6 Denial Message",
@@ -145,7 +145,7 @@ class ApprovalWorkflowTestMixin:
145
145
  )
146
146
  cls.approval_workflow_2_stage_def_1 = models.ApprovalWorkflowStageDefinition.objects.create(
147
147
  approval_workflow_definition=cls.approval_workflow_def_2,
148
- weight=100,
148
+ sequence=100,
149
149
  name="Test Approval Workflow 2 Stage 1",
150
150
  min_approvers=2,
151
151
  denial_message="Stage 1 Denial Message",
@@ -153,7 +153,7 @@ class ApprovalWorkflowTestMixin:
153
153
  )
154
154
  cls.approval_workflow_2_stage_def_2 = models.ApprovalWorkflowStageDefinition.objects.create(
155
155
  approval_workflow_definition=cls.approval_workflow_def_2,
156
- weight=200,
156
+ sequence=200,
157
157
  name="Test Approval Workflow 2 Stage 2",
158
158
  min_approvers=2,
159
159
  denial_message="Stage 2 Denial Message",
@@ -161,7 +161,7 @@ class ApprovalWorkflowTestMixin:
161
161
  )
162
162
  cls.approval_workflow_2_stage_def_3 = models.ApprovalWorkflowStageDefinition.objects.create(
163
163
  approval_workflow_definition=cls.approval_workflow_def_2,
164
- weight=300,
164
+ sequence=300,
165
165
  name="Test Approval Workflow 2 Stage 3",
166
166
  min_approvers=2,
167
167
  denial_message="Stage 3 Denial Message",
@@ -232,19 +232,19 @@ class ApprovalWorkflowDefinitionAPITest(ApprovalWorkflowTestMixin, APIViewTestCa
232
232
  "name": "Approval Workflow Definition 1",
233
233
  "model_content_type": "extras.scheduledjob",
234
234
  "model_constraints": {"name": "Bulk Delete Objects"},
235
- "priority": 5,
235
+ "weight": 5,
236
236
  },
237
237
  {
238
238
  "name": "Approval Workflow Definition 2",
239
239
  "model_content_type": "extras.scheduledjob",
240
240
  "model_constraints": {},
241
- "priority": 6,
241
+ "weight": 6,
242
242
  },
243
243
  {
244
244
  "name": "Approval Workflow Definition 3",
245
245
  "model_content_type": "extras.scheduledjob",
246
246
  "model_constraints": {"name": "Bulk Delete Objects"},
247
- "priority": 7,
247
+ "weight": 7,
248
248
  },
249
249
  ]
250
250
 
@@ -271,18 +271,18 @@ class ApprovalWorkflowDefinitionManagerTest(TestCase):
271
271
  models.ApprovalWorkflowDefinition.objects.create(
272
272
  name=f"Test Approval Workflow Definition {i}",
273
273
  model_content_type=scheduled_job_ct,
274
- priority=i,
274
+ weight=len(range(4)) - 1 - i, # first with highest weight
275
275
  )
276
276
  for i in range(4)
277
277
  ]
278
278
 
279
279
  def test_find_for_model(self):
280
- """Test that the workflow definition with the highest priority and no constraints is returned."""
280
+ """Test that the workflow definition with the highest weight and no constraints is returned."""
281
281
  self.assertEqual(
282
282
  models.ApprovalWorkflowDefinition.objects.find_for_model(self.scheduled_job),
283
283
  self.approval_workflow_defs[0],
284
284
  )
285
- self.assertEqual(self.approval_workflow_defs[0].priority, 0)
285
+ self.assertEqual(self.approval_workflow_defs[0].weight, 3)
286
286
 
287
287
  def test_find_for_model_with_filter_match_constraints(self):
288
288
  """Test that a workflow definition with filter matching constraints is correctly returned."""
@@ -294,7 +294,7 @@ class ApprovalWorkflowDefinitionManagerTest(TestCase):
294
294
  models.ApprovalWorkflowDefinition.objects.find_for_model(self.scheduled_job),
295
295
  self.approval_workflow_defs[0],
296
296
  )
297
- self.assertEqual(self.approval_workflow_defs[0].priority, 0)
297
+ self.assertEqual(self.approval_workflow_defs[0].weight, 3)
298
298
 
299
299
  export_job_model = models.Job.objects.get_for_class_path(ExportObjectList.class_path)
300
300
  export_scheduled_job = models.ScheduledJob.objects.create(
@@ -309,7 +309,7 @@ class ApprovalWorkflowDefinitionManagerTest(TestCase):
309
309
  models.ApprovalWorkflowDefinition.objects.find_for_model(export_scheduled_job),
310
310
  self.approval_workflow_defs[0],
311
311
  )
312
- self.assertEqual(self.approval_workflow_defs[0].priority, 0)
312
+ self.assertEqual(self.approval_workflow_defs[0].weight, 3)
313
313
 
314
314
  def test_find_for_model_with_exact_match_constraints(self):
315
315
  """Test that a workflow definition with exact matching constraints is correctly returned."""
@@ -319,12 +319,12 @@ class ApprovalWorkflowDefinitionManagerTest(TestCase):
319
319
  models.ApprovalWorkflowDefinition.objects.find_for_model(self.scheduled_job),
320
320
  self.approval_workflow_defs[0],
321
321
  )
322
- self.assertEqual(self.approval_workflow_defs[0].priority, 0)
322
+ self.assertEqual(self.approval_workflow_defs[0].weight, 3)
323
323
 
324
- def test_find_for_model_returns_highest_priority_when_all_match(self):
324
+ def test_find_for_model_returns_highest_weight_when_all_match(self):
325
325
  """
326
326
  Test that when all workflow definitions match the model instance,
327
- the one with the highest priority (lowest number) is returned.
327
+ the one with the highest weight is returned.
328
328
  """
329
329
  # Set all definitions to have matching constraints
330
330
  for definition in self.approval_workflow_defs:
@@ -335,14 +335,14 @@ class ApprovalWorkflowDefinitionManagerTest(TestCase):
335
335
  models.ApprovalWorkflowDefinition.objects.find_for_model(self.scheduled_job),
336
336
  self.approval_workflow_defs[0],
337
337
  )
338
- self.assertEqual(self.approval_workflow_defs[0].priority, 0)
338
+ self.assertEqual(self.approval_workflow_defs[0].weight, 3)
339
339
 
340
340
  def test_find_for_model_skips_unmatched_constraints_and_returns_next_without_constraints(self):
341
341
  """
342
- Test that if the highest priority workflow definition has unmatched constraints,
342
+ Test that if the highest weight workflow definition has unmatched constraints,
343
343
  the method skips it and returns the next one with no constraints.
344
344
  """
345
- # Set constraints on the highest priority definition that do not match the instance
345
+ # Set constraints on the highest weight definition that do not match the instance
346
346
  self.approval_workflow_defs[0].model_constraints = {"name": "Non Matching Name"}
347
347
  self.approval_workflow_defs[0].save()
348
348
 
@@ -354,12 +354,12 @@ class ApprovalWorkflowDefinitionManagerTest(TestCase):
354
354
  models.ApprovalWorkflowDefinition.objects.find_for_model(self.scheduled_job),
355
355
  self.approval_workflow_defs[1],
356
356
  )
357
- self.assertEqual(self.approval_workflow_defs[1].priority, 1)
357
+ self.assertEqual(self.approval_workflow_defs[1].weight, 2)
358
358
 
359
- def test_find_for_model_matches_lower_priority_if_higher_fails(self):
359
+ def test_find_for_model_matches_lower_weight_if_higher_fails(self):
360
360
  """
361
- Test that if the highest priority workflow definition's constraints don't match,
362
- the next matching lower-priority definition is returned.
361
+ Test that if the highest weight workflow definition's constraints don't match,
362
+ the next matching lower-weight definition is returned.
363
363
  """
364
364
  self.approval_workflow_defs[0].model_constraints = {"name": "Non Matching Name"}
365
365
  self.approval_workflow_defs[0].save()
@@ -371,7 +371,7 @@ class ApprovalWorkflowDefinitionManagerTest(TestCase):
371
371
  models.ApprovalWorkflowDefinition.objects.find_for_model(self.scheduled_job),
372
372
  self.approval_workflow_defs[1],
373
373
  )
374
- self.assertEqual(self.approval_workflow_defs[1].priority, 1)
374
+ self.assertEqual(self.approval_workflow_defs[1].weight, 2)
375
375
 
376
376
  def test_find_for_model_ignores_constraints_matching_different_instance(self):
377
377
  """
@@ -412,7 +412,7 @@ class ApprovalWorkflowStageDefinitionAPITest(ApprovalWorkflowTestMixin, APIViewT
412
412
  cls.create_data = [
413
413
  {
414
414
  "approval_workflow_definition": cls.approval_workflow_def_2.pk,
415
- "weight": 400,
415
+ "sequence": 400,
416
416
  "name": "Test Approval Workflow 2 Stage 4 Definition",
417
417
  "min_approvers": 3,
418
418
  "denial_message": "Stage 4 Denial Message",
@@ -420,7 +420,7 @@ class ApprovalWorkflowStageDefinitionAPITest(ApprovalWorkflowTestMixin, APIViewT
420
420
  },
421
421
  {
422
422
  "approval_workflow_definition": cls.approval_workflow_def_2.pk,
423
- "weight": 500,
423
+ "sequence": 500,
424
424
  "name": "Test Approval Workflow 2 Stage 5 Definition",
425
425
  "min_approvers": 2,
426
426
  "denial_message": "Stage 5 Denial Message",
@@ -428,7 +428,7 @@ class ApprovalWorkflowStageDefinitionAPITest(ApprovalWorkflowTestMixin, APIViewT
428
428
  },
429
429
  {
430
430
  "approval_workflow_definition": cls.approval_workflow_def_2.pk,
431
- "weight": 600,
431
+ "sequence": 600,
432
432
  "name": "Test Approval Workflow 2 Stage 6 Definition",
433
433
  "min_approvers": 1,
434
434
  "denial_message": "Stage 6 Denial Message",
@@ -437,7 +437,7 @@ class ApprovalWorkflowStageDefinitionAPITest(ApprovalWorkflowTestMixin, APIViewT
437
437
  ]
438
438
 
439
439
  cls.update_data = {
440
- "weight": 700,
440
+ "sequence": 700,
441
441
  "approval_workflow_definition": cls.approval_workflow_def_2.pk,
442
442
  "min_approvers": 4,
443
443
  "denial_message": "Updated Denial Message",
@@ -505,7 +505,7 @@ class ApprovalWorkflowTriggerAPITest(APITestCase):
505
505
  )
506
506
  models.ApprovalWorkflowStageDefinition.objects.create(
507
507
  approval_workflow_definition=self.approval_workflow_def,
508
- weight=100,
508
+ sequence=100,
509
509
  name="Test Approval Workflow Stage 1",
510
510
  min_approvers=2,
511
511
  denial_message="Stage 1 Denial Message",
@@ -1,5 +1,7 @@
1
+ import uuid
2
+
1
3
  from django.contrib.contenttypes.models import ContentType
2
- from django.test import override_settings
4
+ from django.test import override_settings, tag
3
5
  from django.urls import reverse
4
6
  from django.utils.html import escape
5
7
  from rest_framework import status
@@ -22,8 +24,6 @@ from nautobot.extras.models import CustomField, CustomFieldChoice, DynamicGroup,
22
24
  from nautobot.ipam.models import VLAN, VLANGroup
23
25
  from nautobot.virtualization.models import Cluster, ClusterType, VirtualMachine, VMInterface
24
26
 
25
- from example_app.signals import EXAMPLE_APP_CUSTOM_FIELD_DEFAULT, EXAMPLE_APP_CUSTOM_FIELD_NAME
26
-
27
27
 
28
28
  class ChangeLogViewTest(ModelViewTestCase):
29
29
  model = Location
@@ -234,6 +234,32 @@ class ChangeLogViewTest(ModelViewTestCase):
234
234
  self.assertContains(resp, escape('"description": "changed description2"'))
235
235
  self.assertContains(resp, escape('"description": "changed description3"'))
236
236
 
237
+ def test_objectchange_skips_add_conditional_prefetch(self):
238
+ """
239
+ Test that ObjectChange.objects.all() skips prefetch_related on ContentTypes without a model class.
240
+ """
241
+ self.add_permissions("extras.view_objectchange")
242
+
243
+ ct = ContentType.objects.create(app_label="nonexistent_app", model="nonexistentmodel")
244
+ oc = ObjectChange.objects.create(
245
+ changed_object_type=ct,
246
+ changed_object_id=1,
247
+ object_repr="nonexistentobject",
248
+ action=ObjectChangeActionChoices.ACTION_CREATE,
249
+ user=self.user,
250
+ object_data={},
251
+ request_id=uuid.uuid4(),
252
+ )
253
+ url = reverse("extras:objectchange_list")
254
+ with self.assertLogs(level="WARNING") as cm:
255
+ response = self.client.get(url)
256
+ self.assertHttpStatus(response, 200)
257
+ self.assertContains(response, oc.object_repr)
258
+ self.assertIn(
259
+ ("One or more ContentType entries in the database are invalid."),
260
+ cm.output[0],
261
+ )
262
+
237
263
 
238
264
  class ChangeLogAPITest(APITestCase):
239
265
  def setUp(self):
@@ -260,7 +286,10 @@ class ChangeLogAPITest(APITestCase):
260
286
  self.tags = Tag.objects.get_for_model(Location)
261
287
  self.statuses = Status.objects.get_for_model(Location)
262
288
 
289
+ @tag("example_app")
263
290
  def test_create_object(self):
291
+ from example_app.signals import EXAMPLE_APP_CUSTOM_FIELD_DEFAULT, EXAMPLE_APP_CUSTOM_FIELD_NAME
292
+
264
293
  location_type = LocationType.objects.get(name="Campus")
265
294
  data = {
266
295
  "name": "Test Location 1",
@@ -290,8 +319,11 @@ class ChangeLogAPITest(APITestCase):
290
319
  self.assertEqual(oc.object_data["tags"], sorted([self.tags[0].name, self.tags[1].name]))
291
320
  self.assertEqual(oc.user_id, self.user.pk)
292
321
 
322
+ @tag("example_app")
293
323
  def test_update_object(self):
294
324
  """Test PUT with changelogs."""
325
+ from example_app.signals import EXAMPLE_APP_CUSTOM_FIELD_DEFAULT, EXAMPLE_APP_CUSTOM_FIELD_NAME
326
+
295
327
  location_type = LocationType.objects.get(name="Campus")
296
328
  location = Location.objects.create(
297
329
  name="Test Location 1",
@@ -1,11 +1,11 @@
1
1
  import json
2
2
  import logging
3
3
 
4
- from django.conf import settings
5
4
  from django.contrib.contenttypes.models import ContentType
6
5
  from django.core.exceptions import ValidationError
7
6
  from django.db.models import ProtectedError, QuerySet
8
7
  from django.forms import ChoiceField, IntegerField, NumberInput
8
+ from django.test import tag
9
9
  from django.urls import reverse
10
10
  from rest_framework import status
11
11
 
@@ -31,6 +31,7 @@ from nautobot.virtualization.models import VirtualMachine
31
31
  # TODO: this needs to be both a BaseModelTestCase (as it tests the model class) and a (views) TestCase,
32
32
  # (due to the test_multi_select_field_value_after_bulk_update() test).
33
33
  # At some point we should probably split this into separate classes.
34
+ @tag("example_app")
34
35
  class CustomFieldTest(ModelTestCases.BaseModelTestCase, TestCase):
35
36
  model = CustomField
36
37
 
@@ -397,6 +398,7 @@ class CustomFieldTest(ModelTestCases.BaseModelTestCase, TestCase):
397
398
  self.assertIsInstance(filter_field.widget, NumberInput)
398
399
 
399
400
 
401
+ @tag("example_app")
400
402
  class CustomFieldManagerTest(TestCase):
401
403
  def setUp(self):
402
404
  self.content_type = ContentType.objects.get_for_model(Location)
@@ -569,6 +571,7 @@ class ComputedFieldManagerTestCase(TestCase):
569
571
  self.assertQuerysetEqualAndNotEmpty(qs, listing)
570
572
 
571
573
 
574
+ @tag("example_app")
572
575
  class CustomFieldDataAPITest(APITestCase):
573
576
  """
574
577
  Check that object representations in the REST API include their custom field data.
@@ -693,9 +696,8 @@ class CustomFieldDataAPITest(APITestCase):
693
696
  self.cf_json,
694
697
  ]
695
698
 
696
- if "example_app" in settings.PLUGINS:
697
- self.cf_plugin_field = CustomField.objects.get(key="example_app_auto_custom_field")
698
- self.all_cfs.append(self.cf_plugin_field)
699
+ self.cf_plugin_field = CustomField.objects.get(key="example_app_auto_custom_field")
700
+ self.all_cfs.append(self.cf_plugin_field)
699
701
  self.statuses = Status.objects.get_for_model(Location)
700
702
 
701
703
  # Create some locations
@@ -717,8 +719,7 @@ class CustomFieldDataAPITest(APITestCase):
717
719
  self.cf_markdown.key: "### Hello world!\n\n- Item 1\n- Item 2\n- Item 3",
718
720
  self.cf_json.key: {"hello": "world"},
719
721
  }
720
- if "example_app" in settings.PLUGINS:
721
- self.locations[1]._custom_field_data[self.cf_plugin_field.key] = "Custom value"
722
+ self.locations[1]._custom_field_data[self.cf_plugin_field.key] = "Custom value"
722
723
  self.locations[1].validated_save()
723
724
  self.list_url = reverse("dcim-api:location-list")
724
725
  self.detail_url = reverse("dcim-api:location-detail", kwargs={"pk": self.locations[1].pk})
@@ -792,8 +793,7 @@ class CustomFieldDataAPITest(APITestCase):
792
793
  self.cf_json.key: {"foo": "bar"},
793
794
  },
794
795
  }
795
- if "example_app" in settings.PLUGINS:
796
- data["custom_fields"]["example_app_auto_custom_field"] = "Custom value"
796
+ data["custom_fields"]["example_app_auto_custom_field"] = "Custom value"
797
797
  response = self.client.post(self.list_url, data, format="json", **self.header)
798
798
  self.assertHttpStatus(response, status.HTTP_201_CREATED)
799
799
 
@@ -865,9 +865,8 @@ class CustomFieldDataAPITest(APITestCase):
865
865
  self.cf_markdown.key: "### Heading",
866
866
  self.cf_json.key: {"dict1": {"dict2": {}}},
867
867
  }
868
- if "example_app" in settings.PLUGINS:
869
- self.cf_plugin_field = CustomField.objects.get(key="example_app_auto_custom_field")
870
- custom_field_data[self.cf_plugin_field.key] = "Custom Value"
868
+ self.cf_plugin_field = CustomField.objects.get(key="example_app_auto_custom_field")
869
+ custom_field_data[self.cf_plugin_field.key] = "Custom Value"
871
870
  data = (
872
871
  {
873
872
  "name": "Location 3",
@@ -1169,6 +1168,7 @@ class CustomFieldDataAPITest(APITestCase):
1169
1168
  self.assertContains(response, "Invalid choice", status_code=status.HTTP_400_BAD_REQUEST)
1170
1169
 
1171
1170
 
1171
+ @tag("example_app")
1172
1172
  class CustomFieldImportTest(TestCase):
1173
1173
  """
1174
1174
  Test importing object custom field data along with the object itself.
@@ -2009,20 +2009,29 @@ class CustomFieldFilterTest(TestCase):
2009
2009
  )
2010
2010
 
2011
2011
  def test_filter_multi_select(self):
2012
- self.assertQuerysetEqual(
2012
+ self.assertQuerysetEqualAndNotEmpty(
2013
2013
  self.filterset({"cf_cf9": "Foo"}, self.queryset).qs,
2014
2014
  self.queryset.filter(_custom_field_data__cf9__contains="Foo"),
2015
2015
  )
2016
- self.assertQuerysetEqual(
2016
+ self.assertQuerysetEqualAndNotEmpty(
2017
2017
  self.filterset({"cf_cf9": "Bar"}, self.queryset).qs,
2018
2018
  self.queryset.filter(_custom_field_data__cf9__contains="Bar"),
2019
2019
  )
2020
+ self.assertQuerysetEqualAndNotEmpty(
2021
+ self.filterset({"cf_cf9": ["Foo"]}, self.queryset).qs,
2022
+ self.queryset.filter(_custom_field_data__cf9__contains="Foo"),
2023
+ )
2024
+ self.assertQuerysetEqualAndNotEmpty(
2025
+ self.filterset({"cf_cf9": ["Bar"]}, self.queryset).qs,
2026
+ self.queryset.filter(_custom_field_data__cf9__contains="Bar"),
2027
+ )
2020
2028
  self.assertQuerysetEqualAndNotEmpty( # https://github.com/nautobot/nautobot/issues/5009
2021
2029
  self.filterset({"cf_cf9": str(self.multiselect_choices[0].pk)}, self.queryset).qs,
2022
2030
  self.queryset.filter(_custom_field_data__cf9__contains=self.multiselect_choices[0].value),
2023
2031
  )
2024
2032
 
2025
2033
 
2034
+ @tag("example_app")
2026
2035
  class CustomFieldChoiceTest(ModelTestCases.BaseModelTestCase):
2027
2036
  model = CustomFieldChoice
2028
2037