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
@@ -132,14 +132,14 @@ class ApprovalWorkflowDefinitionViewTestCase(
132
132
  ApprovalWorkflowDefinition.objects.create(
133
133
  name=f"Test Approval Workflow {i}",
134
134
  model_content_type=cls.scheduledjob_ct,
135
- priority=i,
135
+ weight=i,
136
136
  )
137
137
 
138
138
  cls.form_data = {
139
139
  "name": "Test Approval Workflow Definition 5",
140
140
  "model_content_type": cls.scheduledjob_ct.pk,
141
141
  "model_constraints": '{"name": "Bulk Delete Objects"}',
142
- "priority": 5,
142
+ "weight": 5,
143
143
  # These are the "management_form" fields required by the dynamic CustomFieldChoice formsets.
144
144
  "approval_workflow_stage_definitions-TOTAL_FORMS": "0", # Set to 0 so validation succeeds until we need it
145
145
  "approval_workflow_stage_definitions-INITIAL_FORMS": "1",
@@ -161,14 +161,14 @@ class ApprovalWorkflowStageDefinitionViewTestCase(ViewTestCases.PrimaryObjectVie
161
161
  cls.approval_workflow_definition = ApprovalWorkflowDefinition.objects.create(
162
162
  name="Test Approval Workflow Definition 1",
163
163
  model_content_type=cls.scheduledjob_ct,
164
- priority=10,
164
+ weight=10,
165
165
  )
166
166
  cls.approver_group = Group.objects.create(name="Test Group 1")
167
167
  cls.updated_approver_group = Group.objects.create(name="Test Group 2")
168
168
  # Deletable objects
169
169
  ApprovalWorkflowStageDefinition.objects.create(
170
170
  approval_workflow_definition=cls.approval_workflow_definition,
171
- weight=100,
171
+ sequence=100,
172
172
  name="Test Approval Workflow 1 Stage 1 Definition",
173
173
  min_approvers=2,
174
174
  denial_message="Stage 1 Denial Message",
@@ -176,7 +176,7 @@ class ApprovalWorkflowStageDefinitionViewTestCase(ViewTestCases.PrimaryObjectVie
176
176
  )
177
177
  ApprovalWorkflowStageDefinition.objects.create(
178
178
  approval_workflow_definition=cls.approval_workflow_definition,
179
- weight=200,
179
+ sequence=200,
180
180
  name="Test Approval Workflow 1 Stage 2 Definition",
181
181
  min_approvers=3,
182
182
  denial_message="Stage 2 Denial Message",
@@ -184,7 +184,7 @@ class ApprovalWorkflowStageDefinitionViewTestCase(ViewTestCases.PrimaryObjectVie
184
184
  )
185
185
  ApprovalWorkflowStageDefinition.objects.create(
186
186
  approval_workflow_definition=cls.approval_workflow_definition,
187
- weight=300,
187
+ sequence=300,
188
188
  name="Test Approval Workflow 1 Stage 3 Definition",
189
189
  min_approvers=4,
190
190
  denial_message="Stage 3 Denial Message",
@@ -192,7 +192,7 @@ class ApprovalWorkflowStageDefinitionViewTestCase(ViewTestCases.PrimaryObjectVie
192
192
  )
193
193
  ApprovalWorkflowStageDefinition.objects.create(
194
194
  approval_workflow_definition=cls.approval_workflow_definition,
195
- weight=400,
195
+ sequence=400,
196
196
  name="Test Approval Workflow 1 Stage 4 Definition",
197
197
  min_approvers=4,
198
198
  denial_message="Stage 4 Denial Message",
@@ -200,7 +200,7 @@ class ApprovalWorkflowStageDefinitionViewTestCase(ViewTestCases.PrimaryObjectVie
200
200
  )
201
201
  ApprovalWorkflowStageDefinition.objects.create(
202
202
  approval_workflow_definition=cls.approval_workflow_definition,
203
- weight=500,
203
+ sequence=500,
204
204
  name="Test Approval Workflow 1 Stage 5 Definition",
205
205
  min_approvers=4,
206
206
  denial_message="Stage 5 Denial Message",
@@ -209,7 +209,7 @@ class ApprovalWorkflowStageDefinitionViewTestCase(ViewTestCases.PrimaryObjectVie
209
209
 
210
210
  cls.form_data = {
211
211
  "approval_workflow_definition": cls.approval_workflow_definition.pk,
212
- "weight": 600,
212
+ "sequence": 600,
213
213
  "name": "Approval Workflow Stage 1 Definition",
214
214
  "min_approvers": 2,
215
215
  "denial_message": "Stage 1 is denied",
@@ -218,7 +218,7 @@ class ApprovalWorkflowStageDefinitionViewTestCase(ViewTestCases.PrimaryObjectVie
218
218
 
219
219
  cls.update_data = {
220
220
  "approval_workflow_definition": cls.approval_workflow_definition.pk,
221
- "weight": 700,
221
+ "sequence": 700,
222
222
  "name": "Updated approval workflow stage 1",
223
223
  "min_approvers": 3,
224
224
  "denial_message": "updated message",
@@ -226,7 +226,7 @@ class ApprovalWorkflowStageDefinitionViewTestCase(ViewTestCases.PrimaryObjectVie
226
226
  }
227
227
 
228
228
  cls.bulk_edit_data = {
229
- "weight": 800,
229
+ "sequence": 800,
230
230
  "min_approvers": 5,
231
231
  "denial_message": "updated denial message",
232
232
  }
@@ -264,7 +264,7 @@ class ApprovalWorkflowViewTestCase(
264
264
  ]
265
265
  approval_workflow_definitions = [
266
266
  ApprovalWorkflowDefinition.objects.create(
267
- name=f"Test Approval Workflow {i}", model_content_type=cls.scheduledjob_ct, priority=i
267
+ name=f"Test Approval Workflow {i}", model_content_type=cls.scheduledjob_ct, weight=i
268
268
  )
269
269
  for i in range(5)
270
270
  ]
@@ -332,7 +332,7 @@ class ApprovalWorkflowStageViewTestCase(
332
332
  ApprovalWorkflowDefinition.objects.create(
333
333
  name=f"Test Approval Workflow {i}",
334
334
  model_content_type=cls.scheduledjob_ct,
335
- priority=i,
335
+ weight=i,
336
336
  )
337
337
  for i in range(5)
338
338
  ]
@@ -342,7 +342,7 @@ class ApprovalWorkflowStageViewTestCase(
342
342
  cls.approval_workflow_stage_definitions.append(
343
343
  ApprovalWorkflowStageDefinition.objects.create(
344
344
  approval_workflow_definition=approval_workflow_definition,
345
- weight=i * 100,
345
+ sequence=i * 100,
346
346
  name=f"Test Approval Workflow Stage {i} Definition",
347
347
  min_approvers=i + 1,
348
348
  denial_message=f"Stage {i} Denial Message",
@@ -516,7 +516,7 @@ class ApprovalWorkflowStageResponseViewTestCase(
516
516
  ApprovalWorkflowDefinition.objects.create(
517
517
  name=f"Test Approval Workflow {i} Definition",
518
518
  model_content_type=cls.scheduledjob_ct,
519
- priority=i,
519
+ weight=i,
520
520
  )
521
521
  for i in range(5)
522
522
  ]
@@ -526,7 +526,7 @@ class ApprovalWorkflowStageResponseViewTestCase(
526
526
  cls.approval_workflow_stage_definitions.append(
527
527
  ApprovalWorkflowStageDefinition.objects.create(
528
528
  approval_workflow_definition=approval_workflow_definition,
529
- weight=i * 100,
529
+ sequence=i * 100,
530
530
  name=f"Test Approval Workflow Stage {i} Definition",
531
531
  min_approvers=i + 1,
532
532
  denial_message=f"Stage {i} Denial Message",
@@ -2346,6 +2346,7 @@ class SavedViewTest(ModelViewTestCase):
2346
2346
  self.assertIn(str(sv_shared.pk), response_body, msg=response_body)
2347
2347
  self.assertNotIn(str(sv_not_shared.pk), response_body, msg=response_body)
2348
2348
 
2349
+ @tag("example_app")
2349
2350
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2350
2351
  def test_create_saved_views_contain_boolean_filter_params(self):
2351
2352
  """
@@ -3461,7 +3462,7 @@ class JobTestCase(
3461
3462
  ApprovalWorkflowDefinition.objects.create(
3462
3463
  name="Test Approval Workflow Definition 1",
3463
3464
  model_content_type=ContentType.objects.get_for_model(ScheduledJob),
3464
- priority=0,
3465
+ weight=0,
3465
3466
  )
3466
3467
 
3467
3468
  self.add_permissions("extras.run_job")
@@ -3491,7 +3492,7 @@ class JobTestCase(
3491
3492
  ApprovalWorkflowDefinition.objects.create(
3492
3493
  name="Approval Definition",
3493
3494
  model_content_type=ContentType.objects.get_for_model(ScheduledJob),
3494
- priority=0,
3495
+ weight=0,
3495
3496
  )
3496
3497
  data = {
3497
3498
  "_schedule_type": "immediately",
@@ -3513,7 +3514,7 @@ class JobTestCase(
3513
3514
  ApprovalWorkflowDefinition.objects.create(
3514
3515
  name="Approval Definition",
3515
3516
  model_content_type=ContentType.objects.get_for_model(ScheduledJob),
3516
- priority=0,
3517
+ weight=0,
3517
3518
  )
3518
3519
  data = {
3519
3520
  "_schedule_type": "future",
@@ -3574,7 +3575,7 @@ class JobTestCase(
3574
3575
  ApprovalWorkflowDefinition.objects.create(
3575
3576
  name="Approval Definition",
3576
3577
  model_content_type=ContentType.objects.get_for_model(ScheduledJob),
3577
- priority=0,
3578
+ weight=0,
3578
3579
  )
3579
3580
 
3580
3581
  data = {
@@ -3599,7 +3600,7 @@ class JobTestCase(
3599
3600
  ApprovalWorkflowDefinition.objects.create(
3600
3601
  name="Approval Definition",
3601
3602
  model_content_type=ContentType.objects.get_for_model(ScheduledJob),
3602
- priority=0,
3603
+ weight=0,
3603
3604
  )
3604
3605
 
3605
3606
  data = {
@@ -3625,7 +3626,7 @@ class JobTestCase(
3625
3626
  ApprovalWorkflowDefinition.objects.create(
3626
3627
  name="Approval Definition",
3627
3628
  model_content_type=ContentType.objects.get_for_model(ScheduledJob),
3628
- priority=0,
3629
+ weight=0,
3629
3630
  )
3630
3631
  data = {
3631
3632
  "_schedule_type": "future",
@@ -3765,14 +3766,12 @@ class JobButtonRenderingTestCase(TestCase):
3765
3766
 
3766
3767
  self.location_type = LocationType.objects.get(name="Campus")
3767
3768
 
3768
- @tag("fix_in_v3")
3769
3769
  def test_view_object_with_job_button(self):
3770
3770
  """Ensure that the job button is rendered."""
3771
3771
  response = self.client.get(self.location_type.get_absolute_url(), follow=True)
3772
3772
  self.assertBodyContains(response, f"JobButton {self.location_type.name}")
3773
3773
  self.assertBodyContains(response, "Click me!")
3774
3774
 
3775
- @tag("fix_in_v3")
3776
3775
  def test_task_queue_hidden_input_is_present(self):
3777
3776
  """
3778
3777
  Ensure that the job button respects the job class' task_queues and the job class default job queue is passed as a hidden form input.
@@ -3800,7 +3799,6 @@ class JobButtonRenderingTestCase(TestCase):
3800
3799
  f'<input type="hidden" name="_job_queue" value="{self.job.default_job_queue.pk}">', content, content
3801
3800
  )
3802
3801
 
3803
- @tag("fix_in_v3")
3804
3802
  def test_view_object_with_unsafe_text(self):
3805
3803
  """Ensure that JobButton text can't be used as a vector for XSS."""
3806
3804
  self.job_button_1.text = '<script>alert("Hello world!")</script>'
@@ -3820,7 +3818,6 @@ class JobButtonRenderingTestCase(TestCase):
3820
3818
  self.assertNotIn("<script>alert", content, content)
3821
3819
  self.assertIn("&lt;script&gt;alert", content, content)
3822
3820
 
3823
- @tag("fix_in_v3")
3824
3821
  def test_view_object_with_unsafe_name(self):
3825
3822
  """Ensure that JobButton names can't be used as a vector for XSS."""
3826
3823
  self.job_button_1.text = "JobButton {{ obj"
@@ -3832,7 +3829,6 @@ class JobButtonRenderingTestCase(TestCase):
3832
3829
  self.assertNotIn("<script>alert", content, content)
3833
3830
  self.assertIn("&lt;script&gt;alert", content, content)
3834
3831
 
3835
- @tag("fix_in_v3")
3836
3832
  def test_render_constrained_run_permissions(self):
3837
3833
  obj_perm = ObjectPermission(
3838
3834
  name="Test permission",
@@ -3851,8 +3847,9 @@ class JobButtonRenderingTestCase(TestCase):
3851
3847
  NO_CONFIRM_BUTTON.format(
3852
3848
  button_id=self.job_button_1.pk,
3853
3849
  button_text=f"JobButton {self.location_type.name}",
3854
- button_class=self.job_button_1.button_class,
3850
+ button_class=self.job_button_1.button_class_css_class,
3855
3851
  disabled="",
3852
+ menu_item="",
3856
3853
  ),
3857
3854
  content,
3858
3855
  )
@@ -3860,8 +3857,9 @@ class JobButtonRenderingTestCase(TestCase):
3860
3857
  NO_CONFIRM_BUTTON.format(
3861
3858
  button_id=self.job_button_2.pk,
3862
3859
  button_text="Click me!",
3863
- button_class=self.job_button_2.button_class,
3860
+ button_class=self.job_button_2.button_class_css_class,
3864
3861
  disabled="disabled",
3862
+ menu_item="",
3865
3863
  ),
3866
3864
  content,
3867
3865
  )
@@ -3882,6 +3880,7 @@ class JobButtonRenderingTestCase(TestCase):
3882
3880
  button_text=f"JobButton {self.location_type.name}",
3883
3881
  button_class="link",
3884
3882
  disabled="",
3883
+ menu_item="dropdown-item",
3885
3884
  )
3886
3885
  + "</li>",
3887
3886
  content,
@@ -3893,12 +3892,14 @@ class JobButtonRenderingTestCase(TestCase):
3893
3892
  button_text="Click me!",
3894
3893
  button_class="link",
3895
3894
  disabled="disabled",
3895
+ menu_item="dropdown-item",
3896
3896
  )
3897
3897
  + "</li>",
3898
3898
  content,
3899
3899
  )
3900
3900
 
3901
3901
 
3902
+ @tag("example_app")
3902
3903
  class JobCustomTemplateTestCase(TestCase):
3903
3904
  @classmethod
3904
3905
  def setUpTestData(cls):
@@ -4611,7 +4612,7 @@ class RoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase, ViewTestCases
4611
4612
  if result == "Contact Associations":
4612
4613
  # AssociationContact Table in the contact tab should be there.
4613
4614
  self.assertInHTML(
4614
- f'<strong>{result}</strong><div class="pull-right d-print-none">',
4615
+ f'<strong>{result}</strong><div class="float-end d-print-none">',
4615
4616
  response_body,
4616
4617
  )
4617
4618
  # ContactAssociationTable related to this role instances should not be there.
nautobot/extras/views.py CHANGED
@@ -24,6 +24,7 @@ from django_tables2 import RequestConfig
24
24
  from jsonschema.validators import Draft7Validator
25
25
  from rest_framework.decorators import action
26
26
  from rest_framework.permissions import IsAuthenticated
27
+ from rest_framework.response import Response
27
28
 
28
29
  from nautobot.core.choices import ButtonActionColorChoices
29
30
  from nautobot.core.constants import PAGINATE_COUNT_DEFAULT
@@ -90,6 +91,8 @@ from nautobot.ipam.models import IPAddress, Prefix, VLAN
90
91
  from nautobot.ipam.tables import IPAddressTable, PrefixTable, VLANTable
91
92
  from nautobot.virtualization.models import VirtualMachine, VMInterface
92
93
  from nautobot.virtualization.tables import VirtualMachineTable, VMInterfaceTable
94
+ from nautobot.vpn.models import VPN, VPNProfile, VPNTunnel, VPNTunnelEndpoint
95
+ from nautobot.vpn.tables import VPNProfileTable, VPNTable, VPNTunnelEndpointTable, VPNTunnelTable
93
96
 
94
97
  from . import filters, forms, jobs_ui, tables
95
98
  from .api import serializers
@@ -1262,12 +1265,7 @@ class DynamicGroupUIViewSet(NautobotUIViewSet):
1262
1265
  elif self.action == "retrieve":
1263
1266
  model = instance.model
1264
1267
  table_class = get_table_for_model(model)
1265
- if instance.group_type != DynamicGroupTypeChoices.TYPE_STATIC:
1266
- # Ensure that members cache is up-to-date for this specific group
1267
- members = instance.update_cached_members()
1268
- messages.success(request, f"Refreshed cached members list for {instance}")
1269
- else:
1270
- members = instance.members
1268
+ members = instance.members
1271
1269
  if table_class is not None:
1272
1270
  if hasattr(members, "without_tree_fields"):
1273
1271
  members = members.without_tree_fields()
@@ -1322,7 +1320,7 @@ class DynamicGroupUIViewSet(NautobotUIViewSet):
1322
1320
 
1323
1321
  return context
1324
1322
 
1325
- def form_save(self, form, **kwargs):
1323
+ def form_save(self, form, commit=True, **kwargs):
1326
1324
  obj = form.save(commit=False)
1327
1325
  context = self.get_extra_context(self.request, obj)
1328
1326
 
@@ -1341,10 +1339,18 @@ class DynamicGroupUIViewSet(NautobotUIViewSet):
1341
1339
  form.add_error(None, msg)
1342
1340
  raise
1343
1341
 
1344
- # After filters have been set, now we save the object to the database.
1345
- obj.save()
1346
- # Save m2m fields, such as Tags https://docs.djangoproject.com/en/3.2/topics/forms/modelforms/#the-save-method
1347
- form.save_m2m()
1342
+ if commit:
1343
+ # After filters have been set, now we save the object to the database.
1344
+ obj.save(update_cached_members=False)
1345
+ # Save m2m fields, such as Tags https://docs.djangoproject.com/en/3.2/topics/forms/modelforms/#the-save-method
1346
+ form.save_m2m()
1347
+
1348
+ if obj.group_type != DynamicGroupTypeChoices.TYPE_STATIC:
1349
+ messages.warning(
1350
+ self.request,
1351
+ "Dynamic Group membership is not automatically recalculated after creating/editing the group, "
1352
+ 'as it may take some time to complete. You can use the "Refresh Members" button when ready.',
1353
+ )
1348
1354
 
1349
1355
  # Process the formsets for children
1350
1356
  children = context.get("children")
@@ -1359,7 +1365,7 @@ class DynamicGroupUIViewSet(NautobotUIViewSet):
1359
1365
  added_errors.add(msg)
1360
1366
  raise ValidationError("invalid DynamicGroupMembershipFormSet")
1361
1367
 
1362
- if children:
1368
+ if commit and children:
1363
1369
  children.save()
1364
1370
 
1365
1371
  return obj
@@ -1658,6 +1664,7 @@ class GitRepositoryUIViewSet(NautobotUIViewSet):
1658
1664
  filterset_class = filters.GitRepositoryFilterSet
1659
1665
  serializer_class = serializers.GitRepositorySerializer
1660
1666
  table_class = tables.GitRepositoryTable
1667
+ view_titles = Titles(titles={"result": "{{ object.display|default:object }} - Synchronization Status"})
1661
1668
 
1662
1669
  def get_extra_context(self, request, instance=None):
1663
1670
  context = super().get_extra_context(request, instance)
@@ -1701,14 +1708,18 @@ class GitRepositoryUIViewSet(NautobotUIViewSet):
1701
1708
  job_result = instance.get_latest_sync()
1702
1709
 
1703
1710
  context = {
1711
+ **super().get_extra_context(request, instance),
1704
1712
  "result": job_result or {},
1705
- "base_template": "extras/gitrepository.html",
1713
+ "base_template": "extras/gitrepository_retrieve.html",
1706
1714
  "object": instance,
1707
1715
  "active_tab": "result",
1708
1716
  "verbose_name": instance._meta.verbose_name,
1709
1717
  }
1710
1718
 
1711
- return render(request, "extras/gitrepository_result.html", context)
1719
+ return Response(
1720
+ context,
1721
+ template_name="extras/gitrepository_result.html",
1722
+ )
1712
1723
 
1713
1724
  @action(
1714
1725
  detail=True,
@@ -1755,6 +1766,27 @@ class GraphQLQueryUIViewSet(
1755
1766
  table_class = tables.GraphQLQueryTable
1756
1767
  action_buttons = ("add",)
1757
1768
 
1769
+ object_detail_content = object_detail.ObjectDetailContent(
1770
+ panels=(
1771
+ object_detail.ObjectFieldsPanel(
1772
+ label="Query",
1773
+ weight=100,
1774
+ section=SectionChoices.LEFT_HALF,
1775
+ fields=["name", "query", "variables"],
1776
+ value_transforms={
1777
+ "query": [lambda val: format_html('<pre><code class="language-graphql">{}</code></pre>', val)],
1778
+ "variables": [lambda val: helpers.render_json(val, syntax_highlight=True, pretty_print=True)],
1779
+ },
1780
+ ),
1781
+ object_detail.Panel(
1782
+ weight=100,
1783
+ section=object_detail.SectionChoices.RIGHT_HALF,
1784
+ body_content_template_path="extras/inc/graphqlquery_execute.html",
1785
+ body_wrapper_template_path="components/panel/body_wrapper_table.html",
1786
+ ),
1787
+ )
1788
+ )
1789
+
1758
1790
 
1759
1791
  #
1760
1792
  # Image attachments
@@ -2086,7 +2118,7 @@ class JobRunView(ObjectPermissionRequiredMixin, View):
2086
2118
 
2087
2119
  class JobView(generic.ObjectView):
2088
2120
  queryset = JobModel.objects.all()
2089
- template_name = "extras/job_detail.html"
2121
+ template_name = "generic/object_retrieve.html"
2090
2122
  object_detail_content = object_detail.ObjectDetailContent(
2091
2123
  panels=[
2092
2124
  object_detail.ObjectFieldsPanel(
@@ -2701,6 +2733,12 @@ class ObjectChangeUIViewSet(ObjectDetailViewMixin, ObjectListViewMixin):
2701
2733
  table_class = tables.ObjectChangeTable
2702
2734
  action_buttons = ("export",)
2703
2735
 
2736
+ def __init__(self, *args, **kwargs):
2737
+ super().__init__(*args, **kwargs)
2738
+ self.object_detail_content = object_detail.ObjectDetailContent()
2739
+ # Remove "Advanced" tab while keeping the main.
2740
+ self.object_detail_content.tabs = self.object_detail_content.tabs[:1]
2741
+
2704
2742
  # 2.0 TODO: Remove this remapping and solve it at the `BaseFilterSet` as it is addressing a breaking change.
2705
2743
  def get(self, request, *args, **kwargs):
2706
2744
  # Remappings below allow previous queries of time_before and time_after to use
@@ -2789,7 +2827,7 @@ class ObjectChangeLogView(generic.GenericView):
2789
2827
 
2790
2828
  return render(
2791
2829
  request,
2792
- "extras/object_changelog.html",
2830
+ "generic/object_changelog.html",
2793
2831
  {
2794
2832
  "object": obj,
2795
2833
  "verbose_name": obj._meta.verbose_name,
@@ -2996,7 +3034,7 @@ class ObjectNotesView(generic.GenericView):
2996
3034
 
2997
3035
  return render(
2998
3036
  request,
2999
- "extras/object_notes.html",
3037
+ "generic/object_notes.html",
3000
3038
  {
3001
3039
  "object": obj,
3002
3040
  "verbose_name": obj._meta.verbose_name,
@@ -3171,6 +3209,30 @@ class RoleUIViewSet(viewsets.NautobotUIViewSet):
3171
3209
  vdc_table.columns.hide("role")
3172
3210
  RequestConfig(request, paginate).configure(vdc_table)
3173
3211
  context["vdc_table"] = vdc_table
3212
+ if ContentType.objects.get_for_model(VPN) in context["content_types"]:
3213
+ vpns = instance.vpns.restrict(request.user, "view")
3214
+ vpn_table = VPNTable(vpns)
3215
+ vpn_table.columns.hide("role")
3216
+ RequestConfig(request, paginate).configure(vpn_table)
3217
+ context["vpn_table"] = vpn_table
3218
+ if ContentType.objects.get_for_model(VPNProfile) in context["content_types"]:
3219
+ vpn_profiles = instance.vpn_profiles.restrict(request.user, "view")
3220
+ vpn_profile_table = VPNProfileTable(vpn_profiles)
3221
+ vpn_profile_table.columns.hide("role")
3222
+ RequestConfig(request, paginate).configure(vpn_profile_table)
3223
+ context["vpn_profile_table"] = vpn_profile_table
3224
+ if ContentType.objects.get_for_model(VPNTunnel) in context["content_types"]:
3225
+ vpn_tunnels = instance.vpn_tunnels.restrict(request.user, "view")
3226
+ vpn_tunnel_table = VPNTunnelTable(vpn_tunnels)
3227
+ vpn_tunnel_table.columns.hide("role")
3228
+ RequestConfig(request, paginate).configure(vpn_tunnel_table)
3229
+ context["vpn_tunnel_table"] = vpn_tunnel_table
3230
+ if ContentType.objects.get_for_model(VPNTunnelEndpoint) in context["content_types"]:
3231
+ vpn_tunnel_endpoints = instance.vpn_tunnel_endpoints.restrict(request.user, "view")
3232
+ vpn_tunnel_endpoint_table = VPNTunnelEndpointTable(vpn_tunnel_endpoints)
3233
+ vpn_tunnel_endpoint_table.columns.hide("role")
3234
+ RequestConfig(request, paginate).configure(vpn_tunnel_endpoint_table)
3235
+ context["vpn_tunnel_endpoint_table"] = vpn_tunnel_endpoint_table
3174
3236
  return context
3175
3237
 
3176
3238
 
@@ -3490,8 +3552,8 @@ class WebhookUIViewSet(NautobotUIViewSet):
3490
3552
 
3491
3553
 
3492
3554
  class JobObjectChangeLogView(ObjectChangeLogView):
3493
- base_template = "extras/job_detail.html"
3555
+ base_template = "generic/object_retrieve.html"
3494
3556
 
3495
3557
 
3496
3558
  class JobObjectNotesView(ObjectNotesView):
3497
- base_template = "extras/job_detail.html"
3559
+ base_template = "generic/object_retrieve.html"
nautobot/ipam/factory.py CHANGED
@@ -278,9 +278,16 @@ class NamespaceFactory(PrimaryModelFactory):
278
278
 
279
279
  class Meta:
280
280
  model = Namespace
281
+ exclude = ("has_tenant", "has_description")
281
282
 
282
283
  name = UniqueFaker("text", max_nb_chars=20) # could be up to CHARFIELD_MAX_LENGTH but that gets unwieldy fast
283
284
 
285
+ has_tenant = NautobotBoolIterator()
286
+ tenant = factory.Maybe("has_tenant", random_instance(Tenant))
287
+
288
+ has_description = NautobotBoolIterator()
289
+ description = factory.Maybe("has_description", factory.Faker("text", max_nb_chars=CHARFIELD_MAX_LENGTH), "")
290
+
284
291
 
285
292
  class PrefixFactory(PrimaryModelFactory):
286
293
  """Create random Prefix objects with randomized data.
@@ -0,0 +1,38 @@
1
+ import uuid
2
+
3
+ from nautobot.core.filters import NaturalKeyOrPKMultipleChoiceFilter
4
+ from nautobot.ipam import formfields
5
+ from nautobot.ipam.models import Prefix
6
+
7
+
8
+ class PrefixFilter(NaturalKeyOrPKMultipleChoiceFilter):
9
+ """
10
+ Filter that supports filtering a foreign key to Prefix by either its PK or by a literal `prefix` string.
11
+ """
12
+
13
+ field_class = formfields.PrefixFilterFormField
14
+
15
+ def __init__(self, *args, **kwargs):
16
+ kwargs.setdefault("to_field_name", "pk")
17
+ kwargs.setdefault("label", "Prefix (ID or prefix string)")
18
+ kwargs.setdefault("queryset", Prefix.objects.all())
19
+ super().__init__(*args, **kwargs)
20
+
21
+ def get_filter_predicate(self, v):
22
+ # Null value filtering
23
+ if v is None:
24
+ return {f"{self.field_name}__isnull": True}
25
+
26
+ # If value is a model instance, stringify it to a pk.
27
+ if isinstance(v, Prefix):
28
+ v = v.pk
29
+
30
+ # Try to cast the value to a UUID to distinguish between PKs and prefix strings
31
+ v = str(v)
32
+ try:
33
+ uuid.UUID(v)
34
+ return {self.field_name: v}
35
+ except (AttributeError, TypeError, ValueError):
36
+ # It's a prefix string
37
+ prefixes_queryset = Prefix.objects.net_equals(v)
38
+ return {f"{self.field_name}__in": prefixes_queryset.values_list("pk", flat=True)}
nautobot/ipam/filters.py CHANGED
@@ -19,12 +19,15 @@ from nautobot.core.filters import (
19
19
  SearchFilter,
20
20
  TreeNodeMultipleChoiceFilter,
21
21
  )
22
+ from nautobot.core.utils.data import is_uuid
22
23
  from nautobot.dcim.filters import LocatableModelFilterSetMixin
23
24
  from nautobot.dcim.models import Device, Interface, Location, VirtualDeviceContext
24
25
  from nautobot.extras.filters import NautobotFilterSet, RoleModelFilterSetMixin, StatusModelFilterSetMixin
25
- from nautobot.ipam import choices, formfields
26
- from nautobot.tenancy.filters.mixins import TenancyModelFilterSetMixin
26
+ from nautobot.ipam import choices
27
+ from nautobot.ipam.filter_mixins import PrefixFilter
28
+ from nautobot.tenancy.filter_mixins import TenancyModelFilterSetMixin
27
29
  from nautobot.virtualization.models import VirtualMachine, VMInterface
30
+ from nautobot.vpn.models import VPNTunnelEndpoint
28
31
 
29
32
  from .models import (
30
33
  IPAddress,
@@ -57,40 +60,7 @@ __all__ = (
57
60
  )
58
61
 
59
62
 
60
- class PrefixFilter(NaturalKeyOrPKMultipleChoiceFilter):
61
- """
62
- Filter that supports filtering a foreign key to Prefix by either its PK or by a literal `prefix` string.
63
- """
64
-
65
- field_class = formfields.PrefixFilterFormField
66
-
67
- def __init__(self, *args, **kwargs):
68
- kwargs.setdefault("to_field_name", "pk")
69
- kwargs.setdefault("label", "Prefix (ID or prefix string)")
70
- kwargs.setdefault("queryset", Prefix.objects.all())
71
- super().__init__(*args, **kwargs)
72
-
73
- def get_filter_predicate(self, v):
74
- # Null value filtering
75
- if v is None:
76
- return {f"{self.field_name}__isnull": True}
77
-
78
- # If value is a model instance, stringify it to a pk.
79
- if isinstance(v, Prefix):
80
- v = v.pk
81
-
82
- # Try to cast the value to a UUID to distinguish between PKs and prefix strings
83
- v = str(v)
84
- try:
85
- uuid.UUID(v)
86
- return {self.field_name: v}
87
- except (AttributeError, TypeError, ValueError):
88
- # It's a prefix string
89
- prefixes_queryset = Prefix.objects.net_equals(v)
90
- return {f"{self.field_name}__in": prefixes_queryset.values_list("pk", flat=True)}
91
-
92
-
93
- class NamespaceFilterSet(NautobotFilterSet):
63
+ class NamespaceFilterSet(NautobotFilterSet, TenancyModelFilterSetMixin):
94
64
  q = SearchFilter(
95
65
  filter_predicates={
96
66
  "name": "icontains",
@@ -320,13 +290,25 @@ class PrefixFilterSet(
320
290
  queryset=CloudNetwork.objects.all(),
321
291
  to_field_name="name",
322
292
  )
293
+ vpn_tunnel_endpoints = NaturalKeyOrPKMultipleChoiceFilter(
294
+ queryset=VPNTunnelEndpoint.objects.all(),
295
+ to_field_name="pk",
296
+ label="VPN Tunnel Endpoint ID",
297
+ )
323
298
 
324
299
  class Meta:
325
300
  model = Prefix
326
301
  fields = ["date_allocated", "id", "prefix_length", "tags"]
327
302
 
328
303
  def _strip_values(self, values):
329
- return [value.strip() for value in values if value.strip()]
304
+ result = []
305
+ for value in values:
306
+ value = value.strip()
307
+ if is_uuid(value):
308
+ result.append(Prefix.objects.get(pk=value).prefix)
309
+ elif value:
310
+ result.append(value)
311
+ return result
330
312
 
331
313
  def filter_prefix(self, queryset, name, value):
332
314
  prefixes = self._strip_values(value)
@@ -505,7 +487,14 @@ class IPAddressFilterSet(
505
487
  return queryset.filter(params)
506
488
 
507
489
  def search_by_prefix(self, queryset, name, value):
508
- prefixes = [prefix.strip() for prefix in value if prefix.strip()]
490
+ prefixes = []
491
+ for prefix in value:
492
+ prefix = prefix.strip()
493
+ if is_uuid(prefix):
494
+ prefixes.append(Prefix.objects.get(pk=prefix).prefix)
495
+ elif prefix:
496
+ prefixes.append(prefix)
497
+
509
498
  return queryset.net_host_contained(*prefixes)
510
499
 
511
500
  def filter_address(self, queryset, name, value):
@@ -67,7 +67,7 @@ class IPNetworkFormField(forms.Field):
67
67
  class PrefixFilterFormField(MultiMatchModelMultipleChoiceField):
68
68
  @property
69
69
  def filter(self):
70
- from nautobot.ipam.filters import PrefixFilter # avoid circular definition
70
+ from nautobot.ipam.filter_mixins import PrefixFilter # avoid circular definition
71
71
 
72
72
  return PrefixFilter
73
73