nautobot 2.4.21__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 (919) hide show
  1. nautobot/apps/choices.py +2 -2
  2. nautobot/apps/filters.py +9 -9
  3. nautobot/apps/forms.py +2 -0
  4. nautobot/apps/models.py +7 -2
  5. nautobot/apps/ui.py +20 -1
  6. nautobot/apps/utils.py +2 -3
  7. nautobot/apps/views.py +7 -1
  8. nautobot/circuits/filters.py +8 -23
  9. nautobot/circuits/navigation.py +3 -1
  10. nautobot/circuits/templates/circuits/circuit_create.html +9 -9
  11. nautobot/circuits/templates/circuits/circuit_terminations_swap.html +2 -2
  12. nautobot/circuits/templates/circuits/circuittermination_create.html +24 -33
  13. nautobot/circuits/templates/circuits/inc/circuit_termination.html +10 -10
  14. nautobot/circuits/templates/circuits/inc/circuit_termination_cable_fragment.html +13 -13
  15. nautobot/circuits/templates/circuits/inc/circuit_termination_header_extra_content.html +6 -6
  16. nautobot/circuits/templates/circuits/inc/circuit_termination_speed_fragment.html +3 -3
  17. nautobot/circuits/templates/circuits/inc/speed_widget.html +13 -13
  18. nautobot/circuits/templates/circuits/provider_create.html +9 -9
  19. nautobot/circuits/tests/integration/test_circuit.py +19 -19
  20. nautobot/circuits/tests/integration/test_circuits_bulk_operations.py +3 -0
  21. nautobot/circuits/tests/integration/test_relationships.py +4 -12
  22. nautobot/circuits/views.py +0 -2
  23. nautobot/cloud/filters.py +1 -13
  24. nautobot/cloud/navigation.py +3 -1
  25. nautobot/cloud/templates/cloud/cloudnetwork_update.html +9 -9
  26. nautobot/cloud/templates/cloud/cloudservice_update.html +6 -6
  27. nautobot/core/api/fields.py +30 -2
  28. nautobot/core/api/schema.py +1 -1
  29. nautobot/core/api/serializers.py +9 -2
  30. nautobot/core/api/urls.py +2 -0
  31. nautobot/core/api/views.py +58 -37
  32. nautobot/core/apps/__init__.py +6 -12
  33. nautobot/core/branching.py +83 -0
  34. nautobot/core/celery/__init__.py +11 -6
  35. nautobot/core/celery/backends.py +2 -0
  36. nautobot/core/celery/encoders.py +7 -0
  37. nautobot/core/celery/task.py +44 -0
  38. nautobot/core/checks.py +60 -0
  39. nautobot/core/cli/bootstrap_v3_to_v5.py +776 -0
  40. nautobot/core/constants.py +9 -0
  41. nautobot/core/context_processors.py +84 -0
  42. nautobot/core/filters.py +131 -2
  43. nautobot/core/forms/__init__.py +4 -2
  44. nautobot/core/forms/fields.py +10 -8
  45. nautobot/core/forms/forms.py +21 -9
  46. nautobot/core/forms/search.py +0 -15
  47. nautobot/core/forms/widgets.py +3 -2
  48. nautobot/core/graphql/__init__.py +8 -26
  49. nautobot/core/graphql/generators.py +16 -6
  50. nautobot/core/graphql/schema.py +1 -1
  51. nautobot/core/graphql/schema_init.py +1 -2
  52. nautobot/core/graphql/utils.py +7 -9
  53. nautobot/core/jobs/__init__.py +158 -0
  54. nautobot/core/management/commands/generate_test_data.py +28 -9
  55. nautobot/core/models/__init__.py +17 -2
  56. nautobot/core/models/fields.py +3 -2
  57. nautobot/core/models/generics.py +9 -1
  58. nautobot/core/models/name_color_content_types.py +1 -1
  59. nautobot/core/models/ordering.py +7 -5
  60. nautobot/core/models/querysets.py +77 -2
  61. nautobot/core/models/tree_queries.py +6 -4
  62. nautobot/core/settings.py +30 -16
  63. nautobot/core/settings.yaml +13 -7
  64. nautobot/core/tables.py +114 -44
  65. nautobot/core/templates/403.html +1 -1
  66. nautobot/core/templates/403_csrf_failure.html +1 -1
  67. nautobot/core/templates/404.html +1 -1
  68. nautobot/core/templates/40x.html +8 -8
  69. nautobot/core/templates/500.html +10 -10
  70. nautobot/core/templates/about.html +13 -12
  71. nautobot/core/templates/admin/actions.html +1 -1
  72. nautobot/core/templates/admin/app_index.html +3 -3
  73. nautobot/core/templates/admin/base.html +45 -52
  74. nautobot/core/templates/admin/base_site.html +0 -9
  75. nautobot/core/templates/admin/change_form.html +5 -5
  76. nautobot/core/templates/admin/change_list.html +8 -12
  77. nautobot/core/templates/admin/change_list_results.html +3 -3
  78. nautobot/core/templates/admin/config/config.html +24 -24
  79. nautobot/core/templates/admin/delete_confirmation.html +5 -5
  80. nautobot/core/templates/admin/edit_inline/stacked.html +5 -5
  81. nautobot/core/templates/admin/edit_inline/tabular.html +3 -3
  82. nautobot/core/templates/admin/includes/fieldset.html +15 -15
  83. nautobot/core/templates/admin/index.html +8 -8
  84. nautobot/core/templates/admin/submit_line.html +5 -5
  85. nautobot/core/templates/base_django.html +36 -32
  86. nautobot/core/templates/buttons/add.html +1 -1
  87. nautobot/core/templates/buttons/consolidated_detail_view_action_buttons.html +2 -2
  88. nautobot/core/templates/buttons/export.html +17 -18
  89. nautobot/core/templates/buttons/job_import.html +2 -2
  90. nautobot/core/templates/components/breadcrumbs.html +19 -17
  91. nautobot/core/templates/components/button/dropdown.html +7 -5
  92. nautobot/core/templates/components/echarts.html +2 -0
  93. nautobot/core/templates/components/layout/one_over_two.html +3 -3
  94. nautobot/core/templates/components/layout/two_over_one.html +3 -3
  95. nautobot/core/templates/components/panel/body_content_data_table.html +2 -2
  96. nautobot/core/templates/components/panel/body_content_tags.html +1 -1
  97. nautobot/core/templates/components/panel/body_wrapper_generic.html +4 -2
  98. nautobot/core/templates/components/panel/body_wrapper_generic_table.html +1 -1
  99. nautobot/core/templates/components/panel/body_wrapper_key_value_table.html +5 -3
  100. nautobot/core/templates/components/panel/body_wrapper_table.html +4 -2
  101. nautobot/core/templates/components/panel/footer_contacts_table.html +4 -4
  102. nautobot/core/templates/components/panel/footer_content_table.html +3 -3
  103. nautobot/core/templates/components/panel/grouping_toggle.html +12 -11
  104. nautobot/core/templates/components/panel/header_extra_content_table.html +2 -11
  105. nautobot/core/templates/components/panel/panel.html +6 -3
  106. nautobot/core/templates/components/panel/stats_panel_body.html +9 -7
  107. nautobot/core/templates/components/tab/content_wrapper.html +29 -1
  108. nautobot/core/templates/components/tab/label_wrapper.html +10 -2
  109. nautobot/core/templates/components/tab/label_wrapper_distinct_view.html +11 -4
  110. nautobot/core/templates/echarts/echarts.html +20 -0
  111. nautobot/core/templates/exceptions/import_error.html +2 -2
  112. nautobot/core/templates/exceptions/permission_error.html +1 -1
  113. nautobot/core/templates/exceptions/programming_error.html +2 -2
  114. nautobot/core/templates/generic/object_bulk_add_component.html +29 -20
  115. nautobot/core/templates/generic/object_bulk_create.html +87 -75
  116. nautobot/core/templates/generic/object_bulk_destroy.html +35 -37
  117. nautobot/core/templates/generic/object_bulk_remove.html +30 -26
  118. nautobot/core/templates/generic/object_bulk_rename.html +53 -40
  119. nautobot/core/templates/generic/object_bulk_update.html +36 -29
  120. nautobot/core/templates/generic/object_create.html +40 -27
  121. nautobot/core/templates/generic/object_import.html +36 -24
  122. nautobot/core/templates/generic/object_list.html +279 -215
  123. nautobot/core/templates/generic/object_notes.html +21 -11
  124. nautobot/core/templates/generic/object_retrieve.html +161 -213
  125. nautobot/core/templates/graphene/graphiql.html +113 -60
  126. nautobot/core/templates/home.html +164 -87
  127. nautobot/core/templates/import_success.html +3 -2
  128. nautobot/core/templates/inc/ajax_loader.html +1 -1
  129. nautobot/core/templates/inc/computed_fields/panel_data.html +25 -13
  130. nautobot/core/templates/inc/created_updated.html +12 -7
  131. nautobot/core/templates/inc/custom_fields/panel_data.html +28 -16
  132. nautobot/core/templates/inc/custom_fields_panel.html +3 -3
  133. nautobot/core/templates/inc/dynamic_groups_panel.html +3 -3
  134. nautobot/core/templates/inc/extras_features_edit_form_fields.html +15 -15
  135. nautobot/core/templates/inc/footer.html +90 -40
  136. nautobot/core/templates/inc/form_static_field.html +6 -0
  137. nautobot/core/templates/inc/header.html +75 -0
  138. nautobot/core/templates/inc/header_banners.html +17 -0
  139. nautobot/core/templates/inc/header_messages.html +6 -0
  140. nautobot/core/templates/inc/image_attachments.html +9 -9
  141. nautobot/core/templates/inc/javascript.html +7 -24
  142. nautobot/core/templates/inc/media.html +4 -29
  143. nautobot/core/templates/inc/modal.html +2 -2
  144. nautobot/core/templates/inc/nav_favorites.html +27 -0
  145. nautobot/core/templates/inc/nav_menu.html +150 -108
  146. nautobot/core/templates/inc/object_details_advanced_panel.html +84 -71
  147. nautobot/core/templates/inc/page_title.html +23 -0
  148. nautobot/core/templates/inc/paginator.html +39 -28
  149. nautobot/core/templates/inc/relationships/panel_override.html +3 -3
  150. nautobot/core/templates/inc/relationships_panel.html +3 -3
  151. nautobot/core/templates/inc/relationships_table_rows.html +1 -1
  152. nautobot/core/templates/inc/search_panel.html +22 -16
  153. nautobot/core/templates/inc/table.html +61 -36
  154. nautobot/core/templates/inc/tenancy_form_panel.html +3 -3
  155. nautobot/core/templates/login.html +17 -59
  156. nautobot/core/templates/modals/modal_theme.html +12 -23
  157. nautobot/core/templates/nautobot_config.py.j2 +6 -5
  158. nautobot/core/templates/panel_table.html +8 -12
  159. nautobot/core/templates/redoc_ui.html +80 -0
  160. nautobot/core/templates/rest_framework/api.html +43 -21
  161. nautobot/core/templates/search.html +12 -13
  162. nautobot/core/templates/swagger_ui.html +19 -4
  163. nautobot/core/templates/system_jobs/import_objects.html +70 -58
  164. nautobot/core/templates/template.css +0 -6
  165. nautobot/core/templates/utilities/comment_form.html +34 -0
  166. nautobot/core/templates/utilities/confirmation_form.html +17 -9
  167. nautobot/core/templates/utilities/obj_table.html +19 -11
  168. nautobot/core/templates/utilities/render_field.html +27 -21
  169. nautobot/core/templates/utilities/render_jinja2.html +22 -25
  170. nautobot/core/templates/utilities/templatetags/advanced_filter_indicator.html +8 -0
  171. nautobot/core/templates/utilities/templatetags/badge.html +1 -1
  172. nautobot/core/templates/utilities/templatetags/dynamic_group_assignment_modal.html +2 -3
  173. nautobot/core/templates/utilities/templatetags/filter_form_drawer.html +482 -0
  174. nautobot/core/templates/utilities/templatetags/modal_form_as_dialog.html +14 -18
  175. nautobot/core/templates/utilities/templatetags/saved_view_modal.html +11 -11
  176. nautobot/core/templates/utilities/templatetags/table_config_form.html +51 -24
  177. nautobot/core/templates/utilities/templatetags/tag.html +1 -1
  178. nautobot/core/templates/utilities/templatetags/utilization_graph.html +3 -3
  179. nautobot/core/templates/utilities/theme_preview.html +829 -566
  180. nautobot/core/templates/utilities/worker_status.html +42 -41
  181. nautobot/core/templates/widgets/selectwithdisabled_option.html +3 -1
  182. nautobot/core/templates/widgets/sluginput.html +2 -2
  183. nautobot/core/templatetags/buttons.py +38 -40
  184. nautobot/core/templatetags/helpers.py +105 -28
  185. nautobot/core/templatetags/ui_framework.py +17 -0
  186. nautobot/core/testing/api.py +76 -12
  187. nautobot/core/testing/filters.py +11 -27
  188. nautobot/core/testing/integration.py +128 -10
  189. nautobot/core/testing/mixins.py +7 -4
  190. nautobot/core/testing/utils.py +28 -5
  191. nautobot/core/testing/views.py +125 -27
  192. nautobot/core/tests/integration/test_app_home.py +39 -35
  193. nautobot/core/tests/integration/test_app_navbar.py +60 -67
  194. nautobot/core/tests/integration/test_filters.py +123 -55
  195. nautobot/core/tests/integration/test_general_functionality.py +1 -1
  196. nautobot/core/tests/integration/test_home.py +10 -18
  197. nautobot/core/tests/integration/test_import_objects_ui.py +2 -9
  198. nautobot/core/tests/integration/test_navbar.py +41 -16
  199. nautobot/core/tests/integration/test_swagger.py +1 -7
  200. nautobot/core/tests/integration/test_theme.py +3 -0
  201. nautobot/core/tests/nautobot_config_without_example_apps.py +4 -0
  202. nautobot/core/tests/runner.py +6 -1
  203. nautobot/core/tests/test_api.py +5 -3
  204. nautobot/core/tests/test_branching.py +154 -0
  205. nautobot/core/tests/test_breadcrumbs.py +7 -8
  206. nautobot/core/tests/test_checks.py +28 -0
  207. nautobot/core/tests/test_commands.py +0 -41
  208. nautobot/core/tests/test_config.py +2 -1
  209. nautobot/core/tests/test_csv.py +4 -7
  210. nautobot/core/tests/test_filters.py +326 -318
  211. nautobot/core/tests/test_forms.py +19 -30
  212. nautobot/core/tests/test_graphql.py +67 -57
  213. nautobot/core/tests/test_models.py +1 -1
  214. nautobot/core/tests/test_nautobot_server.py +2 -0
  215. nautobot/core/tests/test_navigations.py +78 -10
  216. nautobot/core/tests/test_tables.py +3 -1
  217. nautobot/core/tests/test_templatetags_helpers.py +61 -21
  218. nautobot/core/tests/test_templatetags_ui_framework.py +36 -18
  219. nautobot/core/tests/test_ui.py +207 -2
  220. nautobot/core/tests/test_utils.py +147 -2
  221. nautobot/core/tests/test_views.py +201 -64
  222. nautobot/core/tests/test_views_utils.py +1 -1
  223. nautobot/core/ui/breadcrumbs.py +2 -12
  224. nautobot/core/ui/choices.py +190 -0
  225. nautobot/core/ui/constants.py +86 -0
  226. nautobot/core/ui/echarts.py +474 -0
  227. nautobot/core/ui/nav.py +5 -1
  228. nautobot/core/ui/object_detail.py +180 -16
  229. nautobot/core/urls.py +13 -1
  230. nautobot/core/utils/cache.py +71 -0
  231. nautobot/core/utils/data.py +8 -5
  232. nautobot/core/utils/filtering.py +8 -2
  233. nautobot/core/utils/git.py +3 -3
  234. nautobot/core/utils/lookup.py +87 -13
  235. nautobot/core/utils/migrations.py +22 -0
  236. nautobot/core/utils/module_loading.py +26 -0
  237. nautobot/core/utils/permissions.py +9 -5
  238. nautobot/core/views/__init__.py +114 -63
  239. nautobot/core/views/generic.py +34 -27
  240. nautobot/core/views/mixins.py +49 -27
  241. nautobot/core/views/renderers.py +3 -5
  242. nautobot/core/views/utils.py +10 -5
  243. nautobot/core/views/viewsets.py +2 -1
  244. nautobot/data_validation/__init__.py +0 -0
  245. nautobot/data_validation/api/__init__.py +1 -0
  246. nautobot/data_validation/api/serializers.py +80 -0
  247. nautobot/data_validation/api/urls.py +20 -0
  248. nautobot/data_validation/api/views.py +44 -0
  249. nautobot/data_validation/apps.py +18 -0
  250. nautobot/data_validation/custom_validators.py +330 -0
  251. nautobot/data_validation/filters.py +133 -0
  252. nautobot/data_validation/form_mixin.py +25 -0
  253. nautobot/data_validation/forms.py +342 -0
  254. nautobot/data_validation/migrations/0001_initial.py +224 -0
  255. nautobot/data_validation/migrations/0002_data_migration_from_app.py +324 -0
  256. nautobot/data_validation/migrations/__init__.py +0 -0
  257. nautobot/data_validation/models.py +361 -0
  258. nautobot/data_validation/navigation.py +74 -0
  259. nautobot/data_validation/signals.py +30 -0
  260. nautobot/data_validation/tables.py +259 -0
  261. nautobot/data_validation/templates/data_validation/datacompliance_retrieve.html +1 -0
  262. nautobot/data_validation/templates/data_validation/datacompliance_tab.html +11 -0
  263. nautobot/data_validation/templates/data_validation/device_constraints.html +61 -0
  264. nautobot/data_validation/tests/__init__.py +20 -0
  265. nautobot/data_validation/tests/migrations/__init__.py +0 -0
  266. nautobot/data_validation/tests/migrations/test_migrations.py +489 -0
  267. nautobot/data_validation/tests/test_api.py +238 -0
  268. nautobot/data_validation/tests/test_custom_validators.py +423 -0
  269. nautobot/data_validation/tests/test_data_compliance_rules.py +85 -0
  270. nautobot/data_validation/tests/test_filters.py +240 -0
  271. nautobot/data_validation/tests/test_form_mixin.py +115 -0
  272. nautobot/data_validation/tests/test_models.py +393 -0
  273. nautobot/data_validation/tests/test_views.py +435 -0
  274. nautobot/data_validation/urls.py +21 -0
  275. nautobot/data_validation/views.py +227 -0
  276. nautobot/dcim/api/serializers.py +10 -13
  277. nautobot/dcim/api/urls.py +2 -0
  278. nautobot/dcim/api/views.py +7 -0
  279. nautobot/dcim/apps.py +4 -0
  280. nautobot/dcim/choices.py +16 -0
  281. nautobot/dcim/custom_validators.py +84 -0
  282. nautobot/dcim/filter_mixins.py +353 -4
  283. nautobot/dcim/{filters/__init__.py → filters.py} +70 -157
  284. nautobot/dcim/forms.py +12 -6
  285. nautobot/dcim/graphql/types.py +1 -0
  286. nautobot/dcim/migrations/0075_add_deviceclusterassignment.py +52 -0
  287. nautobot/dcim/migrations/0076_device_cluster_to_clusters_data_migration.py +40 -0
  288. nautobot/dcim/migrations/0077_remove_device_cluster.py +14 -0
  289. nautobot/dcim/migrations/0078_remove_device_location_tenant_name_uniqueness.py +16 -0
  290. nautobot/dcim/migrations/0079_device_name_data_migration.py +59 -0
  291. nautobot/dcim/models/__init__.py +2 -0
  292. nautobot/dcim/models/device_components.py +3 -1
  293. nautobot/dcim/models/devices.py +115 -51
  294. nautobot/dcim/navigation.py +7 -3
  295. nautobot/dcim/querysets.py +6 -0
  296. nautobot/dcim/signals.py +19 -0
  297. nautobot/dcim/tables/devices.py +9 -5
  298. nautobot/dcim/tables/template_code.py +191 -102
  299. nautobot/dcim/templates/dcim/cable.html +1 -1
  300. nautobot/dcim/templates/dcim/cable_connect.html +62 -146
  301. nautobot/dcim/templates/dcim/cable_retrieve.html +10 -10
  302. nautobot/dcim/templates/dcim/cable_trace.html +15 -17
  303. nautobot/dcim/templates/dcim/console_port_connection_list.html +2 -2
  304. nautobot/dcim/templates/dcim/consoleport.html +18 -17
  305. nautobot/dcim/templates/dcim/consoleserverport.html +18 -17
  306. nautobot/dcim/templates/dcim/controller_create.html +12 -8
  307. nautobot/dcim/templates/dcim/controller_wirelessnetworks.html +1 -1
  308. nautobot/dcim/templates/dcim/controllermanageddevicegroup_create.html +6 -6
  309. nautobot/dcim/templates/dcim/controllermanageddevicegroup_retrieve.html +1 -1
  310. nautobot/dcim/templates/dcim/device/config.html +17 -19
  311. nautobot/dcim/templates/dcim/device/lldp_neighbors.html +4 -4
  312. nautobot/dcim/templates/dcim/device/status.html +20 -20
  313. nautobot/dcim/templates/dcim/device_component_add.html +24 -15
  314. nautobot/dcim/templates/dcim/device_create.html +120 -120
  315. nautobot/dcim/templates/dcim/device_list.html +75 -12
  316. nautobot/dcim/templates/dcim/devicebay.html +7 -7
  317. nautobot/dcim/templates/dcim/devicebay_populate.html +29 -23
  318. nautobot/dcim/templates/dcim/deviceredundancygroup_create.html +6 -6
  319. nautobot/dcim/templates/dcim/devicetype.html +1 -1
  320. nautobot/dcim/templates/dcim/devicetype_component_add.html +25 -19
  321. nautobot/dcim/templates/dcim/devicetype_list.html +4 -4
  322. nautobot/dcim/templates/dcim/devicetype_update.html +9 -9
  323. nautobot/dcim/templates/dcim/footer_convert_to_contact_or_team_record.html +3 -3
  324. nautobot/dcim/templates/dcim/frontport.html +21 -20
  325. nautobot/dcim/templates/dcim/inc/cable_form.html +7 -7
  326. nautobot/dcim/templates/dcim/inc/cable_termination.html +1 -1
  327. nautobot/dcim/templates/dcim/inc/cable_toggle_buttons.html +18 -9
  328. nautobot/dcim/templates/dcim/inc/detail_softwareversion_softwareimagefile_rows.html +1 -1
  329. nautobot/dcim/templates/dcim/inc/device_interface_filter.html +1 -1
  330. nautobot/dcim/templates/dcim/inc/devicetype_component_table.html +10 -10
  331. nautobot/dcim/templates/dcim/inc/edit_form_softwareversion_js.html +2 -2
  332. nautobot/dcim/templates/dcim/inc/homepage_connections.html +2 -2
  333. nautobot/dcim/templates/dcim/inc/moduletype_component_table.html +10 -10
  334. nautobot/dcim/templates/dcim/inc/rack_elevation.html +2 -2
  335. nautobot/dcim/templates/dcim/interface.html +42 -22
  336. nautobot/dcim/templates/dcim/interface_connection_list.html +2 -2
  337. nautobot/dcim/templates/dcim/interface_edit.html +26 -11
  338. nautobot/dcim/templates/dcim/interfaceredundancygroupassociation_create.html +3 -3
  339. nautobot/dcim/templates/dcim/inventoryitem.html +3 -3
  340. nautobot/dcim/templates/dcim/inventoryitem_add.html +21 -10
  341. nautobot/dcim/templates/dcim/inventoryitem_bulk_delete.html +1 -1
  342. nautobot/dcim/templates/dcim/inventoryitem_edit.html +6 -4
  343. nautobot/dcim/templates/dcim/location.html +1 -1
  344. nautobot/dcim/templates/dcim/location_migrate_data_to_contact.html +24 -18
  345. nautobot/dcim/templates/dcim/location_retrieve.html +1 -1
  346. nautobot/dcim/templates/dcim/location_update.html +9 -9
  347. nautobot/dcim/templates/dcim/locationtype.html +0 -1
  348. nautobot/dcim/templates/dcim/module/base.html +67 -27
  349. nautobot/dcim/templates/dcim/module_consoleports.html +13 -15
  350. nautobot/dcim/templates/dcim/module_consoleserverports.html +13 -15
  351. nautobot/dcim/templates/dcim/module_frontports.html +13 -15
  352. nautobot/dcim/templates/dcim/module_interfaces.html +14 -16
  353. nautobot/dcim/templates/dcim/module_list.html +59 -10
  354. nautobot/dcim/templates/dcim/module_modulebays.html +12 -14
  355. nautobot/dcim/templates/dcim/module_poweroutlets.html +13 -15
  356. nautobot/dcim/templates/dcim/module_powerports.html +13 -15
  357. nautobot/dcim/templates/dcim/module_rearports.html +13 -15
  358. nautobot/dcim/templates/dcim/module_retrieve.html +3 -3
  359. nautobot/dcim/templates/dcim/module_update.html +15 -9
  360. nautobot/dcim/templates/dcim/modulebay_retrieve.html +0 -93
  361. nautobot/dcim/templates/dcim/modulefamily_retrieve.html +7 -7
  362. nautobot/dcim/templates/dcim/moduletype_list.html +2 -2
  363. nautobot/dcim/templates/dcim/moduletype_retrieve.html +74 -35
  364. nautobot/dcim/templates/dcim/platform_create.html +9 -9
  365. nautobot/dcim/templates/dcim/power_port_connection_list.html +3 -3
  366. nautobot/dcim/templates/dcim/powerfeed.html +1 -1
  367. nautobot/dcim/templates/dcim/powerfeed_edit.html +15 -15
  368. nautobot/dcim/templates/dcim/poweroutlet.html +13 -13
  369. nautobot/dcim/templates/dcim/powerpanel.html +1 -1
  370. nautobot/dcim/templates/dcim/powerport.html +17 -16
  371. nautobot/dcim/templates/dcim/rack.html +1 -1
  372. nautobot/dcim/templates/dcim/rack_elevation.html +2 -2
  373. nautobot/dcim/templates/dcim/rack_elevation_list.html +21 -9
  374. nautobot/dcim/templates/dcim/rack_retrieve.html +75 -57
  375. nautobot/dcim/templates/dcim/rack_update.html +14 -14
  376. nautobot/dcim/templates/dcim/rackreservation.html +1 -1
  377. nautobot/dcim/templates/dcim/rackreservation_edit.html +6 -6
  378. nautobot/dcim/templates/dcim/rearport.html +19 -18
  379. nautobot/dcim/templates/dcim/trace/cable.html +1 -1
  380. nautobot/dcim/templates/dcim/trace/circuit.html +1 -1
  381. nautobot/dcim/templates/dcim/trace/device.html +1 -1
  382. nautobot/dcim/templates/dcim/trace/powerpanel.html +1 -1
  383. nautobot/dcim/templates/dcim/trace/termination.html +1 -1
  384. nautobot/dcim/templates/dcim/virtualchassis.html +1 -1
  385. nautobot/dcim/templates/dcim/virtualchassis_add_member.html +25 -16
  386. nautobot/dcim/templates/dcim/virtualchassis_create.html +6 -6
  387. nautobot/dcim/templates/dcim/virtualchassis_edit.html +1 -1
  388. nautobot/dcim/templates/dcim/virtualchassis_retrieve.html +1 -1
  389. nautobot/dcim/templates/dcim/virtualchassis_update.html +36 -22
  390. nautobot/dcim/templates/dcim/virtualdevicecontext_update.html +9 -9
  391. nautobot/dcim/tests/integration/test_controller.py +6 -6
  392. nautobot/dcim/tests/integration/test_controller_managed_device_group.py +7 -7
  393. nautobot/dcim/tests/integration/test_create_device.py +9 -9
  394. nautobot/dcim/tests/integration/test_device_bulk_operations.py +7 -2
  395. nautobot/dcim/tests/integration/test_fileinputpicker.py +5 -7
  396. nautobot/dcim/tests/integration/test_location_bulk_operations.py +2 -0
  397. nautobot/dcim/tests/integration/test_module_bay_position.py +4 -1
  398. nautobot/dcim/tests/test_api.py +86 -6
  399. nautobot/dcim/tests/test_custom_validators.py +229 -0
  400. nautobot/dcim/tests/test_filters.py +159 -110
  401. nautobot/dcim/tests/test_graphql.py +32 -36
  402. nautobot/dcim/tests/test_jobs.py +1 -1
  403. nautobot/dcim/tests/test_models.py +229 -1
  404. nautobot/dcim/tests/test_views.py +31 -20
  405. nautobot/dcim/utils.py +3 -3
  406. nautobot/dcim/views.py +77 -41
  407. nautobot/extras/api/serializers.py +83 -19
  408. nautobot/extras/api/urls.py +7 -0
  409. nautobot/extras/api/views.py +243 -140
  410. nautobot/extras/choices.py +34 -13
  411. nautobot/extras/constants.py +1 -1
  412. nautobot/extras/context_managers.py +26 -26
  413. nautobot/extras/datasources/git.py +22 -0
  414. nautobot/extras/datasources/registry.py +3 -0
  415. nautobot/extras/exceptions.py +5 -0
  416. nautobot/extras/factory.py +11 -1
  417. nautobot/extras/{filters/mixins.py → filter_mixins.py} +4 -3
  418. nautobot/extras/{filters/__init__.py → filters.py} +203 -58
  419. nautobot/extras/forms/base.py +2 -1
  420. nautobot/extras/forms/forms.py +225 -20
  421. nautobot/extras/forms/mixins.py +0 -41
  422. nautobot/extras/homepage.py +21 -2
  423. nautobot/extras/jobs.py +2 -8
  424. nautobot/extras/jobs_ui.py +2 -2
  425. nautobot/extras/management/__init__.py +9 -0
  426. nautobot/extras/managers.py +31 -22
  427. nautobot/extras/migrations/0126_approval_workflow_pre_check.py +58 -0
  428. nautobot/extras/migrations/0127_approval_workflow_models.py +266 -0
  429. nautobot/extras/migrations/0128_remove_job_approval_required_and_more.py +29 -0
  430. nautobot/extras/migrations/0129_jobresult_debug_log_count_jobresult_error_log_count_and_more.py +37 -0
  431. nautobot/extras/migrations/0130_jobresult_generate_log_entry_counts.py +42 -0
  432. nautobot/extras/models/__init__.py +14 -3
  433. nautobot/extras/models/approvals.py +556 -0
  434. nautobot/extras/models/change_logging.py +1 -0
  435. nautobot/extras/models/contacts.py +2 -0
  436. nautobot/extras/models/customfields.py +57 -22
  437. nautobot/extras/models/datasources.py +21 -0
  438. nautobot/extras/models/groups.py +2 -0
  439. nautobot/extras/models/jobs.py +122 -39
  440. nautobot/extras/models/metadata.py +2 -3
  441. nautobot/extras/models/mixins.py +129 -1
  442. nautobot/extras/models/models.py +22 -14
  443. nautobot/extras/models/relationships.py +47 -10
  444. nautobot/extras/models/secrets.py +1 -0
  445. nautobot/extras/models/statuses.py +0 -15
  446. nautobot/extras/models/tags.py +1 -1
  447. nautobot/extras/navigation.py +42 -15
  448. nautobot/extras/plugins/__init__.py +33 -55
  449. nautobot/extras/plugins/marketplace_manifest.yml +1 -23
  450. nautobot/extras/plugins/tables.py +8 -6
  451. nautobot/extras/plugins/urls.py +2 -21
  452. nautobot/extras/plugins/utils.py +1 -33
  453. nautobot/extras/plugins/validators.py +10 -10
  454. nautobot/extras/plugins/views.py +1 -5
  455. nautobot/extras/querysets.py +17 -21
  456. nautobot/extras/signals.py +23 -8
  457. nautobot/extras/tables.py +420 -99
  458. nautobot/extras/templates/extras/approval_dashboard.html +15 -0
  459. nautobot/extras/templates/extras/approval_workflow/approve.html +11 -0
  460. nautobot/extras/templates/extras/approval_workflow/comment.html +9 -0
  461. nautobot/extras/templates/extras/approval_workflow/deny.html +10 -0
  462. nautobot/extras/templates/extras/approvalworkflowdefinition_update.html +77 -0
  463. nautobot/extras/templates/extras/approvalworkflowstage_retrieve.html +29 -0
  464. nautobot/extras/templates/extras/configcontext_update.html +12 -12
  465. nautobot/extras/templates/extras/configcontextschema.html +1 -1
  466. nautobot/extras/templates/extras/configcontextschema_retrieve.html +9 -9
  467. nautobot/extras/templates/extras/configcontextschema_update.html +6 -6
  468. nautobot/extras/templates/extras/configcontextschema_validation.html +2 -2
  469. nautobot/extras/templates/extras/customfield_update.html +12 -12
  470. nautobot/extras/templates/extras/dynamicgroup.html +1 -1
  471. nautobot/extras/templates/extras/dynamicgroup_edit.html +1 -1
  472. nautobot/extras/templates/extras/dynamicgroup_retrieve.html +17 -17
  473. nautobot/extras/templates/extras/dynamicgroup_update.html +24 -24
  474. nautobot/extras/templates/extras/externalintegration_update.html +6 -6
  475. nautobot/extras/templates/extras/gitrepository.html +1 -1
  476. nautobot/extras/templates/extras/gitrepository_object_edit.html +1 -1
  477. nautobot/extras/templates/extras/gitrepository_result.html +1 -1
  478. nautobot/extras/templates/extras/gitrepository_retrieve.html +12 -12
  479. nautobot/extras/templates/extras/gitrepository_update.html +25 -7
  480. nautobot/extras/templates/extras/graphqlquery_retrieve.html +1 -1
  481. nautobot/extras/templates/extras/inc/approval_buttons_column.html +38 -0
  482. nautobot/extras/templates/extras/inc/bulk_edit_overridable_field.html +14 -13
  483. nautobot/extras/templates/extras/inc/configcontext_format.html +11 -4
  484. nautobot/extras/templates/extras/inc/graphqlquery_execute.html +7 -7
  485. nautobot/extras/templates/extras/inc/job_label.html +5 -5
  486. nautobot/extras/templates/extras/inc/job_table.html +23 -10
  487. nautobot/extras/templates/extras/inc/job_tiles.html +33 -21
  488. nautobot/extras/templates/extras/inc/jobresult.html +6 -6
  489. nautobot/extras/templates/extras/inc/json_format.html +11 -4
  490. nautobot/extras/templates/extras/inc/object_contact_header.html +6 -6
  491. nautobot/extras/templates/extras/inc/overridable_field.html +16 -15
  492. nautobot/extras/templates/extras/inc/panel_approvalworkflowstage.html +34 -0
  493. nautobot/extras/templates/extras/inc/panel_changelog.html +9 -9
  494. nautobot/extras/templates/extras/inc/panel_jobhistory.html +8 -6
  495. nautobot/extras/templates/extras/inc/tags_panel.html +3 -3
  496. nautobot/extras/templates/extras/job.html +154 -155
  497. nautobot/extras/templates/extras/job_approval_confirmation.html +4 -27
  498. nautobot/extras/templates/extras/job_bulk_edit.html +18 -1
  499. nautobot/extras/templates/extras/job_detail.html +1 -1
  500. nautobot/extras/templates/extras/job_edit.html +69 -64
  501. nautobot/extras/templates/extras/job_list.html +37 -60
  502. nautobot/extras/templates/extras/jobresult.html +1 -1
  503. nautobot/extras/templates/extras/jobresult_retrieve.html +17 -17
  504. nautobot/extras/templates/extras/marketplace.html +62 -71
  505. nautobot/extras/templates/extras/metadatatype_create.html +9 -9
  506. nautobot/extras/templates/extras/note.html +1 -1
  507. nautobot/extras/templates/extras/object_approvalworkflow.html +36 -0
  508. nautobot/extras/templates/extras/object_assign_contact_or_team.html +16 -7
  509. nautobot/extras/templates/extras/object_configcontext.html +20 -20
  510. nautobot/extras/templates/extras/object_new_contact.html +6 -6
  511. nautobot/extras/templates/extras/object_new_team.html +6 -6
  512. nautobot/extras/templates/extras/objectchange.html +1 -1
  513. nautobot/extras/templates/extras/objectchange_retrieve.html +37 -56
  514. nautobot/extras/templates/extras/plugin_detail.html +40 -41
  515. nautobot/extras/templates/extras/plugins_list.html +23 -38
  516. nautobot/extras/templates/extras/plugins_tiles.html +28 -28
  517. nautobot/extras/templates/extras/role_retrieve.html +112 -48
  518. nautobot/extras/templates/extras/scheduledjob.html +25 -28
  519. nautobot/extras/templates/extras/secret_create.html +11 -11
  520. nautobot/extras/templates/extras/secretsgroup_update.html +6 -6
  521. nautobot/extras/templates/extras/staticgroupassociation_retrieve.html +3 -3
  522. nautobot/extras/templates/extras/status.html +1 -1
  523. nautobot/extras/templates/extras/tag.html +1 -1
  524. nautobot/extras/templates/extras/tag_update.html +3 -3
  525. nautobot/extras/templates/extras/templatetags/log_level.html +1 -1
  526. nautobot/extras/templates/extras/templatetags/plugin_object_detail_tabs.html +2 -2
  527. nautobot/extras/templates/extras/webhook.html +12 -12
  528. nautobot/extras/templatetags/approvals.py +19 -0
  529. nautobot/extras/templatetags/custom_links.py +12 -12
  530. nautobot/extras/templatetags/job_buttons.py +14 -12
  531. nautobot/extras/test_jobs/invalid_import.py +9 -0
  532. nautobot/extras/test_jobs/log_counts_by_level.py +23 -0
  533. nautobot/extras/test_jobs/missing_import.py +11 -0
  534. nautobot/extras/tests/integration/test_computedfields.py +5 -8
  535. nautobot/extras/tests/integration/test_configcontextschema.py +43 -48
  536. nautobot/extras/tests/integration/test_customfields.py +33 -33
  537. nautobot/extras/tests/integration/test_dynamicgroups.py +5 -10
  538. nautobot/extras/tests/integration/test_jobs.py +2 -4
  539. nautobot/extras/tests/integration/test_notes.py +3 -9
  540. nautobot/extras/tests/integration/test_plugin_banner.py +3 -0
  541. nautobot/extras/tests/integration/test_plugins.py +35 -27
  542. nautobot/extras/tests/integration/test_relationships.py +7 -11
  543. nautobot/extras/tests/integration/test_tagfilter.py +3 -11
  544. nautobot/extras/tests/test_api.py +786 -242
  545. nautobot/extras/tests/test_approvals.py +715 -0
  546. nautobot/extras/tests/test_changelog.py +18 -14
  547. nautobot/extras/tests/test_customfields.py +14 -13
  548. nautobot/extras/tests/test_datasources.py +1 -1
  549. nautobot/extras/tests/test_dynamicgroups.py +9 -4
  550. nautobot/extras/tests/test_filters.py +443 -13
  551. nautobot/extras/tests/test_forms.py +18 -57
  552. nautobot/extras/tests/test_jobs.py +25 -4
  553. nautobot/extras/tests/test_migrations.py +81 -1
  554. nautobot/extras/tests/test_models.py +378 -47
  555. nautobot/extras/tests/test_plugins.py +47 -13
  556. nautobot/extras/tests/test_relationships.py +7 -2
  557. nautobot/extras/tests/test_utils.py +2 -0
  558. nautobot/extras/tests/test_views.py +780 -493
  559. nautobot/extras/urls.py +36 -12
  560. nautobot/extras/utils.py +58 -12
  561. nautobot/extras/views.py +668 -209
  562. nautobot/ipam/factory.py +7 -0
  563. nautobot/ipam/filter_mixins.py +38 -0
  564. nautobot/ipam/filters.py +35 -71
  565. nautobot/ipam/formfields.py +1 -1
  566. nautobot/ipam/forms.py +6 -3
  567. nautobot/ipam/migrations/0030_ipam__namespaces.py +13 -0
  568. nautobot/ipam/migrations/0031_ipam___data_migrations.py +4 -1
  569. nautobot/ipam/migrations/0054_namespace_tenant.py +25 -0
  570. nautobot/ipam/models.py +29 -2
  571. nautobot/ipam/navigation.py +3 -1
  572. nautobot/ipam/querysets.py +1 -2
  573. nautobot/ipam/tables.py +26 -17
  574. nautobot/ipam/templates/ipam/inc/ipadress_edit_header.html +6 -6
  575. nautobot/ipam/templates/ipam/inc/service.html +8 -8
  576. nautobot/ipam/templates/ipam/inc/toggle_available.html +10 -10
  577. nautobot/ipam/templates/ipam/inc/vlangroup_header.html +3 -2
  578. nautobot/ipam/templates/ipam/ipaddress.html +27 -13
  579. nautobot/ipam/templates/ipam/ipaddress_assign.html +31 -24
  580. nautobot/ipam/templates/ipam/ipaddress_bulk_add.html +3 -3
  581. nautobot/ipam/templates/ipam/ipaddress_edit.html +9 -9
  582. nautobot/ipam/templates/ipam/ipaddress_interfaces.html +7 -9
  583. nautobot/ipam/templates/ipam/ipaddress_merge.html +195 -186
  584. nautobot/ipam/templates/ipam/ipaddress_vm_interfaces.html +7 -9
  585. nautobot/ipam/templates/ipam/ipaddresstointerface_retrieve.html +7 -5
  586. nautobot/ipam/templates/ipam/namespace_ip_addresses.html +1 -1
  587. nautobot/ipam/templates/ipam/namespace_prefixes.html +1 -1
  588. nautobot/ipam/templates/ipam/namespace_update.html +15 -0
  589. nautobot/ipam/templates/ipam/namespace_vrfs.html +1 -1
  590. nautobot/ipam/templates/ipam/prefix_create.html +9 -9
  591. nautobot/ipam/templates/ipam/prefix_list.html +15 -14
  592. nautobot/ipam/templates/ipam/prefix_retrieve.html +0 -1
  593. nautobot/ipam/templates/ipam/vlan.html +1 -1
  594. nautobot/ipam/templates/ipam/vlan_interfaces.html +1 -1
  595. nautobot/ipam/templates/ipam/vlan_update.html +6 -6
  596. nautobot/ipam/templates/ipam/vlan_vminterfaces.html +1 -1
  597. nautobot/ipam/templates/ipam/vrf_edit.html +15 -15
  598. nautobot/ipam/tests/integration/test_prefixes.py +5 -13
  599. nautobot/ipam/tests/migration/test_migrations.py +89 -0
  600. nautobot/ipam/tests/test_api.py +20 -7
  601. nautobot/ipam/tests/test_filters.py +10 -0
  602. nautobot/ipam/tests/test_forms.py +1 -1
  603. nautobot/ipam/tests/test_models.py +1 -1
  604. nautobot/ipam/tests/test_tables.py +1 -2
  605. nautobot/ipam/tests/test_utils.py +1 -1
  606. nautobot/ipam/tests/test_views.py +24 -21
  607. nautobot/ipam/ui.py +0 -17
  608. nautobot/ipam/utils/migrations.py +16 -2
  609. nautobot/ipam/utils/testing.py +9 -3
  610. nautobot/ipam/views.py +49 -7
  611. nautobot/project-static/dist/css/graphql-libraries.css +655 -0
  612. nautobot/project-static/dist/css/graphql-libraries.css.map +1 -0
  613. nautobot/project-static/dist/css/materialdesignicons.css +3 -0
  614. nautobot/project-static/dist/css/materialdesignicons.css.map +1 -0
  615. nautobot/project-static/dist/css/nautobot.css +13 -0
  616. nautobot/project-static/dist/css/nautobot.css.map +1 -0
  617. nautobot/project-static/dist/js/graphql-libraries.js +3 -0
  618. nautobot/project-static/dist/js/graphql-libraries.js.LICENSE.txt +62 -0
  619. nautobot/project-static/dist/js/graphql-libraries.js.map +1 -0
  620. nautobot/project-static/dist/js/libraries.js +3 -0
  621. nautobot/project-static/dist/js/libraries.js.LICENSE.txt +65 -0
  622. nautobot/project-static/dist/js/libraries.js.map +1 -0
  623. nautobot/project-static/dist/js/materialdesignicons.js +0 -0
  624. nautobot/project-static/dist/js/nautobot-graphiql.js +2 -0
  625. nautobot/project-static/dist/js/nautobot-graphiql.js.map +1 -0
  626. nautobot/project-static/dist/js/nautobot.js +2 -0
  627. nautobot/project-static/dist/js/nautobot.js.map +1 -0
  628. nautobot/project-static/fonts/Montserrat-v30-Bold.woff2 +0 -0
  629. nautobot/project-static/fonts/Montserrat-v30-Light.woff2 +0 -0
  630. nautobot/project-static/fonts/Montserrat-v30-Regular.woff2 +0 -0
  631. nautobot/project-static/fonts/Roboto-v48-Bold.woff2 +0 -0
  632. nautobot/project-static/fonts/Roboto-v48-Light.woff2 +0 -0
  633. nautobot/project-static/fonts/Roboto-v48-Regular.woff2 +0 -0
  634. nautobot/project-static/img/jinja_logo.svg +21 -92
  635. nautobot/project-static/js/cabletrace.js +1 -1
  636. nautobot/project-static/js/editor.js +4 -4
  637. nautobot/project-static/js/forms.js +67 -717
  638. nautobot/project-static/js/job_result.js +2 -2
  639. nautobot/project-static/nautobot-icons/360-degrees.svg +3 -0
  640. nautobot/project-static/nautobot-icons/arrow-decision.svg +3 -0
  641. nautobot/project-static/nautobot-icons/arrows-expand-rec.svg +3 -0
  642. nautobot/project-static/nautobot-icons/arrows-move-2-rec.svg +3 -0
  643. nautobot/project-static/nautobot-icons/arrows-move-rec.svg +3 -0
  644. nautobot/project-static/nautobot-icons/atom.svg +3 -0
  645. nautobot/project-static/nautobot-icons/battery-3.svg +3 -0
  646. nautobot/project-static/nautobot-icons/branch.svg +3 -0
  647. nautobot/project-static/nautobot-icons/briefcase-2.svg +3 -0
  648. nautobot/project-static/nautobot-icons/cable-data-2.svg +3 -0
  649. nautobot/project-static/nautobot-icons/cable-data.svg +3 -0
  650. nautobot/project-static/nautobot-icons/cast.svg +3 -0
  651. nautobot/project-static/nautobot-icons/check-circle.svg +3 -0
  652. nautobot/project-static/nautobot-icons/checkbox-circle.svg +3 -0
  653. nautobot/project-static/nautobot-icons/checkbox-rec.svg +3 -0
  654. nautobot/project-static/nautobot-icons/cloud-check.svg +3 -0
  655. nautobot/project-static/nautobot-icons/cloud-lightning.svg +3 -0
  656. nautobot/project-static/nautobot-icons/cloud-upload.svg +3 -0
  657. nautobot/project-static/nautobot-icons/cloud.svg +3 -0
  658. nautobot/project-static/nautobot-icons/compass.svg +3 -0
  659. nautobot/project-static/nautobot-icons/control-panel.svg +3 -0
  660. nautobot/project-static/nautobot-icons/credit-card.svg +3 -0
  661. nautobot/project-static/nautobot-icons/device-lifecycle.svg +3 -0
  662. nautobot/project-static/nautobot-icons/direction.svg +3 -0
  663. nautobot/project-static/nautobot-icons/elements.svg +3 -0
  664. nautobot/project-static/nautobot-icons/extensibility.svg +3 -0
  665. nautobot/project-static/nautobot-icons/globe-2.svg +3 -0
  666. nautobot/project-static/nautobot-icons/globe.svg +3 -0
  667. nautobot/project-static/nautobot-icons/hammer.svg +3 -0
  668. nautobot/project-static/nautobot-icons/history.svg +3 -0
  669. nautobot/project-static/nautobot-icons/ip.svg +3 -0
  670. nautobot/project-static/nautobot-icons/laptop.svg +3 -0
  671. nautobot/project-static/nautobot-icons/lightning.svg +3 -0
  672. nautobot/project-static/nautobot-icons/list-unordered.svg +3 -0
  673. nautobot/project-static/nautobot-icons/map-view.svg +3 -0
  674. nautobot/project-static/nautobot-icons/organization.svg +3 -0
  675. nautobot/project-static/nautobot-icons/pin-2.svg +3 -0
  676. nautobot/project-static/nautobot-icons/pin-3.svg +3 -0
  677. nautobot/project-static/nautobot-icons/plug.svg +3 -0
  678. nautobot/project-static/nautobot-icons/refresh-cw.svg +3 -0
  679. nautobot/project-static/nautobot-icons/rocket-2.svg +3 -0
  680. nautobot/project-static/nautobot-icons/rotate-cw.svg +3 -0
  681. nautobot/project-static/nautobot-icons/route.svg +3 -0
  682. nautobot/project-static/nautobot-icons/secrets.svg +3 -0
  683. nautobot/project-static/nautobot-icons/security.svg +3 -0
  684. nautobot/project-static/nautobot-icons/server-2.svg +3 -0
  685. nautobot/project-static/nautobot-icons/server.svg +3 -0
  686. nautobot/project-static/nautobot-icons/share.svg +3 -0
  687. nautobot/project-static/nautobot-icons/shield-check.svg +3 -0
  688. nautobot/project-static/nautobot-icons/sitemap-outline.svg +3 -0
  689. nautobot/project-static/nautobot-icons/sliders-vert-2.svg +3 -0
  690. nautobot/project-static/nautobot-icons/sliders-vert.svg +3 -0
  691. nautobot/project-static/nautobot-icons/star-filled.svg +3 -0
  692. nautobot/project-static/nautobot-icons/star.svg +3 -0
  693. nautobot/project-static/nautobot-icons/transform.svg +3 -0
  694. nautobot/project-static/nautobot-icons/wifi.svg +3 -0
  695. nautobot/tenancy/api/serializers.py +1 -0
  696. nautobot/tenancy/api/views.py +2 -1
  697. nautobot/tenancy/{filters/__init__.py → filters.py} +2 -10
  698. nautobot/tenancy/navigation.py +3 -1
  699. nautobot/tenancy/templates/tenancy/tenant_create.html +6 -6
  700. nautobot/tenancy/tests/test_filters.py +0 -2
  701. nautobot/tenancy/views.py +2 -1
  702. nautobot/ui/.gitignore +137 -0
  703. nautobot/ui/.node-version +1 -0
  704. nautobot/ui/.prettierignore +3 -0
  705. nautobot/ui/eslint.config.js +33 -0
  706. nautobot/ui/package-lock.json +6594 -0
  707. nautobot/ui/package.json +67 -0
  708. nautobot/ui/prettier.config.js +9 -0
  709. nautobot/ui/src/js/collapse.js +69 -0
  710. nautobot/ui/src/js/cookie.js +31 -0
  711. nautobot/ui/src/js/draggable.js +101 -0
  712. nautobot/ui/src/js/drawer.js +106 -0
  713. nautobot/ui/src/js/form.js +23 -0
  714. nautobot/ui/src/js/history.js +51 -0
  715. nautobot/ui/src/js/nautobot-graphiql.js +19 -0
  716. nautobot/ui/src/js/nautobot.js +128 -0
  717. nautobot/ui/src/js/search.js +274 -0
  718. nautobot/ui/src/js/select2.js +318 -0
  719. nautobot/ui/src/js/sidenav.js +87 -0
  720. nautobot/ui/src/js/tabs.js +139 -0
  721. nautobot/ui/src/js/theme.js +104 -0
  722. nautobot/ui/src/js/utils.js +54 -0
  723. nautobot/ui/src/scss/colors.scss +58 -0
  724. nautobot/ui/src/scss/nautobot.scss +2471 -0
  725. nautobot/ui/webpack.config.js +148 -0
  726. nautobot/users/apps.py +3 -0
  727. nautobot/users/filters.py +7 -11
  728. nautobot/users/forms.py +10 -0
  729. nautobot/users/models.py +8 -0
  730. nautobot/users/templates/users/advanced_settings_edit.html +31 -21
  731. nautobot/users/templates/users/api_tokens.html +61 -51
  732. nautobot/users/templates/users/base.html +23 -31
  733. nautobot/users/templates/users/change_password.html +29 -19
  734. nautobot/users/templates/users/preferences.html +55 -45
  735. nautobot/users/templates/users/profile.html +45 -14
  736. nautobot/users/tests/test_api.py +4 -0
  737. nautobot/users/urls.py +2 -0
  738. nautobot/users/views.py +70 -2
  739. nautobot/virtualization/api/views.py +1 -1
  740. nautobot/virtualization/filters.py +18 -32
  741. nautobot/virtualization/forms.py +22 -59
  742. nautobot/virtualization/models.py +1 -19
  743. nautobot/virtualization/navigation.py +3 -1
  744. nautobot/virtualization/tables.py +10 -6
  745. nautobot/virtualization/templates/virtualization/cluster.html +13 -13
  746. nautobot/virtualization/templates/virtualization/cluster_edit.html +6 -6
  747. nautobot/virtualization/templates/virtualization/inc/virtualmachine_vminterface_filter.html +1 -1
  748. nautobot/virtualization/templates/virtualization/virtualmachine.html +1 -1
  749. nautobot/virtualization/templates/virtualization/virtualmachine_component_add.html +24 -16
  750. nautobot/virtualization/templates/virtualization/virtualmachine_edit.html +1 -1
  751. nautobot/virtualization/templates/virtualization/virtualmachine_list.html +4 -4
  752. nautobot/virtualization/templates/virtualization/virtualmachine_update.html +27 -25
  753. nautobot/virtualization/templates/virtualization/vminterface.html +5 -5
  754. nautobot/virtualization/templates/virtualization/vminterface_edit.html +27 -11
  755. nautobot/virtualization/tests/test_api.py +3 -0
  756. nautobot/virtualization/tests/test_models.py +20 -5
  757. nautobot/virtualization/tests/test_views.py +3 -5
  758. nautobot/virtualization/urls.py +0 -11
  759. nautobot/virtualization/views.py +5 -122
  760. nautobot/vpn/__init__.py +0 -0
  761. nautobot/vpn/api/serializers.py +113 -0
  762. nautobot/vpn/api/urls.py +19 -0
  763. nautobot/vpn/api/views.py +70 -0
  764. nautobot/vpn/apps.py +8 -0
  765. nautobot/vpn/choices.py +171 -0
  766. nautobot/vpn/factory.py +209 -0
  767. nautobot/vpn/filters.py +233 -0
  768. nautobot/vpn/forms.py +486 -0
  769. nautobot/vpn/homepage.py +19 -0
  770. nautobot/vpn/migrations/0001_initial.py +541 -0
  771. nautobot/vpn/migrations/0002_populate_defaults.py +199 -0
  772. nautobot/vpn/migrations/__init__.py +0 -0
  773. nautobot/vpn/models.py +527 -0
  774. nautobot/vpn/navigation.py +98 -0
  775. nautobot/vpn/tables.py +380 -0
  776. nautobot/vpn/templates/vpn/vpnprofile.html +2 -0
  777. nautobot/vpn/templates/vpn/vpnprofile_create.html +150 -0
  778. nautobot/vpn/tests/__init__.py +0 -0
  779. nautobot/vpn/tests/test_api.py +341 -0
  780. nautobot/vpn/tests/test_filters.py +139 -0
  781. nautobot/vpn/tests/test_forms.py +294 -0
  782. nautobot/vpn/tests/test_models.py +97 -0
  783. nautobot/vpn/tests/test_views.py +281 -0
  784. nautobot/vpn/urls.py +16 -0
  785. nautobot/vpn/views.py +437 -0
  786. nautobot/wireless/filters.py +0 -8
  787. nautobot/wireless/navigation.py +3 -1
  788. nautobot/wireless/templates/wireless/wirelessnetwork_create.html +6 -6
  789. nautobot/wireless/tests/integration/test_radio_profile.py +3 -7
  790. nautobot/wireless/tests/test_api.py +1 -1
  791. {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/METADATA +5 -4
  792. {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/RECORD +802 -707
  793. {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/entry_points.txt +1 -0
  794. nautobot/core/management/commands/check_job_approval_status.py +0 -47
  795. nautobot/core/templates/search_form.html +0 -9
  796. nautobot/core/templates/utilities/templatetags/filter_form_modal.html +0 -87
  797. nautobot/dcim/filters/mixins.py +0 -354
  798. nautobot/extras/templates/extras/job_approval_request.html +0 -134
  799. nautobot/extras/templates/extras/scheduled_jobs_approval_queue_list.html +0 -28
  800. nautobot/ipam/mixins.py +0 -32
  801. nautobot/ipam/templates/ipam/inc/prefix_header_extra_content_table.html +0 -4
  802. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap-theme.css +0 -587
  803. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap-theme.css.map +0 -1
  804. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap-theme.min.css +0 -6
  805. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap-theme.min.css.map +0 -1
  806. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.css +0 -6865
  807. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.css.map +0 -1
  808. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.min.css +0 -6
  809. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.min.css.map +0 -1
  810. nautobot/project-static/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.eot +0 -0
  811. nautobot/project-static/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.svg +0 -288
  812. nautobot/project-static/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.ttf +0 -0
  813. nautobot/project-static/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.woff +0 -0
  814. nautobot/project-static/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.woff2 +0 -0
  815. nautobot/project-static/bootstrap-3.4.1-dist/js/bootstrap.js +0 -2580
  816. nautobot/project-static/bootstrap-3.4.1-dist/js/bootstrap.min.js +0 -6
  817. nautobot/project-static/bootstrap-3.4.1-dist/js/npm.js +0 -13
  818. nautobot/project-static/clipboard.js-2.0.9/clipboard.min.js +0 -7
  819. nautobot/project-static/css/base.css +0 -1040
  820. nautobot/project-static/css/dark.css +0 -282
  821. nautobot/project-static/flatpickr-4.6.9/flatpickr.min.js +0 -2
  822. nautobot/project-static/flatpickr-4.6.9/themes/light.min.css +0 -1
  823. nautobot/project-static/graphiql-1.5.16/graphiql.min.css +0 -12
  824. nautobot/project-static/graphiql-1.5.16/graphiql.min.js +0 -11
  825. nautobot/project-static/highlight.js-11.9.0/github-dark.min.css +0 -10
  826. nautobot/project-static/highlight.js-11.9.0/github.min.css +0 -10
  827. nautobot/project-static/highlight.js-11.9.0/highlight.min.js +0 -378
  828. nautobot/project-static/jquery/jquery-3.7.1.min.js +0 -2
  829. nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_444444_256x240.png +0 -0
  830. nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_555555_256x240.png +0 -0
  831. nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_777620_256x240.png +0 -0
  832. nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_777777_256x240.png +0 -0
  833. nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_cc0000_256x240.png +0 -0
  834. nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_ffffff_256x240.png +0 -0
  835. nautobot/project-static/jquery-ui-1.13.2/jquery-ui.min.css +0 -7
  836. nautobot/project-static/jquery-ui-1.13.2/jquery-ui.min.js +0 -6
  837. nautobot/project-static/jquery-ui-1.13.2/jquery-ui.structure.min.css +0 -5
  838. nautobot/project-static/jquery-ui-1.13.2/jquery-ui.theme.min.css +0 -5
  839. nautobot/project-static/js/homepage_layout.js +0 -182
  840. nautobot/project-static/js/nav_menu.js +0 -250
  841. nautobot/project-static/js/theme.js +0 -133
  842. nautobot/project-static/materialdesignicons-7.4.47/LICENSE +0 -20
  843. nautobot/project-static/materialdesignicons-7.4.47/css/materialdesignicons.min.css +0 -3
  844. nautobot/project-static/react-16.14.0/react.production.min.js +0 -32
  845. nautobot/project-static/react-dom-16.14.0/react-dom.production.min.js +0 -239
  846. nautobot/project-static/select2-4.0.13/i18n/af.js +0 -3
  847. nautobot/project-static/select2-4.0.13/i18n/ar.js +0 -3
  848. nautobot/project-static/select2-4.0.13/i18n/az.js +0 -3
  849. nautobot/project-static/select2-4.0.13/i18n/bg.js +0 -3
  850. nautobot/project-static/select2-4.0.13/i18n/bn.js +0 -3
  851. nautobot/project-static/select2-4.0.13/i18n/bs.js +0 -3
  852. nautobot/project-static/select2-4.0.13/i18n/ca.js +0 -3
  853. nautobot/project-static/select2-4.0.13/i18n/cs.js +0 -3
  854. nautobot/project-static/select2-4.0.13/i18n/da.js +0 -3
  855. nautobot/project-static/select2-4.0.13/i18n/de.js +0 -3
  856. nautobot/project-static/select2-4.0.13/i18n/dsb.js +0 -3
  857. nautobot/project-static/select2-4.0.13/i18n/el.js +0 -3
  858. nautobot/project-static/select2-4.0.13/i18n/en.js +0 -3
  859. nautobot/project-static/select2-4.0.13/i18n/es.js +0 -3
  860. nautobot/project-static/select2-4.0.13/i18n/et.js +0 -3
  861. nautobot/project-static/select2-4.0.13/i18n/eu.js +0 -3
  862. nautobot/project-static/select2-4.0.13/i18n/fa.js +0 -3
  863. nautobot/project-static/select2-4.0.13/i18n/fi.js +0 -3
  864. nautobot/project-static/select2-4.0.13/i18n/fr.js +0 -3
  865. nautobot/project-static/select2-4.0.13/i18n/gl.js +0 -3
  866. nautobot/project-static/select2-4.0.13/i18n/he.js +0 -3
  867. nautobot/project-static/select2-4.0.13/i18n/hi.js +0 -3
  868. nautobot/project-static/select2-4.0.13/i18n/hr.js +0 -3
  869. nautobot/project-static/select2-4.0.13/i18n/hsb.js +0 -3
  870. nautobot/project-static/select2-4.0.13/i18n/hu.js +0 -3
  871. nautobot/project-static/select2-4.0.13/i18n/hy.js +0 -3
  872. nautobot/project-static/select2-4.0.13/i18n/id.js +0 -3
  873. nautobot/project-static/select2-4.0.13/i18n/is.js +0 -3
  874. nautobot/project-static/select2-4.0.13/i18n/it.js +0 -3
  875. nautobot/project-static/select2-4.0.13/i18n/ja.js +0 -3
  876. nautobot/project-static/select2-4.0.13/i18n/ka.js +0 -3
  877. nautobot/project-static/select2-4.0.13/i18n/km.js +0 -3
  878. nautobot/project-static/select2-4.0.13/i18n/ko.js +0 -3
  879. nautobot/project-static/select2-4.0.13/i18n/lt.js +0 -3
  880. nautobot/project-static/select2-4.0.13/i18n/lv.js +0 -3
  881. nautobot/project-static/select2-4.0.13/i18n/mk.js +0 -3
  882. nautobot/project-static/select2-4.0.13/i18n/ms.js +0 -3
  883. nautobot/project-static/select2-4.0.13/i18n/nb.js +0 -3
  884. nautobot/project-static/select2-4.0.13/i18n/ne.js +0 -3
  885. nautobot/project-static/select2-4.0.13/i18n/nl.js +0 -3
  886. nautobot/project-static/select2-4.0.13/i18n/pl.js +0 -3
  887. nautobot/project-static/select2-4.0.13/i18n/ps.js +0 -3
  888. nautobot/project-static/select2-4.0.13/i18n/pt-BR.js +0 -3
  889. nautobot/project-static/select2-4.0.13/i18n/pt.js +0 -3
  890. nautobot/project-static/select2-4.0.13/i18n/ro.js +0 -3
  891. nautobot/project-static/select2-4.0.13/i18n/ru.js +0 -3
  892. nautobot/project-static/select2-4.0.13/i18n/sk.js +0 -3
  893. nautobot/project-static/select2-4.0.13/i18n/sl.js +0 -3
  894. nautobot/project-static/select2-4.0.13/i18n/sq.js +0 -3
  895. nautobot/project-static/select2-4.0.13/i18n/sr-Cyrl.js +0 -3
  896. nautobot/project-static/select2-4.0.13/i18n/sr.js +0 -3
  897. nautobot/project-static/select2-4.0.13/i18n/sv.js +0 -3
  898. nautobot/project-static/select2-4.0.13/i18n/th.js +0 -3
  899. nautobot/project-static/select2-4.0.13/i18n/tk.js +0 -3
  900. nautobot/project-static/select2-4.0.13/i18n/tr.js +0 -3
  901. nautobot/project-static/select2-4.0.13/i18n/uk.js +0 -3
  902. nautobot/project-static/select2-4.0.13/i18n/vi.js +0 -3
  903. nautobot/project-static/select2-4.0.13/i18n/zh-CN.js +0 -3
  904. nautobot/project-static/select2-4.0.13/i18n/zh-TW.js +0 -3
  905. nautobot/project-static/select2-4.0.13/select2.min.css +0 -1
  906. nautobot/project-static/select2-4.0.13/select2.min.js +0 -2
  907. nautobot/project-static/select2-bootstrap-0.1.0-beta.10/select2-bootstrap.min.css +0 -7
  908. nautobot/project-static/subscriptions-transport-ws-0.9.18/client.min.js +0 -8
  909. nautobot/project-static/whatwg-fetch-3.6.2/fetch.umd.min.js +0 -8
  910. nautobot/virtualization/templates/virtualization/cluster_add_devices.html +0 -37
  911. /nautobot/extras/{filters/customfields.py → filter_mixins_customfields.py} +0 -0
  912. /nautobot/project-static/{materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.ttf → dist/1fcc36272ea3e53d0031.ttf} +0 -0
  913. /nautobot/project-static/{materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.eot → dist/2146c3c82b553977abc7.eot} +0 -0
  914. /nautobot/project-static/{materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff → dist/e55a20c80650829ec5fd.woff} +0 -0
  915. /nautobot/project-static/{materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff2 → dist/ec024da790d2972da002.woff2} +0 -0
  916. /nautobot/tenancy/{filters/mixins.py → filter_mixins.py} +0 -0
  917. {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/LICENSE.txt +0 -0
  918. {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/NOTICE +0 -0
  919. {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/WHEEL +0 -0
@@ -0,0 +1,776 @@
1
+ import argparse
2
+ import logging
3
+ import os
4
+ import re
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ def _fix_breadcrumb_items(html: str, stats: dict) -> str:
10
+ """
11
+ Adds 'breadcrumb-item' class to <li> elements within <ol class="breadcrumb"> using regex.
12
+ Modifies the HTML string directly.
13
+ """
14
+
15
+ def ol_replacer(ol_match):
16
+ ol_tag = ol_match.group(0)
17
+ # Replace all <li...> inside this <ol>
18
+ ol_tag_new = _add_breadcrumb_item_to_li(ol_tag, stats)
19
+ return ol_tag_new
20
+
21
+ # Only operate inside <ol class="breadcrumb">...</ol>
22
+ pattern = re.compile(
23
+ r'<ol\s+class=(["\'])(?:[^"\']*\s)?breadcrumb(?:\s[^"\']*)?\1[^>]*>.*?</ol>', re.DOTALL | re.IGNORECASE
24
+ )
25
+ return pattern.sub(ol_replacer, html)
26
+
27
+
28
+ def _fix_nav_item_to_li(html: str, stats: dict, file_path=None) -> str:
29
+ """
30
+ Adds 'nav-item' class to all <li> tags in the given HTML string,
31
+ and 'nav-link' to <a> or <button> inside, moving 'active' as needed.
32
+ Handles Django template logic for active.
33
+ """
34
+
35
+ def li_replacer(match):
36
+ li_tag = match.group(0)
37
+ # If Django template logic is found, notify user and skip auto-fix
38
+ if re.search(r"{%.*%}", li_tag):
39
+ if "manual_nav_template_lines" not in stats:
40
+ stats["manual_nav_template_lines"] = []
41
+ # Get line number and character position of li_tag
42
+ html_lines = html.splitlines()
43
+ for i, line in enumerate(html_lines):
44
+ if li_tag in line:
45
+ # Append line number, character position and file path for easier identification
46
+ stats["manual_nav_template_lines"].append(
47
+ f"{file_path}:{i + 1}:{line.index(li_tag)} - Please review manually '{li_tag}'"
48
+ )
49
+ break
50
+ else:
51
+ stats["manual_nav_template_lines"].append(f"{file_path} - Please review manually '{li_tag}'")
52
+ return li_tag
53
+
54
+ # Add nav-item to <li>
55
+ class_attr_match = re.search(r'class=(["\'])(.*?)\1', li_tag)
56
+ if class_attr_match:
57
+ classes = class_attr_match.group(2).split()
58
+ if not any("nav-item" in _class for _class in classes):
59
+ classes.append("nav-item")
60
+ stats["nav_items"] += 1
61
+ new_class_attr = f'class="{" ".join(classes)}"'
62
+ li_tag = re.sub(r'class=(["\'])(.*?)\1', new_class_attr, li_tag, count=1)
63
+ else:
64
+ li_tag = re.sub(r"<li(\s|>)", r'<li class="nav-item"\1', li_tag, count=1)
65
+ stats["nav_items"] += 1
66
+
67
+ # Move plain active from <li> to child <a> or <button>
68
+ li_tag, n_active = re.subn(
69
+ r'(<li[^>]+)class=(["\'])([^"\']*\bactive\b[^"\']*)\2',
70
+ lambda m: f'{m.group(1)}class="{" ".join([c for c in m.group(3).split() if c != "active"])}"'
71
+ if "active" in m.group(3).split()
72
+ else m.group(0),
73
+ li_tag,
74
+ )
75
+ if n_active:
76
+
77
+ def add_active_to_child(child_match):
78
+ child_tag = child_match.group(0)
79
+ child_class_match = re.search(r'class=(["\'])(.*?)\1', child_tag)
80
+ if child_class_match:
81
+ child_classes = child_class_match.group(2).split()
82
+ # We use substring rather than exact match for cases with inline Django template fragements like:
83
+ # <a class="nav-link{% if some_condition %} active{% endif %}">
84
+ # where our naive split() call above would create "navlink{%" and "active{%" class entries
85
+ if not any("nav-link" in child_class for child_class in child_classes):
86
+ child_classes.append("nav-link")
87
+ stats["nav_items"] += 1
88
+ if not any("active" in child_class for child_class in child_classes):
89
+ child_classes.append("active")
90
+ stats["nav_items"] += 1
91
+ new_child_class = f'class="{" ".join(child_classes)}"'
92
+ child_tag = re.sub(r'class=(["\'])(.*?)\1', new_child_class, child_tag, count=1)
93
+ else:
94
+ child_tag = re.sub(r"<(a|button)", r'<\1 class="nav-link active"', child_tag, count=1)
95
+ stats["nav_items"] += 1
96
+ return child_tag
97
+
98
+ li_tag = re.sub(r"<(a|button)[^>]*>.*?</\1>", add_active_to_child, li_tag, flags=re.DOTALL)
99
+
100
+ # Always add nav-link to <a> or <button> if not present
101
+ def add_nav_link(child_match):
102
+ child_tag = child_match.group(0)
103
+ child_class_match = re.search(r'class=(["\'])(.*?)\1', child_tag)
104
+ if child_class_match:
105
+ child_classes = child_class_match.group(2).split()
106
+ if not any("nav-link" in child_class for child_class in child_classes):
107
+ child_classes.append("nav-link")
108
+ new_child_class = f'class="{" ".join(child_classes)}"'
109
+ child_tag = re.sub(r'class=(["\'])(.*?)\1', new_child_class, child_tag, count=1)
110
+ else:
111
+ child_tag = re.sub(r"<(a|button)", r'<\1 class="nav-link"', child_tag, count=1)
112
+ return child_tag
113
+
114
+ li_tag = re.sub(r"<(a|button)[^>]*>.*?</\1>", add_nav_link, li_tag, flags=re.DOTALL)
115
+
116
+ return li_tag
117
+
118
+ return re.sub(r"<li[^>]*>.*?</li>", li_replacer, html, flags=re.DOTALL)
119
+
120
+
121
+ def _fix_nav_tabs_items(html: str, stats: dict, file_path=None) -> str:
122
+ """
123
+ Applies nav-item/nav-link logic to <li> elements within <ul class="nav nav-tabs"> using regex.
124
+ """
125
+
126
+ def ul_replacer(ul_match):
127
+ ul_tag = ul_match.group(0)
128
+ ul_tag_new = _fix_nav_item_to_li(ul_tag, stats, file_path=file_path)
129
+ return ul_tag_new
130
+
131
+ pattern = re.compile(
132
+ r'<ul\s+class=(["\'])(?:[^"\']*\s)?nav(?:\s[^"\']*)?nav-tabs(?:\s[^"\']*)?\1[^>]*>.*?</ul>',
133
+ re.DOTALL | re.IGNORECASE,
134
+ )
135
+ return pattern.sub(ul_replacer, html)
136
+
137
+
138
+ def _fix_dropdown_lis(html: str, stats: dict, file_path=None) -> str:
139
+ """Adds 'dropdown-item' class to all <li><a>...</a></li> tags in the given HTML string."""
140
+
141
+ def a_replacer(match):
142
+ a_tag = match.group(0)
143
+ # If Django template logic is found, notify user and skip auto-fix
144
+ if re.search(r"{%.*%}", a_tag):
145
+ if "manual_nav_template_lines" not in stats:
146
+ stats["manual_nav_template_lines"] = []
147
+ # Get line number and character position of li_tag
148
+ html_lines = html.splitlines()
149
+ for i, line in enumerate(html_lines):
150
+ if a_tag in line:
151
+ # Append line number, character position and file path for easier identification
152
+ stats["manual_nav_template_lines"].append(
153
+ f"{file_path}:{i + 1}:{line.index(a_tag)} - Please review manually '{a_tag}'"
154
+ )
155
+ break
156
+ else:
157
+ stats["manual_nav_template_lines"].append(f"{file_path} - Please review manually '{a_tag}'")
158
+ return a_tag
159
+
160
+ # Add dropdown-item to <a>
161
+ class_attr_match = re.search(r"""class=(["'])(.*?)\1""", a_tag)
162
+ if class_attr_match:
163
+ classes = class_attr_match.group(2).split()
164
+ if not any("dropdown-item" in _class for _class in classes):
165
+ classes.append("dropdown-item")
166
+ stats["dropdown_items"] += 1
167
+ new_class_attr = f'class="{" ".join(classes)}"'
168
+ a_tag = re.sub(r"""class=(["'])(.*?)\1""", new_class_attr, a_tag, count=1)
169
+ else:
170
+ a_tag = re.sub(r"<a(\s|>)", r'<a class="dropdown-item"\1', a_tag, count=1)
171
+ stats["dropdown_items"] += 1
172
+
173
+ return a_tag
174
+
175
+ return re.sub("<a[^>]*>.*?</a>", a_replacer, html, flags=re.DOTALL)
176
+
177
+
178
+ def _fix_dropdown_items(html: str, stats: dict, file_path=None) -> str:
179
+ """Ensures that all <li> elements within <ul class="dropdown-menu"> have class="dropdown-item" as appropriate."""
180
+
181
+ def ul_replacer(ul_match):
182
+ ul_tag = ul_match.group(0)
183
+ ul_tag_new = _fix_dropdown_lis(ul_tag, stats, file_path=file_path)
184
+ return ul_tag_new
185
+
186
+ pattern = re.compile(
187
+ r"""<ul\s+class=(["'])(?:[^"']*\s)?dropdown-menu(?:\s[^"']*)?\1[^>]*>.*?</ul>""",
188
+ re.DOTALL | re.IGNORECASE,
189
+ )
190
+ return pattern.sub(ul_replacer, html)
191
+
192
+
193
+ def _fix_extra_nav_tabs_block(html_string: str, stats: dict, file_path: str) -> str:
194
+ """
195
+ Finds {% block extra_nav_tabs %} blocks and adds nav-item/nav-link to <li> tags inside using regex.
196
+ """
197
+ block_pattern = re.compile(
198
+ r"({%\s*block\s+extra_nav_tabs\s*%})(.*?)({%\s*endblock\s+extra_nav_tabs\s*%})", flags=re.DOTALL | re.IGNORECASE
199
+ )
200
+
201
+ def process_match(match):
202
+ block_start_tag = match.group(1)
203
+ block_inner_content = match.group(2)
204
+ block_end_tag = match.group(3)
205
+ new_inner_content = _fix_nav_item_to_li(block_inner_content, stats, file_path=file_path)
206
+ return f"{block_start_tag}{new_inner_content}{block_end_tag}"
207
+
208
+ return block_pattern.sub(process_match, html_string)
209
+
210
+
211
+ def _fix_panel_classes(html: str, stats: dict) -> str:
212
+ """
213
+ Converts Bootstrap 3 panel classes to Bootstrap 5 card equivalents using regex.
214
+ Handles panel-primary context for panel-heading.
215
+ """
216
+ # Pattern to match all class attributes
217
+ class_attr_pattern = re.compile(r'class="([^"]*)"')
218
+ # Patterns for exact class matches
219
+ panel_pattern = re.compile(r"\bpanel\b(?!-)")
220
+ panel_color_patterns = {
221
+ "primary": re.compile(r"\bpanel-primary\b"),
222
+ "success": re.compile(r"\bpanel-success\b"),
223
+ "info": re.compile(r"\bpanel-info\b"),
224
+ "warning": re.compile(r"\bpanel-warning\b"),
225
+ "danger": re.compile(r"\bpanel-danger\b"),
226
+ }
227
+ panel_heading_pattern = re.compile(r"\bpanel-heading\b")
228
+ panel_body_pattern = re.compile(r"\bpanel-body\b")
229
+ panel_footer_pattern = re.compile(r"\bpanel-footer\b")
230
+ panel_title_pattern = re.compile(r"\bpanel-title\b")
231
+
232
+ result = []
233
+ last = 0
234
+ is_color_panel = False
235
+ panel_color = None
236
+
237
+ for match in class_attr_pattern.finditer(html):
238
+ start, end = match.span()
239
+ class_str = match.group(1)
240
+ classes = class_str.split()
241
+ new_classes = []
242
+ changed = False
243
+
244
+ if any(panel_pattern.fullmatch(c) for c in classes):
245
+ # If main panel class is found, reset color flag
246
+ is_color_panel = False
247
+ panel_color = None
248
+ # Detect color panel
249
+ for color, color_pat in panel_color_patterns.items():
250
+ if any(color_pat.fullmatch(c) for c in classes):
251
+ is_color_panel = True
252
+ panel_color = color
253
+ break
254
+
255
+ for c in classes:
256
+ if panel_pattern.fullmatch(c):
257
+ new_classes.append("card")
258
+ changed = True
259
+ stats["panel_classes"] += 1
260
+ elif any(color_pat.fullmatch(c) for color_pat in panel_color_patterns.values()):
261
+ # Add border color for panel-color
262
+ new_classes.append(f"border-{panel_color}")
263
+ changed = True
264
+ stats["panel_classes"] += 1
265
+ elif panel_heading_pattern.fullmatch(c):
266
+ new_classes.append("card-header")
267
+ changed = True
268
+ stats["panel_classes"] += 1
269
+ if is_color_panel and panel_color:
270
+ new_classes.append(f"bg-{panel_color}-subtle")
271
+ new_classes.append(f"border-{panel_color}")
272
+ new_classes.append("text-body")
273
+ elif panel_body_pattern.fullmatch(c):
274
+ new_classes.append("card-body")
275
+ changed = True
276
+ stats["panel_classes"] += 1
277
+ elif panel_footer_pattern.fullmatch(c):
278
+ new_classes.append("card-footer")
279
+ if is_color_panel and panel_color:
280
+ new_classes.append(f"bg-{panel_color}-subtle")
281
+ new_classes.append(f"border-{panel_color}")
282
+ new_classes.append("text-body")
283
+ changed = True
284
+ stats["panel_classes"] += 1
285
+ elif panel_title_pattern.fullmatch(c):
286
+ new_classes.append("card-title")
287
+ changed = True
288
+ stats["panel_classes"] += 1
289
+ else:
290
+ new_classes.append(c)
291
+
292
+ result.append(html[last:start])
293
+ if changed:
294
+ result.append(f'class="{" ".join(new_classes)}"')
295
+ else:
296
+ result.append(match.group(0))
297
+ last = end
298
+
299
+ result.append(html[last:])
300
+ return "".join(result)
301
+
302
+
303
+ def _replace_classes(html_string: str, replacements: dict, stats: dict, file_path: str) -> str:
304
+ """
305
+ Replaces class names in html_string according to the replacements dict.
306
+ Each key is replaced with its value using regex word boundaries, case-insensitive.
307
+ Increments stats['replacements'] by the actual number of replacements made.
308
+ """
309
+
310
+ def class_attr_replacer(match):
311
+ class_value = match.group(1)
312
+ original = class_value
313
+ if "{%" in class_value or "%}" in class_value:
314
+ if any(
315
+ re.search(r"\b(?<!-)" + re.escape(search) + r"\b(?!-)", class_value) for search in replacements.keys()
316
+ ):
317
+ if "manual_nav_template_lines" not in stats:
318
+ stats["manual_nav_template_lines"] = []
319
+ stats["manual_nav_template_lines"].append(f"{file_path} - Please review manually '{class_value}'")
320
+ else:
321
+ for search, replace in replacements.items():
322
+ # Only replace whole words in class attribute
323
+ pattern = r"\b(?<!-)" + re.escape(search) + r"\b(?!-)"
324
+ class_value, num_replacements = re.subn(pattern, replace, class_value, flags=re.IGNORECASE)
325
+ if num_replacements > 0:
326
+ logger.debug(
327
+ 'Replaced "%s" with "%s" (%d times) in class="%s"', search, replace, num_replacements, original
328
+ )
329
+ stats["replacements"] += num_replacements
330
+ return f'class="{class_value}"'
331
+
332
+ # Only replace within class attributes
333
+ return re.sub(r'class="([^"]*)"', class_attr_replacer, html_string)
334
+
335
+
336
+ def _replace_attributes(html_string: str, replacements: dict, stats: dict) -> str:
337
+ """
338
+ Replaces attribute names in html_string according to the replacements dict.
339
+ Each key is replaced with its value using regex word boundaries, case-insensitive.
340
+ Increments stats['replacements'] by the actual number of replacements made.
341
+ """
342
+ for search, replace in replacements.items():
343
+ # Use re.subn to get the number of replacements
344
+ pattern = r"\b" + re.escape(search) + r"\b"
345
+ html_string, num_replacements = re.subn(pattern, replace, html_string, flags=re.IGNORECASE)
346
+ if num_replacements > 0:
347
+ logger.debug('Replaced "%s" with "%s" (%d times).', search, replace, num_replacements)
348
+ stats["replacements"] += num_replacements
349
+ return html_string
350
+
351
+
352
+ # TODO: Fix this to use regex only, but leaving for now due to the parent span logic. Not necessary for the bootstrap upgrade.
353
+ # def _convert_i_to_span_mdi(soup: BeautifulSoup, stats: dict) -> str:
354
+ # """
355
+ # Converts <i> tags with mdi classes to <span> tags.
356
+ # """
357
+ # for icon in soup.find_all('i', class_=re.compile(r'(?<!-)\bmdi\b(?!-)', re.IGNORECASE)):
358
+ # # If parent is already a span, move class and data to parent and remove <i>
359
+ # if icon.parent.name == 'span':
360
+ # # Ensure parent has a class attribute
361
+ # if not icon.parent.has_attr('class'):
362
+ # icon.parent['class'] = []
363
+ # for class_name in icon.get('class', []):
364
+ # if class_name not in icon.parent.get('class', []):
365
+ # icon.parent['class'].append(class_name)
366
+ # for attr, value in icon.attrs.items():
367
+ # if attr != 'class':
368
+ # icon.parent[attr] = value
369
+ # icon.decompose()
370
+ # stats['replacements'] += 1
371
+ # continue
372
+ # icon.name = 'span'
373
+ # stats['replacements'] += 1
374
+ # print(f"DEBUG: Converted <i> to <span> for mdi icon: {icon.prettify().strip().splitlines()[0]}...")
375
+
376
+ # return str(soup)
377
+
378
+
379
+ def _convert_caret_in_span_to_mdi(html: str, stats: dict) -> str:
380
+ """
381
+ Converts <span> elements with a caret class to use mdi icons.
382
+ Replaces <span class="caret ...">...</span> with <span class="mdi mdi-chevron-down">...</span>
383
+ """
384
+ # Pattern to match <span ...class="...caret...">...</span>
385
+ pattern = re.compile(r'<span([^>]*)class="([^"]*\bcaret\b[^"]*)"([^>]*)>(.*?)</span>', re.DOTALL | re.IGNORECASE)
386
+
387
+ def replacer(match):
388
+ before = match.group(1)
389
+ class_attr = match.group(2)
390
+ after = match.group(3)
391
+ inner_html = match.group(4)
392
+ classes = [c for c in class_attr.split() if c != "caret"]
393
+ classes += ["mdi", "mdi-chevron-down"]
394
+ stats["replacements"] += 1
395
+ return f'<span{before}class="{" ".join(classes)}"{after}>{inner_html}</span>'
396
+
397
+ return pattern.sub(replacer, html)
398
+
399
+
400
+ def _convert_hover_copy_buttons(html: str, stats: dict) -> str:
401
+ """
402
+ Converts hover copy buttons to the new design (Bootstrap 5).
403
+ """
404
+ # Pattern to match <button ...hover_copy_button...>...</button>
405
+ button_pattern = re.compile(
406
+ r'(<button[^>]*class="[^"]*\bhover_copy_button\b[^"]*"[^>]*>)(.*?)(</button>)', re.DOTALL | re.IGNORECASE
407
+ )
408
+
409
+ def button_replacer(match):
410
+ open_tag = match.group(1)
411
+ inner_html = match.group(2)
412
+ close_tag = match.group(3)
413
+
414
+ # Fix the class attribute
415
+ def class_replacer(m):
416
+ classes = m.group(1).split()
417
+ classes = [c for c in classes if c != "hover_copy_button"]
418
+ classes = ["btn-secondary" if c == "btn-default" else c for c in classes]
419
+ if "nb-btn-inline-hover" not in classes:
420
+ classes.append("nb-btn-inline-hover")
421
+ return f'class="{" ".join(classes)}"'
422
+
423
+ open_tag = re.sub(r'class="([^"]*)"', class_replacer, open_tag)
424
+
425
+ # Ensure mdi icon has aria-hidden="true"
426
+ def mdi_replacer(m):
427
+ tag = m.group(0)
428
+ if "aria-hidden=" not in tag:
429
+ tag = tag.replace("<span", '<span aria-hidden="true"', 1)
430
+ return tag
431
+
432
+ inner_html = re.sub(r'<span([^>]*)class="([^"]*\bmdi\b[^"]*)"([^>]*)>', mdi_replacer, inner_html)
433
+
434
+ # Add visually-hidden Copy span if not present
435
+ if not re.search(
436
+ r'<span[^>]*class="[^"]*\bvisually-hidden\b[^"]*"[^>]*>Copy</span>', inner_html, re.IGNORECASE
437
+ ):
438
+ inner_html += '<span class="visually-hidden">Copy</span>'
439
+
440
+ stats["replacements"] += 1
441
+ return f"{open_tag}{inner_html}{close_tag}"
442
+
443
+ return button_pattern.sub(button_replacer, html)
444
+
445
+
446
+ def _remove_classes(html: str, classes_to_remove: list[str], stats: dict) -> str:
447
+ """
448
+ Removes each class in classes_to_remove from all class attributes in the HTML string.
449
+ """
450
+ # Build a regex pattern to match any of the classes as a whole word in a class attribute
451
+ pattern = re.compile(r'class="([^"]*)"')
452
+
453
+ def class_replacer(match):
454
+ class_attr = match.group(1)
455
+ classes = class_attr.split()
456
+ new_classes = [c for c in classes if c not in classes_to_remove]
457
+ removed = len(classes) - len(new_classes)
458
+ if removed > 0:
459
+ stats["replacements"] += removed
460
+ logger.debug(
461
+ 'Removed %d class(es) %s from class="%s" → class="%s"',
462
+ removed,
463
+ classes_to_remove,
464
+ class_attr,
465
+ " ".join(new_classes),
466
+ )
467
+ # If no classes left, remove the class attribute entirely
468
+ if new_classes:
469
+ return f'class="{" ".join(new_classes)}"'
470
+ else:
471
+ return 'class=""' # Return empty class attribute to be cleaned up later
472
+ return f'class="{class_attr}"'
473
+
474
+ # Replace all class attributes in the HTML
475
+ html = pattern.sub(class_replacer, html)
476
+ # Remove empty class attributes and leading whitespace if leading whitespace exists
477
+ html = re.sub(r'\sclass=""', "", html)
478
+ return html
479
+
480
+
481
+ # --- Rule Function (Regex-based for specific block content) ---
482
+
483
+
484
+ def _add_breadcrumb_item_to_li(html: str, stats: dict) -> str:
485
+ """
486
+ Adds 'breadcrumb-item' class to all <li> tags in the given HTML string.
487
+ """
488
+
489
+ def li_replacer(match):
490
+ li_tag = match.group(0)
491
+ # If <li> already has class attribute
492
+ class_attr_match = re.search(r'class=(["\'])(.*?)\1', li_tag, flags=re.IGNORECASE)
493
+ if class_attr_match:
494
+ classes = class_attr_match.group(2).split()
495
+ if "breadcrumb-item" not in classes:
496
+ classes.append("breadcrumb-item")
497
+ stats["breadcrumb_items"] += 1
498
+ # Replace the class attribute with the new value
499
+ new_class_attr = f'class="{" ".join(classes)}"'
500
+ li_tag = re.sub(r'class=(["\'])(.*?)\1', new_class_attr, li_tag, flags=re.IGNORECASE)
501
+ else:
502
+ # Add class attribute with breadcrumb-item
503
+ li_tag = re.sub(r"<li", '<li class="breadcrumb-item"', li_tag, count=1)
504
+ stats["breadcrumb_items"] += 1
505
+ return li_tag
506
+
507
+ # Replace all <li ...> tags
508
+ return re.sub(r"<li[^>]*>", li_replacer, html)
509
+
510
+
511
+ def _fix_extra_breadcrumbs_block(html_string: str, stats: dict) -> str:
512
+ """
513
+ Finds {% block extra_breadcrumbs %} blocks and adds 'breadcrumb-item' to <li> tags inside using regex.
514
+ """
515
+ block_pattern = re.compile(
516
+ r"({%\s*block\s+extra_breadcrumbs\s*%})(.*?)({%\s*endblock\s+extra_breadcrumbs\s*%})",
517
+ flags=re.DOTALL | re.IGNORECASE,
518
+ )
519
+
520
+ def process_match(match):
521
+ block_start_tag = match.group(1)
522
+ block_inner_content = match.group(2)
523
+ block_end_tag = match.group(3)
524
+ new_inner_content = _add_breadcrumb_item_to_li(block_inner_content, stats)
525
+ return f"{block_start_tag}{new_inner_content}{block_end_tag}"
526
+
527
+ return block_pattern.sub(process_match, html_string)
528
+
529
+
530
+ def _fix_breadcrumbs_block(html_string: str, stats: dict) -> str:
531
+ """
532
+ Finds {% block breadcrumbs %} blocks and adds 'breadcrumb-item' to <li> tags inside using regex.
533
+ """
534
+ block_pattern = re.compile(
535
+ r"({%\s*block\s+breadcrumbs\s*%})(.*?)({%\s*endblock\s+breadcrumbs\s*%})", flags=re.DOTALL | re.IGNORECASE
536
+ )
537
+
538
+ def process_match(match):
539
+ block_start_tag = match.group(1)
540
+ block_inner_content = match.group(2)
541
+ block_end_tag = match.group(3)
542
+ new_inner_content = _add_breadcrumb_item_to_li(block_inner_content, stats)
543
+ return f"{block_start_tag}{new_inner_content}{block_end_tag}"
544
+
545
+ return block_pattern.sub(process_match, html_string)
546
+
547
+
548
+ # --- Main Conversion Function ---
549
+
550
+
551
+ def convert_bootstrap_classes(html_input: str, file_path: str) -> tuple[str, dict]:
552
+ """
553
+ Applies various Bootstrap 3 to 5 conversion rules to the HTML content.
554
+ """
555
+ current_html = html_input # Start with the original HTML
556
+
557
+ # Initialize stats
558
+ stats = {
559
+ "replacements": 0,
560
+ "extra_breadcrumbs": 0,
561
+ "breadcrumb_items": 0,
562
+ "nav_items": 0,
563
+ "dropdown_items": 0,
564
+ "panel_classes": 0,
565
+ "manual_nav_template_lines": [],
566
+ }
567
+
568
+ # --- Stage 1: Apply rules that work directly on the HTML string (simple string/regex replacements) ---
569
+ class_replacements = {
570
+ "pull-left": "float-start",
571
+ "pull-right": "float-end",
572
+ "center-block": "d-block mx-auto", # Bootstrap 5 uses mx-auto for centering
573
+ "btn-xs": "btn-sm",
574
+ "btn-lg": "btn", # btn-lg is supported in Bootstrap 5 but not meaningful different from btn in Nautobot's theme
575
+ "btn-default": "btn-secondary",
576
+ "checkbox": "form-check",
577
+ "checkbox-inline": "form-check-input",
578
+ "close": "btn-close",
579
+ "control-label": "col-form-label",
580
+ "dropdown-menu-right": "dropdown-menu-end",
581
+ "form-control-static": "form-control-plaintext",
582
+ "form-group": "mb-10 d-flex justify-content-center",
583
+ "help-block": "form-text",
584
+ "label label-default": "badge bg-default", # Bootstrap 5 uses general background classes instead of label-default
585
+ "label label-primary": "badge bg-primary",
586
+ "label label-success": "badge bg-success",
587
+ "label label-warning": "badge bg-warning",
588
+ "label label-danger": "badge bg-danger",
589
+ "label label-info": "badge bg-info",
590
+ "label label-transparent": "badge bg-transparent",
591
+ "text-left": "text-start",
592
+ "text-muted": "text-secondary",
593
+ "text-right": "text-end",
594
+ "sr-only": "visually-hidden", # Bootstrap 5 uses visually-hidden instead of sr-only
595
+ "sr-only-focusable": "visually-hidden-focusable", # Bootstrap 5 uses visually-hidden-focusable instead of sr-only-focusable
596
+ "accordion-toggle": "nb-collapse-toggle", # Custom class to handle accordion toggles
597
+ "banner-bottom": "nb-banner-bottom", # Custom class to handle bottom banners
598
+ "color-block": "nb-color-block", # Custom class to handle color blocks
599
+ "editor-container": "nb-editor-container", # Custom class to handle editor containers
600
+ "loading": "nb-loading", # Custom class to handle loading indicators
601
+ "required": "nb-required", # Custom class to handle required fields
602
+ "noprint": "d-print-none", # Bootstrap 5 uses d-print-none instead of noprint
603
+ "report-stats": "nb-report-stats", # Custom class to handle report stats
604
+ "right-side-panel": "nb-right-side-panel", # Custom class to handle right side panels
605
+ "software-image-hierarchy": "nb-software-image-hierarchy", # Custom class to handle software image hierarchy
606
+ "tree-hierarchy": "nb-tree-hierarchy", # Custom class to handle tree hierarchy
607
+ "tiles": "nb-tiles", # Custom class
608
+ "tile": "nb-tile", # Custom class
609
+ "tile-description": "nb-tile-description", # Custom class
610
+ "tile-footer": "nb-tile-footer", # Custom class
611
+ "tile-header": "nb-tile-header", # Custom class
612
+ "table-headings": "nb-table-headings", # Custom class
613
+ "description": "nb-description", # Custom class
614
+ "style-line": "nb-style-line", # Custom class
615
+ }
616
+ # Add column offset fixup, e.g. col-sm-offset-4 --> offset-sm-4
617
+ for bkpt in ["xs", "sm", "md", "lg", "xl", "xxl"]:
618
+ for size in range(0, 13):
619
+ class_replacements[f"col-{bkpt}-offset-{size}"] = f"offset-{bkpt}-{size}"
620
+
621
+ attribute_replacements = {
622
+ "data-toggle": "data-bs-toggle", # Bootstrap 5 uses data-bs-* attributes
623
+ "data-dismiss": "data-bs-dismiss", # Bootstrap 5 uses data-bs-* attributes
624
+ "data-target": "data-bs-target", # Bootstrap 5 uses data-bs-* attributes
625
+ "data-title": "data-bs-title", # Bootstrap 5 uses data-bs-* attributes
626
+ "data-backdrop": "data-bs-backdrop", # Bootstrap 5 uses data-bs-* attributes
627
+ }
628
+
629
+ current_html = _replace_attributes(current_html, attribute_replacements, stats)
630
+ current_html = _replace_classes(current_html, class_replacements, stats, file_path=file_path)
631
+ current_html = _fix_extra_breadcrumbs_block(current_html, stats)
632
+ current_html = _fix_breadcrumbs_block(current_html, stats)
633
+ current_html = _fix_extra_nav_tabs_block(current_html, stats, file_path=file_path)
634
+ current_html = _fix_breadcrumb_items(current_html, stats)
635
+ current_html = _fix_panel_classes(current_html, stats)
636
+ current_html = _remove_classes(
637
+ current_html, ["powered-by-nautobot", "inline-color-block", "panel-default", "hover_copy"], stats
638
+ ) # Remove small form/input classes
639
+ current_html = _convert_caret_in_span_to_mdi(current_html, stats)
640
+ current_html = _convert_hover_copy_buttons(current_html, stats)
641
+ current_html = _fix_nav_tabs_items(current_html, stats, file_path=file_path)
642
+ current_html = _fix_dropdown_items(current_html, stats, file_path=file_path)
643
+
644
+ return current_html, stats
645
+
646
+
647
+ # --- File Processing ---
648
+
649
+
650
+ def fix_html_files_in_directory(directory: str, resize=False) -> None:
651
+ """
652
+ Recursively finds all .html files in the given directory, applies convert_bootstrap_classes,
653
+ and overwrites each file with the fixed content. If resize is True, it will only change the
654
+ breakpoints (This should only be done once.).
655
+ """
656
+
657
+ totals = {
658
+ k: 0
659
+ for k in [
660
+ "replacements",
661
+ "extra_breadcrumbs",
662
+ "breadcrumb_items",
663
+ "nav_items",
664
+ "dropdown_items",
665
+ "panel_classes",
666
+ "resizing_xs",
667
+ ]
668
+ }
669
+ # Breakpoints that are not xs do not count as failures in djlint, so we keep a separate counter
670
+ resizing_other = 0
671
+
672
+ if os.path.isfile(directory):
673
+ only_filename = os.path.basename(directory)
674
+ directory = os.path.dirname(directory)
675
+ else:
676
+ only_filename = None
677
+
678
+ # Define the breakpoint mapping
679
+ breakpoint_map = {"xs": "sm", "sm": "md", "md": "lg", "lg": "xl", "xl": "xxl"}
680
+
681
+ for root, _, files in os.walk(directory):
682
+ for filename in files:
683
+ if only_filename and only_filename != filename:
684
+ continue
685
+ if filename.lower().endswith(".html"):
686
+ file_path = os.path.join(root, filename)
687
+ logger.info("Processing: %s", file_path)
688
+ with open(file_path, "r", encoding="utf-8") as f:
689
+ original_content = f.read()
690
+
691
+ content = original_content
692
+
693
+ if resize:
694
+ # If resize is True, we only change the breakpoints
695
+ # This is a one-time operation to adjust the breakpoints.
696
+ logger.info("Resizing Breakpoints: %s", file_path)
697
+
698
+ resizing_other = 0
699
+
700
+ # Iterate from the highest breakpoint to the lowest
701
+ for bkpt in ["xl", "lg", "md", "sm", "xs"]:
702
+ # Replace with regex, e.g., col-xs-12 → col-sm-12
703
+ regex = re.compile(rf"(\bcol-{bkpt})([a-zA-Z0-9-]*)")
704
+
705
+ def regex_repl(m, captured_bkpt=bkpt):
706
+ nonlocal resizing_other
707
+ if captured_bkpt == "xs":
708
+ totals["resizing_xs"] += 1
709
+ else:
710
+ resizing_other += 1
711
+ new_bkpt = breakpoint_map[captured_bkpt]
712
+ return f"col-{new_bkpt}{m.group(2)}"
713
+
714
+ content = regex.sub(regex_repl, content)
715
+
716
+ fixed_content, stats = convert_bootstrap_classes(content, file_path=file_path)
717
+
718
+ with open(file_path, "w", encoding="utf-8") as f:
719
+ f.write(fixed_content)
720
+ logger.info("Fixed: %s", file_path)
721
+
722
+ if any(stats.values()):
723
+ print(f"→ {os.path.relpath(file_path, directory)}: ", end="")
724
+ if stats["replacements"]:
725
+ print(f"{stats['replacements']} class replacements, ", end="")
726
+ if stats["extra_breadcrumbs"]:
727
+ print(f"{stats['extra_breadcrumbs']} extra-breadcrumbs, ", end="")
728
+ if stats["breadcrumb_items"]:
729
+ print(f"{stats['breadcrumb_items']} breadcrumb-items, ", end="")
730
+ if stats["nav_items"]:
731
+ print(f"{stats['nav_items']} nav-items, ", end="")
732
+ if stats["dropdown_items"]:
733
+ print(f"{stats['dropdown_items']} dropdown-items, ", end="")
734
+ if stats["panel_classes"]:
735
+ print(f"{stats['panel_classes']} panel replacements, ", end="")
736
+ print()
737
+
738
+ if stats.get("manual_nav_template_lines"):
739
+ print(" !!! Manual review needed for nav-item fixes at:")
740
+ for line in stats["manual_nav_template_lines"]:
741
+ print(f" - {line}")
742
+ for k, v in stats.items():
743
+ if k in totals:
744
+ totals[k] += v
745
+
746
+ # Global summary
747
+ total_issues = sum(totals.values())
748
+ print("=== Global Summary ===")
749
+ print(f"Total issues fixed: {total_issues}")
750
+ print(f"- Class replacements: {totals['replacements']}")
751
+ print(f"- Extra-breadcrumb fixes: {totals['extra_breadcrumbs']}")
752
+ print(f"- <li> in <ol.breadcrumb>: {totals['breadcrumb_items']}")
753
+ print(f"- <li> in <ul.nav-tabs>: {totals['nav_items']}")
754
+ print(f"- <a> in <ul.dropdown-menu>: {totals['dropdown_items']}")
755
+ print(f"- Panel class replacements: {totals['panel_classes']}")
756
+ print(f"- Resizing breakpoint xs: {totals['resizing_xs']}")
757
+ print("-------------------------------------")
758
+ print(f"- Resizing other breakpoints: {resizing_other}")
759
+
760
+
761
+ def main():
762
+ parser = argparse.ArgumentParser(description="Bootstrap 3 to 5 HTML fixer.")
763
+ parser.add_argument(
764
+ "-r",
765
+ "--resize",
766
+ action="store_true",
767
+ help="Change column breakpoints to be one level higher, such as 'col-xs-*' to 'col-sm-*'",
768
+ )
769
+ parser.add_argument("path", type=str, help="Path to directory in which to recursively fix all .html files.")
770
+ args = parser.parse_args()
771
+
772
+ fix_html_files_in_directory(args.path, resize=args.resize)
773
+
774
+
775
+ if __name__ == "__main__":
776
+ main()