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.
- nautobot/apps/choices.py +2 -2
- nautobot/apps/filters.py +9 -9
- nautobot/apps/forms.py +2 -0
- nautobot/apps/models.py +7 -2
- nautobot/apps/ui.py +20 -1
- nautobot/apps/utils.py +2 -3
- nautobot/apps/views.py +7 -1
- nautobot/circuits/filters.py +8 -23
- nautobot/circuits/navigation.py +3 -1
- nautobot/circuits/templates/circuits/circuit_create.html +9 -9
- nautobot/circuits/templates/circuits/circuit_terminations_swap.html +2 -2
- nautobot/circuits/templates/circuits/circuittermination_create.html +24 -33
- nautobot/circuits/templates/circuits/inc/circuit_termination.html +10 -10
- nautobot/circuits/templates/circuits/inc/circuit_termination_cable_fragment.html +13 -13
- nautobot/circuits/templates/circuits/inc/circuit_termination_header_extra_content.html +6 -6
- nautobot/circuits/templates/circuits/inc/circuit_termination_speed_fragment.html +3 -3
- nautobot/circuits/templates/circuits/inc/speed_widget.html +13 -13
- nautobot/circuits/templates/circuits/provider_create.html +9 -9
- nautobot/circuits/tests/integration/test_circuit.py +19 -19
- nautobot/circuits/tests/integration/test_circuits_bulk_operations.py +3 -0
- nautobot/circuits/tests/integration/test_relationships.py +4 -12
- nautobot/circuits/views.py +0 -2
- nautobot/cloud/filters.py +1 -13
- nautobot/cloud/navigation.py +3 -1
- nautobot/cloud/templates/cloud/cloudnetwork_update.html +9 -9
- nautobot/cloud/templates/cloud/cloudservice_update.html +6 -6
- nautobot/core/api/fields.py +30 -2
- nautobot/core/api/schema.py +1 -1
- nautobot/core/api/serializers.py +9 -2
- nautobot/core/api/urls.py +2 -0
- nautobot/core/api/views.py +58 -37
- nautobot/core/apps/__init__.py +6 -12
- nautobot/core/branching.py +83 -0
- nautobot/core/celery/__init__.py +11 -6
- nautobot/core/celery/backends.py +2 -0
- nautobot/core/celery/encoders.py +7 -0
- nautobot/core/celery/task.py +44 -0
- nautobot/core/checks.py +60 -0
- nautobot/core/cli/bootstrap_v3_to_v5.py +776 -0
- nautobot/core/constants.py +9 -0
- nautobot/core/context_processors.py +84 -0
- nautobot/core/filters.py +131 -2
- nautobot/core/forms/__init__.py +4 -2
- nautobot/core/forms/fields.py +10 -8
- nautobot/core/forms/forms.py +21 -9
- nautobot/core/forms/search.py +0 -15
- nautobot/core/forms/widgets.py +3 -2
- nautobot/core/graphql/__init__.py +8 -26
- nautobot/core/graphql/generators.py +16 -6
- nautobot/core/graphql/schema.py +1 -1
- nautobot/core/graphql/schema_init.py +1 -2
- nautobot/core/graphql/utils.py +7 -9
- nautobot/core/jobs/__init__.py +158 -0
- nautobot/core/management/commands/generate_test_data.py +28 -9
- nautobot/core/models/__init__.py +17 -2
- nautobot/core/models/fields.py +3 -2
- nautobot/core/models/generics.py +9 -1
- nautobot/core/models/name_color_content_types.py +1 -1
- nautobot/core/models/ordering.py +7 -5
- nautobot/core/models/querysets.py +77 -2
- nautobot/core/models/tree_queries.py +6 -4
- nautobot/core/settings.py +30 -16
- nautobot/core/settings.yaml +13 -7
- nautobot/core/tables.py +114 -44
- nautobot/core/templates/403.html +1 -1
- nautobot/core/templates/403_csrf_failure.html +1 -1
- nautobot/core/templates/404.html +1 -1
- nautobot/core/templates/40x.html +8 -8
- nautobot/core/templates/500.html +10 -10
- nautobot/core/templates/about.html +13 -12
- nautobot/core/templates/admin/actions.html +1 -1
- nautobot/core/templates/admin/app_index.html +3 -3
- nautobot/core/templates/admin/base.html +45 -52
- nautobot/core/templates/admin/base_site.html +0 -9
- nautobot/core/templates/admin/change_form.html +5 -5
- nautobot/core/templates/admin/change_list.html +8 -12
- nautobot/core/templates/admin/change_list_results.html +3 -3
- nautobot/core/templates/admin/config/config.html +24 -24
- nautobot/core/templates/admin/delete_confirmation.html +5 -5
- nautobot/core/templates/admin/edit_inline/stacked.html +5 -5
- nautobot/core/templates/admin/edit_inline/tabular.html +3 -3
- nautobot/core/templates/admin/includes/fieldset.html +15 -15
- nautobot/core/templates/admin/index.html +8 -8
- nautobot/core/templates/admin/submit_line.html +5 -5
- nautobot/core/templates/base_django.html +36 -32
- nautobot/core/templates/buttons/add.html +1 -1
- nautobot/core/templates/buttons/consolidated_detail_view_action_buttons.html +2 -2
- nautobot/core/templates/buttons/export.html +17 -18
- nautobot/core/templates/buttons/job_import.html +2 -2
- nautobot/core/templates/components/breadcrumbs.html +19 -17
- nautobot/core/templates/components/button/dropdown.html +7 -5
- nautobot/core/templates/components/echarts.html +2 -0
- nautobot/core/templates/components/layout/one_over_two.html +3 -3
- nautobot/core/templates/components/layout/two_over_one.html +3 -3
- nautobot/core/templates/components/panel/body_content_data_table.html +2 -2
- nautobot/core/templates/components/panel/body_content_tags.html +1 -1
- nautobot/core/templates/components/panel/body_wrapper_generic.html +4 -2
- nautobot/core/templates/components/panel/body_wrapper_generic_table.html +1 -1
- nautobot/core/templates/components/panel/body_wrapper_key_value_table.html +5 -3
- nautobot/core/templates/components/panel/body_wrapper_table.html +4 -2
- nautobot/core/templates/components/panel/footer_contacts_table.html +4 -4
- nautobot/core/templates/components/panel/footer_content_table.html +3 -3
- nautobot/core/templates/components/panel/grouping_toggle.html +12 -11
- nautobot/core/templates/components/panel/header_extra_content_table.html +2 -11
- nautobot/core/templates/components/panel/panel.html +6 -3
- nautobot/core/templates/components/panel/stats_panel_body.html +9 -7
- nautobot/core/templates/components/tab/content_wrapper.html +29 -1
- nautobot/core/templates/components/tab/label_wrapper.html +10 -2
- nautobot/core/templates/components/tab/label_wrapper_distinct_view.html +11 -4
- nautobot/core/templates/echarts/echarts.html +20 -0
- nautobot/core/templates/exceptions/import_error.html +2 -2
- nautobot/core/templates/exceptions/permission_error.html +1 -1
- nautobot/core/templates/exceptions/programming_error.html +2 -2
- nautobot/core/templates/generic/object_bulk_add_component.html +29 -20
- nautobot/core/templates/generic/object_bulk_create.html +87 -75
- nautobot/core/templates/generic/object_bulk_destroy.html +35 -37
- nautobot/core/templates/generic/object_bulk_remove.html +30 -26
- nautobot/core/templates/generic/object_bulk_rename.html +53 -40
- nautobot/core/templates/generic/object_bulk_update.html +36 -29
- nautobot/core/templates/generic/object_create.html +40 -27
- nautobot/core/templates/generic/object_import.html +36 -24
- nautobot/core/templates/generic/object_list.html +279 -215
- nautobot/core/templates/generic/object_notes.html +21 -11
- nautobot/core/templates/generic/object_retrieve.html +161 -213
- nautobot/core/templates/graphene/graphiql.html +113 -60
- nautobot/core/templates/home.html +164 -87
- nautobot/core/templates/import_success.html +3 -2
- nautobot/core/templates/inc/ajax_loader.html +1 -1
- nautobot/core/templates/inc/computed_fields/panel_data.html +25 -13
- nautobot/core/templates/inc/created_updated.html +12 -7
- nautobot/core/templates/inc/custom_fields/panel_data.html +28 -16
- nautobot/core/templates/inc/custom_fields_panel.html +3 -3
- nautobot/core/templates/inc/dynamic_groups_panel.html +3 -3
- nautobot/core/templates/inc/extras_features_edit_form_fields.html +15 -15
- nautobot/core/templates/inc/footer.html +90 -40
- nautobot/core/templates/inc/form_static_field.html +6 -0
- nautobot/core/templates/inc/header.html +75 -0
- nautobot/core/templates/inc/header_banners.html +17 -0
- nautobot/core/templates/inc/header_messages.html +6 -0
- nautobot/core/templates/inc/image_attachments.html +9 -9
- nautobot/core/templates/inc/javascript.html +7 -24
- nautobot/core/templates/inc/media.html +4 -29
- nautobot/core/templates/inc/modal.html +2 -2
- nautobot/core/templates/inc/nav_favorites.html +27 -0
- nautobot/core/templates/inc/nav_menu.html +150 -108
- nautobot/core/templates/inc/object_details_advanced_panel.html +84 -71
- nautobot/core/templates/inc/page_title.html +23 -0
- nautobot/core/templates/inc/paginator.html +39 -28
- nautobot/core/templates/inc/relationships/panel_override.html +3 -3
- nautobot/core/templates/inc/relationships_panel.html +3 -3
- nautobot/core/templates/inc/relationships_table_rows.html +1 -1
- nautobot/core/templates/inc/search_panel.html +22 -16
- nautobot/core/templates/inc/table.html +61 -36
- nautobot/core/templates/inc/tenancy_form_panel.html +3 -3
- nautobot/core/templates/login.html +17 -59
- nautobot/core/templates/modals/modal_theme.html +12 -23
- nautobot/core/templates/nautobot_config.py.j2 +6 -5
- nautobot/core/templates/panel_table.html +8 -12
- nautobot/core/templates/redoc_ui.html +80 -0
- nautobot/core/templates/rest_framework/api.html +43 -21
- nautobot/core/templates/search.html +12 -13
- nautobot/core/templates/swagger_ui.html +19 -4
- nautobot/core/templates/system_jobs/import_objects.html +70 -58
- nautobot/core/templates/template.css +0 -6
- nautobot/core/templates/utilities/comment_form.html +34 -0
- nautobot/core/templates/utilities/confirmation_form.html +17 -9
- nautobot/core/templates/utilities/obj_table.html +19 -11
- nautobot/core/templates/utilities/render_field.html +27 -21
- nautobot/core/templates/utilities/render_jinja2.html +22 -25
- nautobot/core/templates/utilities/templatetags/advanced_filter_indicator.html +8 -0
- nautobot/core/templates/utilities/templatetags/badge.html +1 -1
- nautobot/core/templates/utilities/templatetags/dynamic_group_assignment_modal.html +2 -3
- nautobot/core/templates/utilities/templatetags/filter_form_drawer.html +482 -0
- nautobot/core/templates/utilities/templatetags/modal_form_as_dialog.html +14 -18
- nautobot/core/templates/utilities/templatetags/saved_view_modal.html +11 -11
- nautobot/core/templates/utilities/templatetags/table_config_form.html +51 -24
- nautobot/core/templates/utilities/templatetags/tag.html +1 -1
- nautobot/core/templates/utilities/templatetags/utilization_graph.html +3 -3
- nautobot/core/templates/utilities/theme_preview.html +829 -566
- nautobot/core/templates/utilities/worker_status.html +42 -41
- nautobot/core/templates/widgets/selectwithdisabled_option.html +3 -1
- nautobot/core/templates/widgets/sluginput.html +2 -2
- nautobot/core/templatetags/buttons.py +38 -40
- nautobot/core/templatetags/helpers.py +105 -28
- nautobot/core/templatetags/ui_framework.py +17 -0
- nautobot/core/testing/api.py +76 -12
- nautobot/core/testing/filters.py +11 -27
- nautobot/core/testing/integration.py +128 -10
- nautobot/core/testing/mixins.py +7 -4
- nautobot/core/testing/utils.py +28 -5
- nautobot/core/testing/views.py +125 -27
- nautobot/core/tests/integration/test_app_home.py +39 -35
- nautobot/core/tests/integration/test_app_navbar.py +60 -67
- nautobot/core/tests/integration/test_filters.py +123 -55
- nautobot/core/tests/integration/test_general_functionality.py +1 -1
- nautobot/core/tests/integration/test_home.py +10 -18
- nautobot/core/tests/integration/test_import_objects_ui.py +2 -9
- nautobot/core/tests/integration/test_navbar.py +41 -16
- nautobot/core/tests/integration/test_swagger.py +1 -7
- nautobot/core/tests/integration/test_theme.py +3 -0
- nautobot/core/tests/nautobot_config_without_example_apps.py +4 -0
- nautobot/core/tests/runner.py +6 -1
- nautobot/core/tests/test_api.py +5 -3
- nautobot/core/tests/test_branching.py +154 -0
- nautobot/core/tests/test_breadcrumbs.py +7 -8
- nautobot/core/tests/test_checks.py +28 -0
- nautobot/core/tests/test_commands.py +0 -41
- nautobot/core/tests/test_config.py +2 -1
- nautobot/core/tests/test_csv.py +4 -7
- nautobot/core/tests/test_filters.py +326 -318
- nautobot/core/tests/test_forms.py +19 -30
- nautobot/core/tests/test_graphql.py +67 -57
- nautobot/core/tests/test_models.py +1 -1
- nautobot/core/tests/test_nautobot_server.py +2 -0
- nautobot/core/tests/test_navigations.py +78 -10
- nautobot/core/tests/test_tables.py +3 -1
- nautobot/core/tests/test_templatetags_helpers.py +61 -21
- nautobot/core/tests/test_templatetags_ui_framework.py +36 -18
- nautobot/core/tests/test_ui.py +207 -2
- nautobot/core/tests/test_utils.py +147 -2
- nautobot/core/tests/test_views.py +201 -64
- nautobot/core/tests/test_views_utils.py +1 -1
- nautobot/core/ui/breadcrumbs.py +2 -12
- nautobot/core/ui/choices.py +190 -0
- nautobot/core/ui/constants.py +86 -0
- nautobot/core/ui/echarts.py +474 -0
- nautobot/core/ui/nav.py +5 -1
- nautobot/core/ui/object_detail.py +180 -16
- nautobot/core/urls.py +13 -1
- nautobot/core/utils/cache.py +71 -0
- nautobot/core/utils/data.py +8 -5
- nautobot/core/utils/filtering.py +8 -2
- nautobot/core/utils/git.py +3 -3
- nautobot/core/utils/lookup.py +87 -13
- nautobot/core/utils/migrations.py +22 -0
- nautobot/core/utils/module_loading.py +26 -0
- nautobot/core/utils/permissions.py +9 -5
- nautobot/core/views/__init__.py +114 -63
- nautobot/core/views/generic.py +34 -27
- nautobot/core/views/mixins.py +49 -27
- nautobot/core/views/renderers.py +3 -5
- nautobot/core/views/utils.py +10 -5
- nautobot/core/views/viewsets.py +2 -1
- nautobot/data_validation/__init__.py +0 -0
- nautobot/data_validation/api/__init__.py +1 -0
- nautobot/data_validation/api/serializers.py +80 -0
- nautobot/data_validation/api/urls.py +20 -0
- nautobot/data_validation/api/views.py +44 -0
- nautobot/data_validation/apps.py +18 -0
- nautobot/data_validation/custom_validators.py +330 -0
- nautobot/data_validation/filters.py +133 -0
- nautobot/data_validation/form_mixin.py +25 -0
- nautobot/data_validation/forms.py +342 -0
- nautobot/data_validation/migrations/0001_initial.py +224 -0
- nautobot/data_validation/migrations/0002_data_migration_from_app.py +324 -0
- nautobot/data_validation/migrations/__init__.py +0 -0
- nautobot/data_validation/models.py +361 -0
- nautobot/data_validation/navigation.py +74 -0
- nautobot/data_validation/signals.py +30 -0
- nautobot/data_validation/tables.py +259 -0
- nautobot/data_validation/templates/data_validation/datacompliance_retrieve.html +1 -0
- nautobot/data_validation/templates/data_validation/datacompliance_tab.html +11 -0
- nautobot/data_validation/templates/data_validation/device_constraints.html +61 -0
- nautobot/data_validation/tests/__init__.py +20 -0
- nautobot/data_validation/tests/migrations/__init__.py +0 -0
- nautobot/data_validation/tests/migrations/test_migrations.py +489 -0
- nautobot/data_validation/tests/test_api.py +238 -0
- nautobot/data_validation/tests/test_custom_validators.py +423 -0
- nautobot/data_validation/tests/test_data_compliance_rules.py +85 -0
- nautobot/data_validation/tests/test_filters.py +240 -0
- nautobot/data_validation/tests/test_form_mixin.py +115 -0
- nautobot/data_validation/tests/test_models.py +393 -0
- nautobot/data_validation/tests/test_views.py +435 -0
- nautobot/data_validation/urls.py +21 -0
- nautobot/data_validation/views.py +227 -0
- nautobot/dcim/api/serializers.py +10 -13
- nautobot/dcim/api/urls.py +2 -0
- nautobot/dcim/api/views.py +7 -0
- nautobot/dcim/apps.py +4 -0
- nautobot/dcim/choices.py +16 -0
- nautobot/dcim/custom_validators.py +84 -0
- nautobot/dcim/filter_mixins.py +353 -4
- nautobot/dcim/{filters/__init__.py → filters.py} +70 -157
- nautobot/dcim/forms.py +12 -6
- nautobot/dcim/graphql/types.py +1 -0
- nautobot/dcim/migrations/0075_add_deviceclusterassignment.py +52 -0
- nautobot/dcim/migrations/0076_device_cluster_to_clusters_data_migration.py +40 -0
- nautobot/dcim/migrations/0077_remove_device_cluster.py +14 -0
- nautobot/dcim/migrations/0078_remove_device_location_tenant_name_uniqueness.py +16 -0
- nautobot/dcim/migrations/0079_device_name_data_migration.py +59 -0
- nautobot/dcim/models/__init__.py +2 -0
- nautobot/dcim/models/device_components.py +3 -1
- nautobot/dcim/models/devices.py +115 -51
- nautobot/dcim/navigation.py +7 -3
- nautobot/dcim/querysets.py +6 -0
- nautobot/dcim/signals.py +19 -0
- nautobot/dcim/tables/devices.py +9 -5
- nautobot/dcim/tables/template_code.py +191 -102
- nautobot/dcim/templates/dcim/cable.html +1 -1
- nautobot/dcim/templates/dcim/cable_connect.html +62 -146
- nautobot/dcim/templates/dcim/cable_retrieve.html +10 -10
- nautobot/dcim/templates/dcim/cable_trace.html +15 -17
- nautobot/dcim/templates/dcim/console_port_connection_list.html +2 -2
- nautobot/dcim/templates/dcim/consoleport.html +18 -17
- nautobot/dcim/templates/dcim/consoleserverport.html +18 -17
- nautobot/dcim/templates/dcim/controller_create.html +12 -8
- nautobot/dcim/templates/dcim/controller_wirelessnetworks.html +1 -1
- nautobot/dcim/templates/dcim/controllermanageddevicegroup_create.html +6 -6
- nautobot/dcim/templates/dcim/controllermanageddevicegroup_retrieve.html +1 -1
- nautobot/dcim/templates/dcim/device/config.html +17 -19
- nautobot/dcim/templates/dcim/device/lldp_neighbors.html +4 -4
- nautobot/dcim/templates/dcim/device/status.html +20 -20
- nautobot/dcim/templates/dcim/device_component_add.html +24 -15
- nautobot/dcim/templates/dcim/device_create.html +120 -120
- nautobot/dcim/templates/dcim/device_list.html +75 -12
- nautobot/dcim/templates/dcim/devicebay.html +7 -7
- nautobot/dcim/templates/dcim/devicebay_populate.html +29 -23
- nautobot/dcim/templates/dcim/deviceredundancygroup_create.html +6 -6
- nautobot/dcim/templates/dcim/devicetype.html +1 -1
- nautobot/dcim/templates/dcim/devicetype_component_add.html +25 -19
- nautobot/dcim/templates/dcim/devicetype_list.html +4 -4
- nautobot/dcim/templates/dcim/devicetype_update.html +9 -9
- nautobot/dcim/templates/dcim/footer_convert_to_contact_or_team_record.html +3 -3
- nautobot/dcim/templates/dcim/frontport.html +21 -20
- nautobot/dcim/templates/dcim/inc/cable_form.html +7 -7
- nautobot/dcim/templates/dcim/inc/cable_termination.html +1 -1
- nautobot/dcim/templates/dcim/inc/cable_toggle_buttons.html +18 -9
- nautobot/dcim/templates/dcim/inc/detail_softwareversion_softwareimagefile_rows.html +1 -1
- nautobot/dcim/templates/dcim/inc/device_interface_filter.html +1 -1
- nautobot/dcim/templates/dcim/inc/devicetype_component_table.html +10 -10
- nautobot/dcim/templates/dcim/inc/edit_form_softwareversion_js.html +2 -2
- nautobot/dcim/templates/dcim/inc/homepage_connections.html +2 -2
- nautobot/dcim/templates/dcim/inc/moduletype_component_table.html +10 -10
- nautobot/dcim/templates/dcim/inc/rack_elevation.html +2 -2
- nautobot/dcim/templates/dcim/interface.html +42 -22
- nautobot/dcim/templates/dcim/interface_connection_list.html +2 -2
- nautobot/dcim/templates/dcim/interface_edit.html +26 -11
- nautobot/dcim/templates/dcim/interfaceredundancygroupassociation_create.html +3 -3
- nautobot/dcim/templates/dcim/inventoryitem.html +3 -3
- nautobot/dcim/templates/dcim/inventoryitem_add.html +21 -10
- nautobot/dcim/templates/dcim/inventoryitem_bulk_delete.html +1 -1
- nautobot/dcim/templates/dcim/inventoryitem_edit.html +6 -4
- nautobot/dcim/templates/dcim/location.html +1 -1
- nautobot/dcim/templates/dcim/location_migrate_data_to_contact.html +24 -18
- nautobot/dcim/templates/dcim/location_retrieve.html +1 -1
- nautobot/dcim/templates/dcim/location_update.html +9 -9
- nautobot/dcim/templates/dcim/locationtype.html +0 -1
- nautobot/dcim/templates/dcim/module/base.html +67 -27
- nautobot/dcim/templates/dcim/module_consoleports.html +13 -15
- nautobot/dcim/templates/dcim/module_consoleserverports.html +13 -15
- nautobot/dcim/templates/dcim/module_frontports.html +13 -15
- nautobot/dcim/templates/dcim/module_interfaces.html +14 -16
- nautobot/dcim/templates/dcim/module_list.html +59 -10
- nautobot/dcim/templates/dcim/module_modulebays.html +12 -14
- nautobot/dcim/templates/dcim/module_poweroutlets.html +13 -15
- nautobot/dcim/templates/dcim/module_powerports.html +13 -15
- nautobot/dcim/templates/dcim/module_rearports.html +13 -15
- nautobot/dcim/templates/dcim/module_retrieve.html +3 -3
- nautobot/dcim/templates/dcim/module_update.html +15 -9
- nautobot/dcim/templates/dcim/modulebay_retrieve.html +0 -93
- nautobot/dcim/templates/dcim/modulefamily_retrieve.html +7 -7
- nautobot/dcim/templates/dcim/moduletype_list.html +2 -2
- nautobot/dcim/templates/dcim/moduletype_retrieve.html +74 -35
- nautobot/dcim/templates/dcim/platform_create.html +9 -9
- nautobot/dcim/templates/dcim/power_port_connection_list.html +3 -3
- nautobot/dcim/templates/dcim/powerfeed.html +1 -1
- nautobot/dcim/templates/dcim/powerfeed_edit.html +15 -15
- nautobot/dcim/templates/dcim/poweroutlet.html +13 -13
- nautobot/dcim/templates/dcim/powerpanel.html +1 -1
- nautobot/dcim/templates/dcim/powerport.html +17 -16
- nautobot/dcim/templates/dcim/rack.html +1 -1
- nautobot/dcim/templates/dcim/rack_elevation.html +2 -2
- nautobot/dcim/templates/dcim/rack_elevation_list.html +21 -9
- nautobot/dcim/templates/dcim/rack_retrieve.html +75 -57
- nautobot/dcim/templates/dcim/rack_update.html +14 -14
- nautobot/dcim/templates/dcim/rackreservation.html +1 -1
- nautobot/dcim/templates/dcim/rackreservation_edit.html +6 -6
- nautobot/dcim/templates/dcim/rearport.html +19 -18
- nautobot/dcim/templates/dcim/trace/cable.html +1 -1
- nautobot/dcim/templates/dcim/trace/circuit.html +1 -1
- nautobot/dcim/templates/dcim/trace/device.html +1 -1
- nautobot/dcim/templates/dcim/trace/powerpanel.html +1 -1
- nautobot/dcim/templates/dcim/trace/termination.html +1 -1
- nautobot/dcim/templates/dcim/virtualchassis.html +1 -1
- nautobot/dcim/templates/dcim/virtualchassis_add_member.html +25 -16
- nautobot/dcim/templates/dcim/virtualchassis_create.html +6 -6
- nautobot/dcim/templates/dcim/virtualchassis_edit.html +1 -1
- nautobot/dcim/templates/dcim/virtualchassis_retrieve.html +1 -1
- nautobot/dcim/templates/dcim/virtualchassis_update.html +36 -22
- nautobot/dcim/templates/dcim/virtualdevicecontext_update.html +9 -9
- nautobot/dcim/tests/integration/test_controller.py +6 -6
- nautobot/dcim/tests/integration/test_controller_managed_device_group.py +7 -7
- nautobot/dcim/tests/integration/test_create_device.py +9 -9
- nautobot/dcim/tests/integration/test_device_bulk_operations.py +7 -2
- nautobot/dcim/tests/integration/test_fileinputpicker.py +5 -7
- nautobot/dcim/tests/integration/test_location_bulk_operations.py +2 -0
- nautobot/dcim/tests/integration/test_module_bay_position.py +4 -1
- nautobot/dcim/tests/test_api.py +86 -6
- nautobot/dcim/tests/test_custom_validators.py +229 -0
- nautobot/dcim/tests/test_filters.py +159 -110
- nautobot/dcim/tests/test_graphql.py +32 -36
- nautobot/dcim/tests/test_jobs.py +1 -1
- nautobot/dcim/tests/test_models.py +229 -1
- nautobot/dcim/tests/test_views.py +31 -20
- nautobot/dcim/utils.py +3 -3
- nautobot/dcim/views.py +77 -41
- nautobot/extras/api/serializers.py +83 -19
- nautobot/extras/api/urls.py +7 -0
- nautobot/extras/api/views.py +243 -140
- nautobot/extras/choices.py +34 -13
- nautobot/extras/constants.py +1 -1
- nautobot/extras/context_managers.py +26 -26
- nautobot/extras/datasources/git.py +22 -0
- nautobot/extras/datasources/registry.py +3 -0
- nautobot/extras/exceptions.py +5 -0
- nautobot/extras/factory.py +11 -1
- nautobot/extras/{filters/mixins.py → filter_mixins.py} +4 -3
- nautobot/extras/{filters/__init__.py → filters.py} +203 -58
- nautobot/extras/forms/base.py +2 -1
- nautobot/extras/forms/forms.py +225 -20
- nautobot/extras/forms/mixins.py +0 -41
- nautobot/extras/homepage.py +21 -2
- nautobot/extras/jobs.py +2 -8
- nautobot/extras/jobs_ui.py +2 -2
- nautobot/extras/management/__init__.py +9 -0
- nautobot/extras/managers.py +31 -22
- nautobot/extras/migrations/0126_approval_workflow_pre_check.py +58 -0
- nautobot/extras/migrations/0127_approval_workflow_models.py +266 -0
- nautobot/extras/migrations/0128_remove_job_approval_required_and_more.py +29 -0
- nautobot/extras/migrations/0129_jobresult_debug_log_count_jobresult_error_log_count_and_more.py +37 -0
- nautobot/extras/migrations/0130_jobresult_generate_log_entry_counts.py +42 -0
- nautobot/extras/models/__init__.py +14 -3
- nautobot/extras/models/approvals.py +556 -0
- nautobot/extras/models/change_logging.py +1 -0
- nautobot/extras/models/contacts.py +2 -0
- nautobot/extras/models/customfields.py +57 -22
- nautobot/extras/models/datasources.py +21 -0
- nautobot/extras/models/groups.py +2 -0
- nautobot/extras/models/jobs.py +122 -39
- nautobot/extras/models/metadata.py +2 -3
- nautobot/extras/models/mixins.py +129 -1
- nautobot/extras/models/models.py +22 -14
- nautobot/extras/models/relationships.py +47 -10
- nautobot/extras/models/secrets.py +1 -0
- nautobot/extras/models/statuses.py +0 -15
- nautobot/extras/models/tags.py +1 -1
- nautobot/extras/navigation.py +42 -15
- nautobot/extras/plugins/__init__.py +33 -55
- nautobot/extras/plugins/marketplace_manifest.yml +1 -23
- nautobot/extras/plugins/tables.py +8 -6
- nautobot/extras/plugins/urls.py +2 -21
- nautobot/extras/plugins/utils.py +1 -33
- nautobot/extras/plugins/validators.py +10 -10
- nautobot/extras/plugins/views.py +1 -5
- nautobot/extras/querysets.py +17 -21
- nautobot/extras/signals.py +23 -8
- nautobot/extras/tables.py +420 -99
- nautobot/extras/templates/extras/approval_dashboard.html +15 -0
- nautobot/extras/templates/extras/approval_workflow/approve.html +11 -0
- nautobot/extras/templates/extras/approval_workflow/comment.html +9 -0
- nautobot/extras/templates/extras/approval_workflow/deny.html +10 -0
- nautobot/extras/templates/extras/approvalworkflowdefinition_update.html +77 -0
- nautobot/extras/templates/extras/approvalworkflowstage_retrieve.html +29 -0
- nautobot/extras/templates/extras/configcontext_update.html +12 -12
- nautobot/extras/templates/extras/configcontextschema.html +1 -1
- nautobot/extras/templates/extras/configcontextschema_retrieve.html +9 -9
- nautobot/extras/templates/extras/configcontextschema_update.html +6 -6
- nautobot/extras/templates/extras/configcontextschema_validation.html +2 -2
- nautobot/extras/templates/extras/customfield_update.html +12 -12
- nautobot/extras/templates/extras/dynamicgroup.html +1 -1
- nautobot/extras/templates/extras/dynamicgroup_edit.html +1 -1
- nautobot/extras/templates/extras/dynamicgroup_retrieve.html +17 -17
- nautobot/extras/templates/extras/dynamicgroup_update.html +24 -24
- nautobot/extras/templates/extras/externalintegration_update.html +6 -6
- nautobot/extras/templates/extras/gitrepository.html +1 -1
- nautobot/extras/templates/extras/gitrepository_object_edit.html +1 -1
- nautobot/extras/templates/extras/gitrepository_result.html +1 -1
- nautobot/extras/templates/extras/gitrepository_retrieve.html +12 -12
- nautobot/extras/templates/extras/gitrepository_update.html +25 -7
- nautobot/extras/templates/extras/graphqlquery_retrieve.html +1 -1
- nautobot/extras/templates/extras/inc/approval_buttons_column.html +38 -0
- nautobot/extras/templates/extras/inc/bulk_edit_overridable_field.html +14 -13
- nautobot/extras/templates/extras/inc/configcontext_format.html +11 -4
- nautobot/extras/templates/extras/inc/graphqlquery_execute.html +7 -7
- nautobot/extras/templates/extras/inc/job_label.html +5 -5
- nautobot/extras/templates/extras/inc/job_table.html +23 -10
- nautobot/extras/templates/extras/inc/job_tiles.html +33 -21
- nautobot/extras/templates/extras/inc/jobresult.html +6 -6
- nautobot/extras/templates/extras/inc/json_format.html +11 -4
- nautobot/extras/templates/extras/inc/object_contact_header.html +6 -6
- nautobot/extras/templates/extras/inc/overridable_field.html +16 -15
- nautobot/extras/templates/extras/inc/panel_approvalworkflowstage.html +34 -0
- nautobot/extras/templates/extras/inc/panel_changelog.html +9 -9
- nautobot/extras/templates/extras/inc/panel_jobhistory.html +8 -6
- nautobot/extras/templates/extras/inc/tags_panel.html +3 -3
- nautobot/extras/templates/extras/job.html +154 -155
- nautobot/extras/templates/extras/job_approval_confirmation.html +4 -27
- nautobot/extras/templates/extras/job_bulk_edit.html +18 -1
- nautobot/extras/templates/extras/job_detail.html +1 -1
- nautobot/extras/templates/extras/job_edit.html +69 -64
- nautobot/extras/templates/extras/job_list.html +37 -60
- nautobot/extras/templates/extras/jobresult.html +1 -1
- nautobot/extras/templates/extras/jobresult_retrieve.html +17 -17
- nautobot/extras/templates/extras/marketplace.html +62 -71
- nautobot/extras/templates/extras/metadatatype_create.html +9 -9
- nautobot/extras/templates/extras/note.html +1 -1
- nautobot/extras/templates/extras/object_approvalworkflow.html +36 -0
- nautobot/extras/templates/extras/object_assign_contact_or_team.html +16 -7
- nautobot/extras/templates/extras/object_configcontext.html +20 -20
- nautobot/extras/templates/extras/object_new_contact.html +6 -6
- nautobot/extras/templates/extras/object_new_team.html +6 -6
- nautobot/extras/templates/extras/objectchange.html +1 -1
- nautobot/extras/templates/extras/objectchange_retrieve.html +37 -56
- nautobot/extras/templates/extras/plugin_detail.html +40 -41
- nautobot/extras/templates/extras/plugins_list.html +23 -38
- nautobot/extras/templates/extras/plugins_tiles.html +28 -28
- nautobot/extras/templates/extras/role_retrieve.html +112 -48
- nautobot/extras/templates/extras/scheduledjob.html +25 -28
- nautobot/extras/templates/extras/secret_create.html +11 -11
- nautobot/extras/templates/extras/secretsgroup_update.html +6 -6
- nautobot/extras/templates/extras/staticgroupassociation_retrieve.html +3 -3
- nautobot/extras/templates/extras/status.html +1 -1
- nautobot/extras/templates/extras/tag.html +1 -1
- nautobot/extras/templates/extras/tag_update.html +3 -3
- nautobot/extras/templates/extras/templatetags/log_level.html +1 -1
- nautobot/extras/templates/extras/templatetags/plugin_object_detail_tabs.html +2 -2
- nautobot/extras/templates/extras/webhook.html +12 -12
- nautobot/extras/templatetags/approvals.py +19 -0
- nautobot/extras/templatetags/custom_links.py +12 -12
- nautobot/extras/templatetags/job_buttons.py +14 -12
- nautobot/extras/test_jobs/invalid_import.py +9 -0
- nautobot/extras/test_jobs/log_counts_by_level.py +23 -0
- nautobot/extras/test_jobs/missing_import.py +11 -0
- nautobot/extras/tests/integration/test_computedfields.py +5 -8
- nautobot/extras/tests/integration/test_configcontextschema.py +43 -48
- nautobot/extras/tests/integration/test_customfields.py +33 -33
- nautobot/extras/tests/integration/test_dynamicgroups.py +5 -10
- nautobot/extras/tests/integration/test_jobs.py +2 -4
- nautobot/extras/tests/integration/test_notes.py +3 -9
- nautobot/extras/tests/integration/test_plugin_banner.py +3 -0
- nautobot/extras/tests/integration/test_plugins.py +35 -27
- nautobot/extras/tests/integration/test_relationships.py +7 -11
- nautobot/extras/tests/integration/test_tagfilter.py +3 -11
- nautobot/extras/tests/test_api.py +786 -242
- nautobot/extras/tests/test_approvals.py +715 -0
- nautobot/extras/tests/test_changelog.py +18 -14
- nautobot/extras/tests/test_customfields.py +14 -13
- nautobot/extras/tests/test_datasources.py +1 -1
- nautobot/extras/tests/test_dynamicgroups.py +9 -4
- nautobot/extras/tests/test_filters.py +443 -13
- nautobot/extras/tests/test_forms.py +18 -57
- nautobot/extras/tests/test_jobs.py +25 -4
- nautobot/extras/tests/test_migrations.py +81 -1
- nautobot/extras/tests/test_models.py +378 -47
- nautobot/extras/tests/test_plugins.py +47 -13
- nautobot/extras/tests/test_relationships.py +7 -2
- nautobot/extras/tests/test_utils.py +2 -0
- nautobot/extras/tests/test_views.py +780 -493
- nautobot/extras/urls.py +36 -12
- nautobot/extras/utils.py +58 -12
- nautobot/extras/views.py +668 -209
- nautobot/ipam/factory.py +7 -0
- nautobot/ipam/filter_mixins.py +38 -0
- nautobot/ipam/filters.py +35 -71
- nautobot/ipam/formfields.py +1 -1
- nautobot/ipam/forms.py +6 -3
- nautobot/ipam/migrations/0030_ipam__namespaces.py +13 -0
- nautobot/ipam/migrations/0031_ipam___data_migrations.py +4 -1
- nautobot/ipam/migrations/0054_namespace_tenant.py +25 -0
- nautobot/ipam/models.py +29 -2
- nautobot/ipam/navigation.py +3 -1
- nautobot/ipam/querysets.py +1 -2
- nautobot/ipam/tables.py +26 -17
- nautobot/ipam/templates/ipam/inc/ipadress_edit_header.html +6 -6
- nautobot/ipam/templates/ipam/inc/service.html +8 -8
- nautobot/ipam/templates/ipam/inc/toggle_available.html +10 -10
- nautobot/ipam/templates/ipam/inc/vlangroup_header.html +3 -2
- nautobot/ipam/templates/ipam/ipaddress.html +27 -13
- nautobot/ipam/templates/ipam/ipaddress_assign.html +31 -24
- nautobot/ipam/templates/ipam/ipaddress_bulk_add.html +3 -3
- nautobot/ipam/templates/ipam/ipaddress_edit.html +9 -9
- nautobot/ipam/templates/ipam/ipaddress_interfaces.html +7 -9
- nautobot/ipam/templates/ipam/ipaddress_merge.html +195 -186
- nautobot/ipam/templates/ipam/ipaddress_vm_interfaces.html +7 -9
- nautobot/ipam/templates/ipam/ipaddresstointerface_retrieve.html +7 -5
- nautobot/ipam/templates/ipam/namespace_ip_addresses.html +1 -1
- nautobot/ipam/templates/ipam/namespace_prefixes.html +1 -1
- nautobot/ipam/templates/ipam/namespace_update.html +15 -0
- nautobot/ipam/templates/ipam/namespace_vrfs.html +1 -1
- nautobot/ipam/templates/ipam/prefix_create.html +9 -9
- nautobot/ipam/templates/ipam/prefix_list.html +15 -14
- nautobot/ipam/templates/ipam/prefix_retrieve.html +0 -1
- nautobot/ipam/templates/ipam/vlan.html +1 -1
- nautobot/ipam/templates/ipam/vlan_interfaces.html +1 -1
- nautobot/ipam/templates/ipam/vlan_update.html +6 -6
- nautobot/ipam/templates/ipam/vlan_vminterfaces.html +1 -1
- nautobot/ipam/templates/ipam/vrf_edit.html +15 -15
- nautobot/ipam/tests/integration/test_prefixes.py +5 -13
- nautobot/ipam/tests/migration/test_migrations.py +89 -0
- nautobot/ipam/tests/test_api.py +20 -7
- nautobot/ipam/tests/test_filters.py +10 -0
- nautobot/ipam/tests/test_forms.py +1 -1
- nautobot/ipam/tests/test_models.py +1 -1
- nautobot/ipam/tests/test_tables.py +1 -2
- nautobot/ipam/tests/test_utils.py +1 -1
- nautobot/ipam/tests/test_views.py +24 -21
- nautobot/ipam/ui.py +0 -17
- nautobot/ipam/utils/migrations.py +16 -2
- nautobot/ipam/utils/testing.py +9 -3
- nautobot/ipam/views.py +49 -7
- nautobot/project-static/dist/css/graphql-libraries.css +655 -0
- nautobot/project-static/dist/css/graphql-libraries.css.map +1 -0
- nautobot/project-static/dist/css/materialdesignicons.css +3 -0
- nautobot/project-static/dist/css/materialdesignicons.css.map +1 -0
- nautobot/project-static/dist/css/nautobot.css +13 -0
- nautobot/project-static/dist/css/nautobot.css.map +1 -0
- nautobot/project-static/dist/js/graphql-libraries.js +3 -0
- nautobot/project-static/dist/js/graphql-libraries.js.LICENSE.txt +62 -0
- nautobot/project-static/dist/js/graphql-libraries.js.map +1 -0
- nautobot/project-static/dist/js/libraries.js +3 -0
- nautobot/project-static/dist/js/libraries.js.LICENSE.txt +65 -0
- nautobot/project-static/dist/js/libraries.js.map +1 -0
- nautobot/project-static/dist/js/materialdesignicons.js +0 -0
- nautobot/project-static/dist/js/nautobot-graphiql.js +2 -0
- nautobot/project-static/dist/js/nautobot-graphiql.js.map +1 -0
- nautobot/project-static/dist/js/nautobot.js +2 -0
- nautobot/project-static/dist/js/nautobot.js.map +1 -0
- nautobot/project-static/fonts/Montserrat-v30-Bold.woff2 +0 -0
- nautobot/project-static/fonts/Montserrat-v30-Light.woff2 +0 -0
- nautobot/project-static/fonts/Montserrat-v30-Regular.woff2 +0 -0
- nautobot/project-static/fonts/Roboto-v48-Bold.woff2 +0 -0
- nautobot/project-static/fonts/Roboto-v48-Light.woff2 +0 -0
- nautobot/project-static/fonts/Roboto-v48-Regular.woff2 +0 -0
- nautobot/project-static/img/jinja_logo.svg +21 -92
- nautobot/project-static/js/cabletrace.js +1 -1
- nautobot/project-static/js/editor.js +4 -4
- nautobot/project-static/js/forms.js +67 -717
- nautobot/project-static/js/job_result.js +2 -2
- nautobot/project-static/nautobot-icons/360-degrees.svg +3 -0
- nautobot/project-static/nautobot-icons/arrow-decision.svg +3 -0
- nautobot/project-static/nautobot-icons/arrows-expand-rec.svg +3 -0
- nautobot/project-static/nautobot-icons/arrows-move-2-rec.svg +3 -0
- nautobot/project-static/nautobot-icons/arrows-move-rec.svg +3 -0
- nautobot/project-static/nautobot-icons/atom.svg +3 -0
- nautobot/project-static/nautobot-icons/battery-3.svg +3 -0
- nautobot/project-static/nautobot-icons/branch.svg +3 -0
- nautobot/project-static/nautobot-icons/briefcase-2.svg +3 -0
- nautobot/project-static/nautobot-icons/cable-data-2.svg +3 -0
- nautobot/project-static/nautobot-icons/cable-data.svg +3 -0
- nautobot/project-static/nautobot-icons/cast.svg +3 -0
- nautobot/project-static/nautobot-icons/check-circle.svg +3 -0
- nautobot/project-static/nautobot-icons/checkbox-circle.svg +3 -0
- nautobot/project-static/nautobot-icons/checkbox-rec.svg +3 -0
- nautobot/project-static/nautobot-icons/cloud-check.svg +3 -0
- nautobot/project-static/nautobot-icons/cloud-lightning.svg +3 -0
- nautobot/project-static/nautobot-icons/cloud-upload.svg +3 -0
- nautobot/project-static/nautobot-icons/cloud.svg +3 -0
- nautobot/project-static/nautobot-icons/compass.svg +3 -0
- nautobot/project-static/nautobot-icons/control-panel.svg +3 -0
- nautobot/project-static/nautobot-icons/credit-card.svg +3 -0
- nautobot/project-static/nautobot-icons/device-lifecycle.svg +3 -0
- nautobot/project-static/nautobot-icons/direction.svg +3 -0
- nautobot/project-static/nautobot-icons/elements.svg +3 -0
- nautobot/project-static/nautobot-icons/extensibility.svg +3 -0
- nautobot/project-static/nautobot-icons/globe-2.svg +3 -0
- nautobot/project-static/nautobot-icons/globe.svg +3 -0
- nautobot/project-static/nautobot-icons/hammer.svg +3 -0
- nautobot/project-static/nautobot-icons/history.svg +3 -0
- nautobot/project-static/nautobot-icons/ip.svg +3 -0
- nautobot/project-static/nautobot-icons/laptop.svg +3 -0
- nautobot/project-static/nautobot-icons/lightning.svg +3 -0
- nautobot/project-static/nautobot-icons/list-unordered.svg +3 -0
- nautobot/project-static/nautobot-icons/map-view.svg +3 -0
- nautobot/project-static/nautobot-icons/organization.svg +3 -0
- nautobot/project-static/nautobot-icons/pin-2.svg +3 -0
- nautobot/project-static/nautobot-icons/pin-3.svg +3 -0
- nautobot/project-static/nautobot-icons/plug.svg +3 -0
- nautobot/project-static/nautobot-icons/refresh-cw.svg +3 -0
- nautobot/project-static/nautobot-icons/rocket-2.svg +3 -0
- nautobot/project-static/nautobot-icons/rotate-cw.svg +3 -0
- nautobot/project-static/nautobot-icons/route.svg +3 -0
- nautobot/project-static/nautobot-icons/secrets.svg +3 -0
- nautobot/project-static/nautobot-icons/security.svg +3 -0
- nautobot/project-static/nautobot-icons/server-2.svg +3 -0
- nautobot/project-static/nautobot-icons/server.svg +3 -0
- nautobot/project-static/nautobot-icons/share.svg +3 -0
- nautobot/project-static/nautobot-icons/shield-check.svg +3 -0
- nautobot/project-static/nautobot-icons/sitemap-outline.svg +3 -0
- nautobot/project-static/nautobot-icons/sliders-vert-2.svg +3 -0
- nautobot/project-static/nautobot-icons/sliders-vert.svg +3 -0
- nautobot/project-static/nautobot-icons/star-filled.svg +3 -0
- nautobot/project-static/nautobot-icons/star.svg +3 -0
- nautobot/project-static/nautobot-icons/transform.svg +3 -0
- nautobot/project-static/nautobot-icons/wifi.svg +3 -0
- nautobot/tenancy/api/serializers.py +1 -0
- nautobot/tenancy/api/views.py +2 -1
- nautobot/tenancy/{filters/__init__.py → filters.py} +2 -10
- nautobot/tenancy/navigation.py +3 -1
- nautobot/tenancy/templates/tenancy/tenant_create.html +6 -6
- nautobot/tenancy/tests/test_filters.py +0 -2
- nautobot/tenancy/views.py +2 -1
- nautobot/ui/.gitignore +137 -0
- nautobot/ui/.node-version +1 -0
- nautobot/ui/.prettierignore +3 -0
- nautobot/ui/eslint.config.js +33 -0
- nautobot/ui/package-lock.json +6594 -0
- nautobot/ui/package.json +67 -0
- nautobot/ui/prettier.config.js +9 -0
- nautobot/ui/src/js/collapse.js +69 -0
- nautobot/ui/src/js/cookie.js +31 -0
- nautobot/ui/src/js/draggable.js +101 -0
- nautobot/ui/src/js/drawer.js +106 -0
- nautobot/ui/src/js/form.js +23 -0
- nautobot/ui/src/js/history.js +51 -0
- nautobot/ui/src/js/nautobot-graphiql.js +19 -0
- nautobot/ui/src/js/nautobot.js +128 -0
- nautobot/ui/src/js/search.js +274 -0
- nautobot/ui/src/js/select2.js +318 -0
- nautobot/ui/src/js/sidenav.js +87 -0
- nautobot/ui/src/js/tabs.js +139 -0
- nautobot/ui/src/js/theme.js +104 -0
- nautobot/ui/src/js/utils.js +54 -0
- nautobot/ui/src/scss/colors.scss +58 -0
- nautobot/ui/src/scss/nautobot.scss +2471 -0
- nautobot/ui/webpack.config.js +148 -0
- nautobot/users/apps.py +3 -0
- nautobot/users/filters.py +7 -11
- nautobot/users/forms.py +10 -0
- nautobot/users/models.py +8 -0
- nautobot/users/templates/users/advanced_settings_edit.html +31 -21
- nautobot/users/templates/users/api_tokens.html +61 -51
- nautobot/users/templates/users/base.html +23 -31
- nautobot/users/templates/users/change_password.html +29 -19
- nautobot/users/templates/users/preferences.html +55 -45
- nautobot/users/templates/users/profile.html +45 -14
- nautobot/users/tests/test_api.py +4 -0
- nautobot/users/urls.py +2 -0
- nautobot/users/views.py +70 -2
- nautobot/virtualization/api/views.py +1 -1
- nautobot/virtualization/filters.py +18 -32
- nautobot/virtualization/forms.py +22 -59
- nautobot/virtualization/models.py +1 -19
- nautobot/virtualization/navigation.py +3 -1
- nautobot/virtualization/tables.py +10 -6
- nautobot/virtualization/templates/virtualization/cluster.html +13 -13
- nautobot/virtualization/templates/virtualization/cluster_edit.html +6 -6
- nautobot/virtualization/templates/virtualization/inc/virtualmachine_vminterface_filter.html +1 -1
- nautobot/virtualization/templates/virtualization/virtualmachine.html +1 -1
- nautobot/virtualization/templates/virtualization/virtualmachine_component_add.html +24 -16
- nautobot/virtualization/templates/virtualization/virtualmachine_edit.html +1 -1
- nautobot/virtualization/templates/virtualization/virtualmachine_list.html +4 -4
- nautobot/virtualization/templates/virtualization/virtualmachine_update.html +27 -25
- nautobot/virtualization/templates/virtualization/vminterface.html +5 -5
- nautobot/virtualization/templates/virtualization/vminterface_edit.html +27 -11
- nautobot/virtualization/tests/test_api.py +3 -0
- nautobot/virtualization/tests/test_models.py +20 -5
- nautobot/virtualization/tests/test_views.py +3 -5
- nautobot/virtualization/urls.py +0 -11
- nautobot/virtualization/views.py +5 -122
- nautobot/vpn/__init__.py +0 -0
- nautobot/vpn/api/serializers.py +113 -0
- nautobot/vpn/api/urls.py +19 -0
- nautobot/vpn/api/views.py +70 -0
- nautobot/vpn/apps.py +8 -0
- nautobot/vpn/choices.py +171 -0
- nautobot/vpn/factory.py +209 -0
- nautobot/vpn/filters.py +233 -0
- nautobot/vpn/forms.py +486 -0
- nautobot/vpn/homepage.py +19 -0
- nautobot/vpn/migrations/0001_initial.py +541 -0
- nautobot/vpn/migrations/0002_populate_defaults.py +199 -0
- nautobot/vpn/migrations/__init__.py +0 -0
- nautobot/vpn/models.py +527 -0
- nautobot/vpn/navigation.py +98 -0
- nautobot/vpn/tables.py +380 -0
- nautobot/vpn/templates/vpn/vpnprofile.html +2 -0
- nautobot/vpn/templates/vpn/vpnprofile_create.html +150 -0
- nautobot/vpn/tests/__init__.py +0 -0
- nautobot/vpn/tests/test_api.py +341 -0
- nautobot/vpn/tests/test_filters.py +139 -0
- nautobot/vpn/tests/test_forms.py +294 -0
- nautobot/vpn/tests/test_models.py +97 -0
- nautobot/vpn/tests/test_views.py +281 -0
- nautobot/vpn/urls.py +16 -0
- nautobot/vpn/views.py +437 -0
- nautobot/wireless/filters.py +0 -8
- nautobot/wireless/navigation.py +3 -1
- nautobot/wireless/templates/wireless/wirelessnetwork_create.html +6 -6
- nautobot/wireless/tests/integration/test_radio_profile.py +3 -7
- nautobot/wireless/tests/test_api.py +1 -1
- {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/METADATA +5 -4
- {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/RECORD +802 -707
- {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/entry_points.txt +1 -0
- nautobot/core/management/commands/check_job_approval_status.py +0 -47
- nautobot/core/templates/search_form.html +0 -9
- nautobot/core/templates/utilities/templatetags/filter_form_modal.html +0 -87
- nautobot/dcim/filters/mixins.py +0 -354
- nautobot/extras/templates/extras/job_approval_request.html +0 -134
- nautobot/extras/templates/extras/scheduled_jobs_approval_queue_list.html +0 -28
- nautobot/ipam/mixins.py +0 -32
- nautobot/ipam/templates/ipam/inc/prefix_header_extra_content_table.html +0 -4
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap-theme.css +0 -587
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap-theme.css.map +0 -1
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap-theme.min.css +0 -6
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap-theme.min.css.map +0 -1
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.css +0 -6865
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.css.map +0 -1
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.min.css +0 -6
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.min.css.map +0 -1
- nautobot/project-static/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.eot +0 -0
- nautobot/project-static/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.svg +0 -288
- nautobot/project-static/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.ttf +0 -0
- nautobot/project-static/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.woff +0 -0
- nautobot/project-static/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.woff2 +0 -0
- nautobot/project-static/bootstrap-3.4.1-dist/js/bootstrap.js +0 -2580
- nautobot/project-static/bootstrap-3.4.1-dist/js/bootstrap.min.js +0 -6
- nautobot/project-static/bootstrap-3.4.1-dist/js/npm.js +0 -13
- nautobot/project-static/clipboard.js-2.0.9/clipboard.min.js +0 -7
- nautobot/project-static/css/base.css +0 -1040
- nautobot/project-static/css/dark.css +0 -282
- nautobot/project-static/flatpickr-4.6.9/flatpickr.min.js +0 -2
- nautobot/project-static/flatpickr-4.6.9/themes/light.min.css +0 -1
- nautobot/project-static/graphiql-1.5.16/graphiql.min.css +0 -12
- nautobot/project-static/graphiql-1.5.16/graphiql.min.js +0 -11
- nautobot/project-static/highlight.js-11.9.0/github-dark.min.css +0 -10
- nautobot/project-static/highlight.js-11.9.0/github.min.css +0 -10
- nautobot/project-static/highlight.js-11.9.0/highlight.min.js +0 -378
- nautobot/project-static/jquery/jquery-3.7.1.min.js +0 -2
- nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_444444_256x240.png +0 -0
- nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_555555_256x240.png +0 -0
- nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_777620_256x240.png +0 -0
- nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_777777_256x240.png +0 -0
- nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_cc0000_256x240.png +0 -0
- nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_ffffff_256x240.png +0 -0
- nautobot/project-static/jquery-ui-1.13.2/jquery-ui.min.css +0 -7
- nautobot/project-static/jquery-ui-1.13.2/jquery-ui.min.js +0 -6
- nautobot/project-static/jquery-ui-1.13.2/jquery-ui.structure.min.css +0 -5
- nautobot/project-static/jquery-ui-1.13.2/jquery-ui.theme.min.css +0 -5
- nautobot/project-static/js/homepage_layout.js +0 -182
- nautobot/project-static/js/nav_menu.js +0 -250
- nautobot/project-static/js/theme.js +0 -133
- nautobot/project-static/materialdesignicons-7.4.47/LICENSE +0 -20
- nautobot/project-static/materialdesignicons-7.4.47/css/materialdesignicons.min.css +0 -3
- nautobot/project-static/react-16.14.0/react.production.min.js +0 -32
- nautobot/project-static/react-dom-16.14.0/react-dom.production.min.js +0 -239
- nautobot/project-static/select2-4.0.13/i18n/af.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/ar.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/az.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/bg.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/bn.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/bs.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/ca.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/cs.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/da.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/de.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/dsb.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/el.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/en.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/es.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/et.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/eu.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/fa.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/fi.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/fr.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/gl.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/he.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/hi.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/hr.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/hsb.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/hu.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/hy.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/id.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/is.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/it.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/ja.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/ka.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/km.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/ko.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/lt.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/lv.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/mk.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/ms.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/nb.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/ne.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/nl.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/pl.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/ps.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/pt-BR.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/pt.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/ro.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/ru.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/sk.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/sl.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/sq.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/sr-Cyrl.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/sr.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/sv.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/th.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/tk.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/tr.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/uk.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/vi.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/zh-CN.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/zh-TW.js +0 -3
- nautobot/project-static/select2-4.0.13/select2.min.css +0 -1
- nautobot/project-static/select2-4.0.13/select2.min.js +0 -2
- nautobot/project-static/select2-bootstrap-0.1.0-beta.10/select2-bootstrap.min.css +0 -7
- nautobot/project-static/subscriptions-transport-ws-0.9.18/client.min.js +0 -8
- nautobot/project-static/whatwg-fetch-3.6.2/fetch.umd.min.js +0 -8
- nautobot/virtualization/templates/virtualization/cluster_add_devices.html +0 -37
- /nautobot/extras/{filters/customfields.py → filter_mixins_customfields.py} +0 -0
- /nautobot/project-static/{materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.ttf → dist/1fcc36272ea3e53d0031.ttf} +0 -0
- /nautobot/project-static/{materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.eot → dist/2146c3c82b553977abc7.eot} +0 -0
- /nautobot/project-static/{materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff → dist/e55a20c80650829ec5fd.woff} +0 -0
- /nautobot/project-static/{materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff2 → dist/ec024da790d2972da002.woff2} +0 -0
- /nautobot/tenancy/{filters/mixins.py → filter_mixins.py} +0 -0
- {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/NOTICE +0 -0
- {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/WHEEL +0 -0
nautobot/extras/views.py
CHANGED
|
@@ -15,7 +15,6 @@ from django.template.defaultfilters import urlencode
|
|
|
15
15
|
from django.template.loader import get_template, TemplateDoesNotExist
|
|
16
16
|
from django.urls import reverse
|
|
17
17
|
from django.urls.exceptions import NoReverseMatch
|
|
18
|
-
from django.utils import timezone
|
|
19
18
|
from django.utils.encoding import iri_to_uri
|
|
20
19
|
from django.utils.html import format_html, format_html_join
|
|
21
20
|
from django.utils.http import url_has_allowed_host_and_scheme
|
|
@@ -29,11 +28,10 @@ from rest_framework.response import Response
|
|
|
29
28
|
|
|
30
29
|
from nautobot.core.choices import ButtonActionColorChoices
|
|
31
30
|
from nautobot.core.constants import PAGINATE_COUNT_DEFAULT
|
|
32
|
-
from nautobot.core.events import publish_event
|
|
33
31
|
from nautobot.core.exceptions import FilterSetFieldNotFound
|
|
34
|
-
from nautobot.core.forms import restrict_form_fields
|
|
32
|
+
from nautobot.core.forms import ApprovalForm, restrict_form_fields
|
|
35
33
|
from nautobot.core.models.querysets import count_related
|
|
36
|
-
from nautobot.core.models.utils import pretty_print_query
|
|
34
|
+
from nautobot.core.models.utils import pretty_print_query
|
|
37
35
|
from nautobot.core.tables import ButtonsColumn
|
|
38
36
|
from nautobot.core.templatetags import helpers
|
|
39
37
|
from nautobot.core.ui import object_detail
|
|
@@ -70,7 +68,7 @@ from nautobot.core.views.mixins import (
|
|
|
70
68
|
ObjectPermissionRequiredMixin,
|
|
71
69
|
)
|
|
72
70
|
from nautobot.core.views.paginator import EnhancedPaginator, get_paginate_count
|
|
73
|
-
from nautobot.core.views.utils import get_obj_from_context, prepare_cloned_fields
|
|
71
|
+
from nautobot.core.views.utils import common_detail_view_context, get_obj_from_context, prepare_cloned_fields
|
|
74
72
|
from nautobot.core.views.viewsets import NautobotUIViewSet
|
|
75
73
|
from nautobot.dcim.models import Controller, Device, Interface, Module, Rack, VirtualDeviceContext
|
|
76
74
|
from nautobot.dcim.tables import (
|
|
@@ -82,15 +80,24 @@ from nautobot.dcim.tables import (
|
|
|
82
80
|
VirtualDeviceContextTable,
|
|
83
81
|
)
|
|
84
82
|
from nautobot.extras.context_managers import deferred_change_logging_for_bulk_operation
|
|
85
|
-
from nautobot.extras.
|
|
83
|
+
from nautobot.extras.templatetags.approvals import render_approval_workflow_state
|
|
84
|
+
from nautobot.extras.utils import (
|
|
85
|
+
fixup_filterset_query_params,
|
|
86
|
+
get_base_template,
|
|
87
|
+
get_pending_approval_workflow_stages,
|
|
88
|
+
get_worker_count,
|
|
89
|
+
)
|
|
86
90
|
from nautobot.ipam.models import IPAddress, Prefix, VLAN
|
|
87
91
|
from nautobot.ipam.tables import IPAddressTable, PrefixTable, VLANTable
|
|
88
92
|
from nautobot.virtualization.models import VirtualMachine, VMInterface
|
|
89
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
|
|
90
96
|
|
|
91
97
|
from . import filters, forms, jobs_ui, tables
|
|
92
98
|
from .api import serializers
|
|
93
99
|
from .choices import (
|
|
100
|
+
ApprovalWorkflowStateChoices,
|
|
94
101
|
DynamicGroupTypeChoices,
|
|
95
102
|
JobExecutionType,
|
|
96
103
|
JobQueueTypeChoices,
|
|
@@ -103,6 +110,11 @@ from .datasources import (
|
|
|
103
110
|
)
|
|
104
111
|
from .jobs import get_job
|
|
105
112
|
from .models import (
|
|
113
|
+
ApprovalWorkflow,
|
|
114
|
+
ApprovalWorkflowDefinition,
|
|
115
|
+
ApprovalWorkflowStage,
|
|
116
|
+
ApprovalWorkflowStageDefinition,
|
|
117
|
+
ApprovalWorkflowStageResponse,
|
|
106
118
|
ComputedField,
|
|
107
119
|
ConfigContext,
|
|
108
120
|
ConfigContextSchema,
|
|
@@ -144,6 +156,509 @@ from .registry import registry
|
|
|
144
156
|
|
|
145
157
|
logger = logging.getLogger(__name__)
|
|
146
158
|
|
|
159
|
+
#
|
|
160
|
+
# Approval Workflows
|
|
161
|
+
#
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class ApprovalWorkflowDefinitionUIViewSet(NautobotUIViewSet):
|
|
165
|
+
"""ViewSet for ApprovalWorkflowDefinition."""
|
|
166
|
+
|
|
167
|
+
bulk_update_form_class = forms.ApprovalWorkflowDefinitionBulkEditForm
|
|
168
|
+
filterset_class = filters.ApprovalWorkflowDefinitionFilterSet
|
|
169
|
+
filterset_form_class = forms.ApprovalWorkflowDefinitionFilterForm
|
|
170
|
+
form_class = forms.ApprovalWorkflowDefinitionForm
|
|
171
|
+
queryset = ApprovalWorkflowDefinition.objects.all()
|
|
172
|
+
serializer_class = serializers.ApprovalWorkflowDefinitionSerializer
|
|
173
|
+
table_class = tables.ApprovalWorkflowDefinitionTable
|
|
174
|
+
|
|
175
|
+
object_detail_content = object_detail.ObjectDetailContent(
|
|
176
|
+
panels=[
|
|
177
|
+
object_detail.ObjectFieldsPanel(
|
|
178
|
+
weight=100,
|
|
179
|
+
section=SectionChoices.LEFT_HALF,
|
|
180
|
+
fields="__all__",
|
|
181
|
+
),
|
|
182
|
+
object_detail.ObjectsTablePanel(
|
|
183
|
+
weight=100,
|
|
184
|
+
table_class=tables.ApprovalWorkflowStageDefinitionTable,
|
|
185
|
+
table_filter="approval_workflow_definition",
|
|
186
|
+
section=SectionChoices.RIGHT_HALF,
|
|
187
|
+
exclude_columns=["approval_workflow_definition", "actions"],
|
|
188
|
+
add_button_route=None,
|
|
189
|
+
table_title="Stages",
|
|
190
|
+
),
|
|
191
|
+
object_detail.ObjectsTablePanel(
|
|
192
|
+
weight=200,
|
|
193
|
+
table_class=tables.ApprovalWorkflowTable,
|
|
194
|
+
table_filter="approval_workflow_definition",
|
|
195
|
+
section=SectionChoices.FULL_WIDTH,
|
|
196
|
+
exclude_columns=["object_under_review_content_type", "approval_workflow_definition"],
|
|
197
|
+
add_button_route=None,
|
|
198
|
+
table_title="Workflows",
|
|
199
|
+
),
|
|
200
|
+
],
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def get_extra_context(self, request, instance):
|
|
204
|
+
ctx = super().get_extra_context(request, instance)
|
|
205
|
+
if self.action in ("create", "update"):
|
|
206
|
+
if request.POST:
|
|
207
|
+
ctx["stages"] = forms.ApprovalWorkflowStageDefinitionFormSet(data=request.POST, instance=instance)
|
|
208
|
+
else:
|
|
209
|
+
ctx["stages"] = forms.ApprovalWorkflowStageDefinitionFormSet(instance=instance)
|
|
210
|
+
|
|
211
|
+
return ctx
|
|
212
|
+
|
|
213
|
+
def form_save(self, form, **kwargs):
|
|
214
|
+
obj = super().form_save(form, **kwargs)
|
|
215
|
+
|
|
216
|
+
# Process the formset for stages
|
|
217
|
+
ctx = self.get_extra_context(self.request, obj)
|
|
218
|
+
stages = ctx["stages"]
|
|
219
|
+
if stages.is_valid():
|
|
220
|
+
stages.save()
|
|
221
|
+
else:
|
|
222
|
+
raise ValidationError(stages.errors)
|
|
223
|
+
|
|
224
|
+
return obj
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class ApprovalWorkflowStageDefinitionUIViewSet(NautobotUIViewSet):
|
|
228
|
+
"""ViewSet for ApprovalWorkflowStageDefinition."""
|
|
229
|
+
|
|
230
|
+
bulk_update_form_class = forms.ApprovalWorkflowStageDefinitionBulkEditForm
|
|
231
|
+
filterset_class = filters.ApprovalWorkflowStageDefinitionFilterSet
|
|
232
|
+
filterset_form_class = forms.ApprovalWorkflowStageDefinitionFilterForm
|
|
233
|
+
form_class = forms.ApprovalWorkflowStageDefinitionForm
|
|
234
|
+
queryset = ApprovalWorkflowStageDefinition.objects.all()
|
|
235
|
+
serializer_class = serializers.ApprovalWorkflowStageDefinitionSerializer
|
|
236
|
+
table_class = tables.ApprovalWorkflowStageDefinitionTable
|
|
237
|
+
|
|
238
|
+
object_detail_content = object_detail.ObjectDetailContent(
|
|
239
|
+
panels=[
|
|
240
|
+
object_detail.ObjectFieldsPanel(
|
|
241
|
+
weight=100,
|
|
242
|
+
section=SectionChoices.LEFT_HALF,
|
|
243
|
+
fields="__all__",
|
|
244
|
+
),
|
|
245
|
+
],
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class ApprovalWorkflowUIViewSet(
|
|
250
|
+
ObjectDetailViewMixin,
|
|
251
|
+
ObjectListViewMixin,
|
|
252
|
+
ObjectDestroyViewMixin,
|
|
253
|
+
ObjectBulkDestroyViewMixin,
|
|
254
|
+
ObjectChangeLogViewMixin,
|
|
255
|
+
ObjectNotesViewMixin,
|
|
256
|
+
):
|
|
257
|
+
"""ViewSet for ApprovalWorkflow."""
|
|
258
|
+
|
|
259
|
+
filterset_class = filters.ApprovalWorkflowFilterSet
|
|
260
|
+
filterset_form_class = forms.ApprovalWorkflowFilterForm
|
|
261
|
+
queryset = ApprovalWorkflow.objects.all()
|
|
262
|
+
serializer_class = serializers.ApprovalWorkflowSerializer
|
|
263
|
+
table_class = tables.ApprovalWorkflowTable
|
|
264
|
+
action_buttons = ()
|
|
265
|
+
|
|
266
|
+
class ApprovalWorkflowPanel(object_detail.ObjectFieldsPanel):
|
|
267
|
+
def __init__(self, **kwargs):
|
|
268
|
+
super().__init__(
|
|
269
|
+
fields=(
|
|
270
|
+
"approval_workflow_definition",
|
|
271
|
+
"object_under_review",
|
|
272
|
+
"current_state",
|
|
273
|
+
"decision_date",
|
|
274
|
+
"user",
|
|
275
|
+
),
|
|
276
|
+
value_transforms={
|
|
277
|
+
"current_state": [render_approval_workflow_state],
|
|
278
|
+
},
|
|
279
|
+
hide_if_unset=("decision_date"),
|
|
280
|
+
**kwargs,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
def render_key(self, key, value, context):
|
|
284
|
+
obj = get_obj_from_context(context)
|
|
285
|
+
|
|
286
|
+
if key == "object_under_review":
|
|
287
|
+
return helpers.bettertitle(obj.object_under_review_content_type.model_class()._meta.verbose_name)
|
|
288
|
+
if key == "user":
|
|
289
|
+
return "Requesting User"
|
|
290
|
+
if key == "decision_date":
|
|
291
|
+
if obj.current_state == ApprovalWorkflowStateChoices.APPROVED:
|
|
292
|
+
return "Approval Date"
|
|
293
|
+
elif obj.current_state == ApprovalWorkflowStateChoices.DENIED:
|
|
294
|
+
return "Denial Date"
|
|
295
|
+
|
|
296
|
+
return super().render_key(key, value, context)
|
|
297
|
+
|
|
298
|
+
def render_value(self, key, value, context):
|
|
299
|
+
obj = get_obj_from_context(context)
|
|
300
|
+
if key == "user":
|
|
301
|
+
if not obj.user:
|
|
302
|
+
return obj.user_name
|
|
303
|
+
|
|
304
|
+
return super().render_value(key, value, context)
|
|
305
|
+
|
|
306
|
+
object_detail_content = object_detail.ObjectDetailContent(
|
|
307
|
+
panels=[
|
|
308
|
+
ApprovalWorkflowPanel(
|
|
309
|
+
weight=100,
|
|
310
|
+
section=SectionChoices.LEFT_HALF,
|
|
311
|
+
),
|
|
312
|
+
object_detail.ObjectsTablePanel(
|
|
313
|
+
weight=200,
|
|
314
|
+
table_title="Stages",
|
|
315
|
+
table_class=tables.RelatedApprovalWorkflowStageTable,
|
|
316
|
+
table_filter="approval_workflow",
|
|
317
|
+
section=SectionChoices.RIGHT_HALF,
|
|
318
|
+
exclude_columns=["approval_workflow"],
|
|
319
|
+
add_button_route=None,
|
|
320
|
+
),
|
|
321
|
+
object_detail.ObjectsTablePanel(
|
|
322
|
+
weight=200,
|
|
323
|
+
table_title="Responses",
|
|
324
|
+
table_class=tables.RelatedApprovalWorkflowStageResponseTable,
|
|
325
|
+
table_filter="approval_workflow_stage__approval_workflow",
|
|
326
|
+
section=SectionChoices.RIGHT_HALF,
|
|
327
|
+
exclude_columns=["approval_workflow"],
|
|
328
|
+
add_button_route=None,
|
|
329
|
+
),
|
|
330
|
+
],
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
class ApprovalWorkflowStageUIViewSet(
|
|
335
|
+
ObjectDetailViewMixin,
|
|
336
|
+
ObjectListViewMixin,
|
|
337
|
+
ObjectDestroyViewMixin,
|
|
338
|
+
ObjectBulkDestroyViewMixin,
|
|
339
|
+
ObjectChangeLogViewMixin,
|
|
340
|
+
ObjectNotesViewMixin,
|
|
341
|
+
):
|
|
342
|
+
"""ViewSet for ApprovalWorkflowStage."""
|
|
343
|
+
|
|
344
|
+
filterset_class = filters.ApprovalWorkflowStageFilterSet
|
|
345
|
+
filterset_form_class = forms.ApprovalWorkflowStageFilterForm
|
|
346
|
+
queryset = ApprovalWorkflowStage.objects.all()
|
|
347
|
+
serializer_class = serializers.ApprovalWorkflowStageSerializer
|
|
348
|
+
table_class = tables.ApprovalWorkflowStageTable
|
|
349
|
+
action_buttons = ()
|
|
350
|
+
|
|
351
|
+
class ApprovalWorkflowStagePanel(object_detail.ObjectFieldsPanel):
|
|
352
|
+
def __init__(self, **kwargs):
|
|
353
|
+
super().__init__(
|
|
354
|
+
fields=(
|
|
355
|
+
"approval_workflow",
|
|
356
|
+
"state",
|
|
357
|
+
"decision_date",
|
|
358
|
+
"approver_group",
|
|
359
|
+
"min_approvers",
|
|
360
|
+
),
|
|
361
|
+
value_transforms={
|
|
362
|
+
"state": [render_approval_workflow_state],
|
|
363
|
+
},
|
|
364
|
+
hide_if_unset=("decision_date"),
|
|
365
|
+
ignore_nonexistent_fields=True,
|
|
366
|
+
**kwargs,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
def render_key(self, key, value, context):
|
|
370
|
+
obj = get_obj_from_context(context)
|
|
371
|
+
|
|
372
|
+
if key == "approval_workflow":
|
|
373
|
+
return "Approval Workflow"
|
|
374
|
+
if key == "decision_date":
|
|
375
|
+
if obj.state == ApprovalWorkflowStateChoices.APPROVED:
|
|
376
|
+
return "Approval Date"
|
|
377
|
+
elif obj.state == ApprovalWorkflowStateChoices.DENIED:
|
|
378
|
+
return "Denial Date"
|
|
379
|
+
if key == "min_approvers":
|
|
380
|
+
return "Minimum Number of Approvers Needed"
|
|
381
|
+
|
|
382
|
+
return super().render_key(key, value, context)
|
|
383
|
+
|
|
384
|
+
def render_value(self, key, value, context):
|
|
385
|
+
if key == "approver_group":
|
|
386
|
+
user_html = format_html(
|
|
387
|
+
"<span>{}</span><ul>{}</ul>",
|
|
388
|
+
value,
|
|
389
|
+
format_html_join("\n", "<li>{}</li>", ((user,) for user in value.user_set.all())),
|
|
390
|
+
)
|
|
391
|
+
return user_html
|
|
392
|
+
|
|
393
|
+
return super().render_value(key, value, context)
|
|
394
|
+
|
|
395
|
+
def get_data(self, context):
|
|
396
|
+
obj = get_obj_from_context(context)
|
|
397
|
+
data = super().get_data(context)
|
|
398
|
+
data["approver_group"] = obj.approval_workflow_stage_definition.approver_group
|
|
399
|
+
data["min_approvers"] = obj.approval_workflow_stage_definition.min_approvers
|
|
400
|
+
return data
|
|
401
|
+
|
|
402
|
+
object_detail_content = object_detail.ObjectDetailContent(
|
|
403
|
+
panels=[
|
|
404
|
+
ApprovalWorkflowStagePanel(
|
|
405
|
+
weight=100,
|
|
406
|
+
section=SectionChoices.LEFT_HALF,
|
|
407
|
+
),
|
|
408
|
+
object_detail.ObjectsTablePanel(
|
|
409
|
+
weight=200,
|
|
410
|
+
table_class=tables.ApprovalWorkflowStageResponseTable,
|
|
411
|
+
table_filter="approval_workflow_stage",
|
|
412
|
+
section=SectionChoices.RIGHT_HALF,
|
|
413
|
+
exclude_columns=["approval_workflow_stage"],
|
|
414
|
+
table_title="Responses",
|
|
415
|
+
),
|
|
416
|
+
],
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
@action(detail=True, url_path="approve", methods=["get", "post"])
|
|
420
|
+
def approve(self, request, *args, **kwargs):
|
|
421
|
+
"""
|
|
422
|
+
Approve the approval workflow stage response.
|
|
423
|
+
"""
|
|
424
|
+
instance = self.get_object()
|
|
425
|
+
|
|
426
|
+
try:
|
|
427
|
+
approval_workflow_stage_response = ApprovalWorkflowStageResponse.objects.get(
|
|
428
|
+
approval_workflow_stage=instance,
|
|
429
|
+
user=request.user,
|
|
430
|
+
)
|
|
431
|
+
except ApprovalWorkflowStageResponse.DoesNotExist:
|
|
432
|
+
approval_workflow_stage_response = ApprovalWorkflowStageResponse.objects.create(
|
|
433
|
+
approval_workflow_stage=instance,
|
|
434
|
+
user=request.user,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
if request.method == "GET":
|
|
438
|
+
obj = approval_workflow_stage_response
|
|
439
|
+
form = ApprovalForm(initial={"comments": obj.comments})
|
|
440
|
+
|
|
441
|
+
object_under_review = instance.approval_workflow.object_under_review
|
|
442
|
+
template_name = getattr(object_under_review, "get_approval_template", lambda: None)()
|
|
443
|
+
if not template_name:
|
|
444
|
+
template_name = "extras/approval_workflow/approve.html"
|
|
445
|
+
|
|
446
|
+
return render(
|
|
447
|
+
request,
|
|
448
|
+
template_name,
|
|
449
|
+
{
|
|
450
|
+
"obj": obj.approval_workflow_stage,
|
|
451
|
+
"object_under_review": obj.approval_workflow_stage.approval_workflow.object_under_review,
|
|
452
|
+
"form": form,
|
|
453
|
+
"obj_type": ApprovalWorkflowStage._meta.verbose_name,
|
|
454
|
+
"return_url": self.get_return_url(request, obj),
|
|
455
|
+
"card_class": "success",
|
|
456
|
+
"button_class": "success",
|
|
457
|
+
},
|
|
458
|
+
)
|
|
459
|
+
approval_workflow_stage_response.comments = request.data.get("comments")
|
|
460
|
+
approval_workflow_stage_response.state = ApprovalWorkflowStateChoices.APPROVED
|
|
461
|
+
approval_workflow_stage_response.save()
|
|
462
|
+
instance.refresh_from_db()
|
|
463
|
+
messages.success(request, f"You approved {instance}.")
|
|
464
|
+
return redirect(self.get_return_url(request))
|
|
465
|
+
|
|
466
|
+
@action(detail=True, url_path="deny", methods=["get", "post"])
|
|
467
|
+
def deny(self, request, *args, **kwargs):
|
|
468
|
+
"""
|
|
469
|
+
Deny the approval workflow stage response.
|
|
470
|
+
"""
|
|
471
|
+
instance = self.get_object()
|
|
472
|
+
|
|
473
|
+
try:
|
|
474
|
+
approval_workflow_stage_response = ApprovalWorkflowStageResponse.objects.get(
|
|
475
|
+
approval_workflow_stage=instance,
|
|
476
|
+
user=request.user,
|
|
477
|
+
)
|
|
478
|
+
except ApprovalWorkflowStageResponse.DoesNotExist:
|
|
479
|
+
approval_workflow_stage_response = ApprovalWorkflowStageResponse.objects.create(
|
|
480
|
+
approval_workflow_stage=instance,
|
|
481
|
+
user=request.user,
|
|
482
|
+
state=ApprovalWorkflowStateChoices.PENDING,
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
if request.method == "GET":
|
|
486
|
+
obj = approval_workflow_stage_response
|
|
487
|
+
form = ApprovalForm(initial={"comments": obj.comments})
|
|
488
|
+
|
|
489
|
+
return render(
|
|
490
|
+
request,
|
|
491
|
+
"extras/approval_workflow/deny.html",
|
|
492
|
+
{
|
|
493
|
+
"obj": obj.approval_workflow_stage,
|
|
494
|
+
"object_under_review": obj.approval_workflow_stage.approval_workflow.object_under_review,
|
|
495
|
+
"form": form,
|
|
496
|
+
"obj_type": ApprovalWorkflowStage._meta.verbose_name,
|
|
497
|
+
"return_url": self.get_return_url(request, obj),
|
|
498
|
+
},
|
|
499
|
+
)
|
|
500
|
+
approval_workflow_stage_response.comments = request.data.get("comments")
|
|
501
|
+
approval_workflow_stage_response.state = ApprovalWorkflowStateChoices.DENIED
|
|
502
|
+
approval_workflow_stage_response.save()
|
|
503
|
+
instance.refresh_from_db()
|
|
504
|
+
messages.success(request, f"You denied {instance}.")
|
|
505
|
+
return redirect(self.get_return_url(request))
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
class ApprovalWorkflowStageResponseUIViewSet(
|
|
509
|
+
ObjectBulkDestroyViewMixin,
|
|
510
|
+
ObjectDestroyViewMixin,
|
|
511
|
+
):
|
|
512
|
+
"""ViewSet for ApprovalWorkflowStageResponse."""
|
|
513
|
+
|
|
514
|
+
filterset_class = filters.ApprovalWorkflowStageResponseFilterSet
|
|
515
|
+
filterset_form_class = forms.ApprovalWorkflowStageResponseFilterForm
|
|
516
|
+
queryset = ApprovalWorkflowStageResponse.objects.all()
|
|
517
|
+
serializer_class = serializers.ApprovalWorkflowStageResponseSerializer
|
|
518
|
+
table_class = tables.ApprovalWorkflowStageResponseTable
|
|
519
|
+
object_detail_content = None
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
class ApproverDashboardView(ObjectListViewMixin):
|
|
523
|
+
"""
|
|
524
|
+
View for the dashboard of approval workflow stages waiting for the current user to approve.
|
|
525
|
+
"""
|
|
526
|
+
|
|
527
|
+
queryset = ApprovalWorkflowStage.objects.all()
|
|
528
|
+
filterset_class = filters.ApprovalWorkflowStageFilterSet
|
|
529
|
+
filterset_form_class = forms.ApprovalWorkflowStageFilterForm
|
|
530
|
+
table_class = tables.ApproverDashboardTable
|
|
531
|
+
template_name = "extras/approval_dashboard.html"
|
|
532
|
+
action_buttons = ()
|
|
533
|
+
|
|
534
|
+
def get_template_name(self):
|
|
535
|
+
"""
|
|
536
|
+
Override the template names to use the custom dashboard template.
|
|
537
|
+
"""
|
|
538
|
+
return self.template_name
|
|
539
|
+
|
|
540
|
+
def get_extra_context(self, request, instance):
|
|
541
|
+
"""
|
|
542
|
+
Get the extra context for the dashboard view.
|
|
543
|
+
"""
|
|
544
|
+
context = super().get_extra_context(request, instance)
|
|
545
|
+
context["title"] = "My Approvals"
|
|
546
|
+
context["approval_view"] = True
|
|
547
|
+
return context
|
|
548
|
+
|
|
549
|
+
def get_queryset(self):
|
|
550
|
+
"""
|
|
551
|
+
Filter the queryset to only include approval workflow stages that are pending approval
|
|
552
|
+
and are assigned to the current user for approval.
|
|
553
|
+
"""
|
|
554
|
+
return get_pending_approval_workflow_stages(self.request.user, super().get_queryset())
|
|
555
|
+
|
|
556
|
+
def list(self, request, *args, **kwargs):
|
|
557
|
+
"""
|
|
558
|
+
Override the list method to display a helpful message regarding the page.
|
|
559
|
+
"""
|
|
560
|
+
messages.info(
|
|
561
|
+
request,
|
|
562
|
+
"You are viewing a dashboard of approval workflow stages that are pending for your approval.",
|
|
563
|
+
)
|
|
564
|
+
return super().list(request, *args, **kwargs)
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
class ApproveeDashboardView(ObjectListViewMixin):
|
|
568
|
+
"""
|
|
569
|
+
View for the dashboard of approval workflows trigger by the current user.
|
|
570
|
+
"""
|
|
571
|
+
|
|
572
|
+
queryset = ApprovalWorkflow.objects.all()
|
|
573
|
+
filterset_class = filters.ApprovalWorkflowFilterSet
|
|
574
|
+
filterset_form_class = forms.ApprovalWorkflowFilterForm
|
|
575
|
+
table_class = tables.ApprovalWorkflowTable
|
|
576
|
+
template_name = "extras/approval_dashboard.html"
|
|
577
|
+
action_buttons = ()
|
|
578
|
+
|
|
579
|
+
def get_template_name(self):
|
|
580
|
+
"""
|
|
581
|
+
Override the template names to use the custom dashboard template.
|
|
582
|
+
"""
|
|
583
|
+
return self.template_name
|
|
584
|
+
|
|
585
|
+
def get_extra_context(self, request, instance):
|
|
586
|
+
"""
|
|
587
|
+
Get the extra context for the dashboard view.
|
|
588
|
+
"""
|
|
589
|
+
context = super().get_extra_context(request, instance)
|
|
590
|
+
context["title"] = "My Requests"
|
|
591
|
+
return context
|
|
592
|
+
|
|
593
|
+
def get_queryset(self):
|
|
594
|
+
"""
|
|
595
|
+
Filter the queryset to only include workflows that triggered by the current users.
|
|
596
|
+
"""
|
|
597
|
+
user = self.request.user
|
|
598
|
+
if user.is_anonymous:
|
|
599
|
+
return ApprovalWorkflow.objects.none()
|
|
600
|
+
queryset = super().get_queryset()
|
|
601
|
+
return queryset.filter(user=user).order_by("created")
|
|
602
|
+
|
|
603
|
+
def list(self, request, *args, **kwargs):
|
|
604
|
+
"""
|
|
605
|
+
Override the list method to display a helpful message regarding the page.
|
|
606
|
+
"""
|
|
607
|
+
messages.info(
|
|
608
|
+
request,
|
|
609
|
+
"You are viewing a dashboard of approval workflows that are requested by you.",
|
|
610
|
+
)
|
|
611
|
+
return super().list(request, *args, **kwargs)
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
class ObjectApprovalWorkflowView(generic.GenericView):
|
|
615
|
+
"""
|
|
616
|
+
Present an pending approval workflow attached to a particular object.
|
|
617
|
+
|
|
618
|
+
base_template: Specify to explicitly identify the base object detail template to render.
|
|
619
|
+
If not provided, "<app>/<model>.html", "<app>/<model>_retrieve.html", or "generic/object_retrieve.html"
|
|
620
|
+
will be used, as per `get_base_template()`.
|
|
621
|
+
"""
|
|
622
|
+
|
|
623
|
+
base_template: Optional[str] = None
|
|
624
|
+
|
|
625
|
+
def get(self, request, model, **kwargs):
|
|
626
|
+
# Handle QuerySet restriction of parent object if needed
|
|
627
|
+
|
|
628
|
+
if hasattr(model.objects, "restrict"):
|
|
629
|
+
obj = get_object_or_404(model.objects.restrict(request.user, "view"), **kwargs)
|
|
630
|
+
else:
|
|
631
|
+
obj = get_object_or_404(model, **kwargs)
|
|
632
|
+
|
|
633
|
+
# Gather all changes for this object (and its related objects)
|
|
634
|
+
approval_workflow = ApprovalWorkflow.objects.get(object_under_review_object_id=obj.pk)
|
|
635
|
+
stage_table = tables.RelatedApprovalWorkflowStageTable(
|
|
636
|
+
ApprovalWorkflowStage.objects.filter(approval_workflow=approval_workflow),
|
|
637
|
+
)
|
|
638
|
+
stage_table.columns.hide("approval_workflow")
|
|
639
|
+
response_table = tables.RelatedApprovalWorkflowStageResponseTable(
|
|
640
|
+
ApprovalWorkflowStageResponse.objects.filter(approval_workflow_stage__approval_workflow=approval_workflow)
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
base_template = get_base_template(self.base_template, model)
|
|
644
|
+
|
|
645
|
+
return render(
|
|
646
|
+
request,
|
|
647
|
+
"extras/object_approvalworkflow.html",
|
|
648
|
+
{
|
|
649
|
+
"object": obj,
|
|
650
|
+
"verbose_name": helpers.bettertitle(obj._meta.verbose_name),
|
|
651
|
+
"verbose_name_plural": obj._meta.verbose_name_plural,
|
|
652
|
+
"approval_workflow": approval_workflow,
|
|
653
|
+
"base_template": base_template,
|
|
654
|
+
"active_tab": "approval_workflow",
|
|
655
|
+
"default_time_zone": get_current_timezone(),
|
|
656
|
+
"stage_table": stage_table,
|
|
657
|
+
"response_table": response_table,
|
|
658
|
+
**common_detail_view_context(request, obj),
|
|
659
|
+
},
|
|
660
|
+
)
|
|
661
|
+
|
|
147
662
|
|
|
148
663
|
#
|
|
149
664
|
# Computed Fields
|
|
@@ -470,6 +985,7 @@ class ContactAssociationUIViewSet(
|
|
|
470
985
|
serializer_class = serializers.ContactAssociationSerializer
|
|
471
986
|
table_class = tables.AssociatedContactsTable
|
|
472
987
|
non_filter_params = ("export", "page", "per_page", "sort")
|
|
988
|
+
object_detail_content = None
|
|
473
989
|
|
|
474
990
|
|
|
475
991
|
class ObjectContactTeamMixin:
|
|
@@ -1194,7 +1710,7 @@ class GitRepositoryUIViewSet(NautobotUIViewSet):
|
|
|
1194
1710
|
context = {
|
|
1195
1711
|
**super().get_extra_context(request, instance),
|
|
1196
1712
|
"result": job_result or {},
|
|
1197
|
-
"base_template": "extras/
|
|
1713
|
+
"base_template": "extras/gitrepository_retrieve.html",
|
|
1198
1714
|
"object": instance,
|
|
1199
1715
|
"active_tab": "result",
|
|
1200
1716
|
"verbose_name": instance._meta.verbose_name,
|
|
@@ -1384,6 +1900,42 @@ class JobRunView(ObjectPermissionRequiredMixin, View):
|
|
|
1384
1900
|
|
|
1385
1901
|
return job_model
|
|
1386
1902
|
|
|
1903
|
+
def _handle_approval_workflow_response(self, request, scheduled_job, return_url):
|
|
1904
|
+
"""Handle response for jobs requiring approval workflow."""
|
|
1905
|
+
messages.success(request, f"Job '{scheduled_job.name}' successfully submitted for approval")
|
|
1906
|
+
return redirect(return_url or reverse("extras:scheduledjob_approvalworkflow", args=[scheduled_job.pk]))
|
|
1907
|
+
|
|
1908
|
+
def _handle_scheduled_job_response(self, request, scheduled_job, return_url):
|
|
1909
|
+
"""Handle response for successfully scheduled jobs."""
|
|
1910
|
+
messages.success(request, f"Job {scheduled_job.name} successfully scheduled")
|
|
1911
|
+
return redirect(return_url or "extras:scheduledjob_list")
|
|
1912
|
+
|
|
1913
|
+
def _handle_immediate_execution(
|
|
1914
|
+
self, request, job_model, job_class, job_form, profile, ignore_singleton_lock, job_queue, return_url
|
|
1915
|
+
):
|
|
1916
|
+
"""Handle immediate job execution."""
|
|
1917
|
+
job_kwargs = job_class.prepare_job_kwargs(job_form.cleaned_data)
|
|
1918
|
+
job_result = JobResult.enqueue_job(
|
|
1919
|
+
job_model,
|
|
1920
|
+
request.user,
|
|
1921
|
+
profile=profile,
|
|
1922
|
+
ignore_singleton_lock=ignore_singleton_lock,
|
|
1923
|
+
job_queue=job_queue,
|
|
1924
|
+
**job_class.serialize_data(job_kwargs),
|
|
1925
|
+
)
|
|
1926
|
+
|
|
1927
|
+
if return_url:
|
|
1928
|
+
messages.info(
|
|
1929
|
+
request,
|
|
1930
|
+
format_html(
|
|
1931
|
+
'Job enqueued. <a href="{}">Click here for the results.</a>',
|
|
1932
|
+
job_result.get_absolute_url(),
|
|
1933
|
+
),
|
|
1934
|
+
)
|
|
1935
|
+
return redirect(return_url)
|
|
1936
|
+
|
|
1937
|
+
return redirect("extras:jobresult", pk=job_result.pk)
|
|
1938
|
+
|
|
1387
1939
|
def get(self, request, class_path=None, pk=None):
|
|
1388
1940
|
job_model = self._get_job_model_or_404(class_path, pk)
|
|
1389
1941
|
|
|
@@ -1470,13 +2022,6 @@ class JobRunView(ObjectPermissionRequiredMixin, View):
|
|
|
1470
2022
|
and request.POST.get("_schedule_type") != JobExecutionType.TYPE_IMMEDIATELY
|
|
1471
2023
|
):
|
|
1472
2024
|
messages.error(request, "Unable to schedule job: Job may have sensitive input variables.")
|
|
1473
|
-
elif job_model.has_sensitive_variables and job_model.approval_required:
|
|
1474
|
-
messages.error(
|
|
1475
|
-
request,
|
|
1476
|
-
"Unable to run or schedule job: "
|
|
1477
|
-
"This job is flagged as possibly having sensitive variables but is also flagged as requiring approval."
|
|
1478
|
-
"One of these two flags must be removed before this job can be scheduled or run.",
|
|
1479
|
-
)
|
|
1480
2025
|
elif job_form is not None and job_form.is_valid() and schedule_form.is_valid():
|
|
1481
2026
|
job_queue = job_form.cleaned_data.pop("_job_queue", None)
|
|
1482
2027
|
if job_queue is None:
|
|
@@ -1497,52 +2042,56 @@ class JobRunView(ObjectPermissionRequiredMixin, View):
|
|
|
1497
2042
|
ignore_singleton_lock = job_form.cleaned_data.pop("_ignore_singleton_lock", False)
|
|
1498
2043
|
schedule_type = schedule_form.cleaned_data["_schedule_type"]
|
|
1499
2044
|
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
2045
|
+
scheduled_job = ScheduledJob.create_schedule(
|
|
2046
|
+
job_model,
|
|
2047
|
+
request.user,
|
|
2048
|
+
name=schedule_form.cleaned_data.get("_schedule_name"),
|
|
2049
|
+
start_time=schedule_form.cleaned_data.get("_schedule_start_time"),
|
|
2050
|
+
interval=schedule_type,
|
|
2051
|
+
crontab=schedule_form.cleaned_data.get("_recurrence_custom_time"),
|
|
2052
|
+
job_queue=job_queue,
|
|
2053
|
+
profile=profile,
|
|
2054
|
+
ignore_singleton_lock=ignore_singleton_lock,
|
|
2055
|
+
validated_save=False,
|
|
2056
|
+
**job_class.serialize_data(job_form.cleaned_data),
|
|
2057
|
+
)
|
|
2058
|
+
scheduled_job_has_approval_workflow = scheduled_job.has_approval_workflow_definition()
|
|
2059
|
+
is_scheduled = schedule_type in JobExecutionType.SCHEDULE_CHOICES
|
|
2060
|
+
if job_model.has_sensitive_variables and scheduled_job_has_approval_workflow:
|
|
2061
|
+
messages.error(
|
|
2062
|
+
request,
|
|
2063
|
+
"Unable to run or schedule job: "
|
|
2064
|
+
"This job is flagged as possibly having sensitive variables but also has an applicable approval workflow definition."
|
|
2065
|
+
"Modify or remove the approval workflow definition or modify the job to set `has_sensitive_variables` to False.",
|
|
1513
2066
|
)
|
|
1514
|
-
|
|
1515
|
-
if job_model.approval_required:
|
|
1516
|
-
messages.success(request, f"Job {scheduled_job.name} successfully submitted for approval")
|
|
1517
|
-
return redirect(return_url or "extras:scheduledjob_approval_queue_list")
|
|
1518
|
-
else:
|
|
1519
|
-
messages.success(request, f"Job {scheduled_job.name} successfully scheduled")
|
|
1520
|
-
return redirect(return_url or "extras:scheduledjob_list")
|
|
1521
|
-
|
|
1522
2067
|
else:
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
2068
|
+
if dryrun and not is_scheduled:
|
|
2069
|
+
# Enqueue job for immediate execution when dryrun and (no schedule, no has_sensitive_variables)
|
|
2070
|
+
return self._handle_immediate_execution(
|
|
2071
|
+
request, job_model, job_class, job_form, profile, ignore_singleton_lock, job_queue, return_url
|
|
2072
|
+
)
|
|
2073
|
+
# Step 1: Check if approval is required
|
|
2074
|
+
if scheduled_job_has_approval_workflow:
|
|
2075
|
+
scheduled_job.validated_save()
|
|
2076
|
+
return self._handle_approval_workflow_response(request, scheduled_job, return_url)
|
|
2077
|
+
|
|
2078
|
+
# Step 3: If approval is not required
|
|
2079
|
+
if is_scheduled:
|
|
2080
|
+
scheduled_job.validated_save()
|
|
2081
|
+
return self._handle_scheduled_job_response(request, scheduled_job, return_url)
|
|
2082
|
+
|
|
2083
|
+
# Step 4: Immediate execution (no schedule, no approval)
|
|
2084
|
+
return self._handle_immediate_execution(
|
|
2085
|
+
request,
|
|
1526
2086
|
job_model,
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
2087
|
+
job_class,
|
|
2088
|
+
job_form,
|
|
2089
|
+
profile,
|
|
2090
|
+
ignore_singleton_lock,
|
|
2091
|
+
job_queue,
|
|
2092
|
+
return_url,
|
|
1532
2093
|
)
|
|
1533
2094
|
|
|
1534
|
-
if return_url:
|
|
1535
|
-
messages.info(
|
|
1536
|
-
request,
|
|
1537
|
-
format_html(
|
|
1538
|
-
'Job enqueued. <a href="{}">Click here for the results.</a>',
|
|
1539
|
-
job_result.get_absolute_url(),
|
|
1540
|
-
),
|
|
1541
|
-
)
|
|
1542
|
-
return redirect(return_url)
|
|
1543
|
-
|
|
1544
|
-
return redirect("extras:jobresult", pk=job_result.pk)
|
|
1545
|
-
|
|
1546
2095
|
if return_url:
|
|
1547
2096
|
return redirect(return_url)
|
|
1548
2097
|
|
|
@@ -1603,7 +2152,6 @@ class JobView(generic.ObjectView):
|
|
|
1603
2152
|
section=SectionChoices.RIGHT_HALF,
|
|
1604
2153
|
label="Properties",
|
|
1605
2154
|
fields=[
|
|
1606
|
-
"approval_required",
|
|
1607
2155
|
"supports_dryrun",
|
|
1608
2156
|
"dryrun_default",
|
|
1609
2157
|
"read_only",
|
|
@@ -1659,140 +2207,6 @@ class JobBulkDeleteView(generic.BulkDeleteView):
|
|
|
1659
2207
|
table = tables.JobTable
|
|
1660
2208
|
|
|
1661
2209
|
|
|
1662
|
-
class JobApprovalRequestView(generic.ObjectView):
|
|
1663
|
-
"""
|
|
1664
|
-
This view handles requests to view and approve a Job execution request.
|
|
1665
|
-
It renders the Job's form in much the same way as `JobView` except all
|
|
1666
|
-
form fields are disabled and actions on the form relate to approval of the
|
|
1667
|
-
job's execution, rather than initial job form input.
|
|
1668
|
-
"""
|
|
1669
|
-
|
|
1670
|
-
queryset = ScheduledJob.objects.needs_approved()
|
|
1671
|
-
template_name = "extras/job_approval_request.html"
|
|
1672
|
-
additional_permissions = ("extras.view_job",)
|
|
1673
|
-
|
|
1674
|
-
def get_extra_context(self, request, instance):
|
|
1675
|
-
"""
|
|
1676
|
-
Render the job form with data from the scheduled_job instance, but mark all fields as disabled.
|
|
1677
|
-
We don't care to actually get any data back from the form as we will not ever change it.
|
|
1678
|
-
Instead, we offer the user three submit buttons, dry-run, approve, and deny, which we act upon in the post.
|
|
1679
|
-
"""
|
|
1680
|
-
job_model = instance.job_model
|
|
1681
|
-
if job_model is not None:
|
|
1682
|
-
job_class = get_job(job_model.class_path, reload=True)
|
|
1683
|
-
else:
|
|
1684
|
-
# 2.0 TODO: remove this fallback?
|
|
1685
|
-
job_class = get_job(instance.job_class)
|
|
1686
|
-
|
|
1687
|
-
if job_class is not None:
|
|
1688
|
-
# Render the form with all fields disabled
|
|
1689
|
-
initial = instance.kwargs
|
|
1690
|
-
initial["_job_queue"] = instance.job_queue
|
|
1691
|
-
initial["_profile"] = instance.celery_kwargs.get("profile", False)
|
|
1692
|
-
job_form = job_class().as_form(initial=initial, approval_view=True)
|
|
1693
|
-
else:
|
|
1694
|
-
job_form = None
|
|
1695
|
-
|
|
1696
|
-
return {"job_form": job_form, **super().get_extra_context(request, instance)}
|
|
1697
|
-
|
|
1698
|
-
def post(self, request, pk):
|
|
1699
|
-
"""
|
|
1700
|
-
Act upon one of the 3 submit button actions from the user.
|
|
1701
|
-
|
|
1702
|
-
dry-run will immediately enqueue the job with commit=False and send the user to the normal JobResult view
|
|
1703
|
-
deny will delete the scheduled_job instance
|
|
1704
|
-
approve will mark the scheduled_job as approved, allowing the schedular to schedule the job execution task
|
|
1705
|
-
"""
|
|
1706
|
-
scheduled_job = get_object_or_404(ScheduledJob, pk=pk)
|
|
1707
|
-
|
|
1708
|
-
post_data = request.POST
|
|
1709
|
-
|
|
1710
|
-
deny = "_deny" in post_data
|
|
1711
|
-
approve = "_approve" in post_data
|
|
1712
|
-
force_approve = "_force_approve" in post_data
|
|
1713
|
-
dry_run = "_dry_run" in post_data
|
|
1714
|
-
|
|
1715
|
-
job_model = scheduled_job.job_model
|
|
1716
|
-
job_class = get_job(job_model.class_path, reload=True)
|
|
1717
|
-
|
|
1718
|
-
if dry_run:
|
|
1719
|
-
# To dry-run a job, a user needs the same permissions that would be needed to run the job directly
|
|
1720
|
-
if job_model is None:
|
|
1721
|
-
messages.error(request, "There is no job associated with this request? Cannot run it!")
|
|
1722
|
-
elif not job_model.runnable:
|
|
1723
|
-
messages.error(request, "This job cannot be run at this time")
|
|
1724
|
-
elif not JobModel.objects.check_perms(self.request.user, instance=job_model, action="run"):
|
|
1725
|
-
messages.error(request, "You do not have permission to run this job")
|
|
1726
|
-
elif not job_model.supports_dryrun:
|
|
1727
|
-
messages.error(request, "This job does not support dryrun")
|
|
1728
|
-
else:
|
|
1729
|
-
# Immediately enqueue the job and send the user to the normal JobResult view
|
|
1730
|
-
job_kwargs = job_class.prepare_job_kwargs(scheduled_job.kwargs or {})
|
|
1731
|
-
job_kwargs["dryrun"] = True
|
|
1732
|
-
job_result = JobResult.enqueue_job(
|
|
1733
|
-
job_model,
|
|
1734
|
-
request.user,
|
|
1735
|
-
celery_kwargs=scheduled_job.celery_kwargs,
|
|
1736
|
-
**job_class.serialize_data(job_kwargs),
|
|
1737
|
-
)
|
|
1738
|
-
|
|
1739
|
-
return redirect("extras:jobresult", pk=job_result.pk)
|
|
1740
|
-
elif deny:
|
|
1741
|
-
if not (
|
|
1742
|
-
self.queryset.check_perms(request.user, instance=scheduled_job, action="delete")
|
|
1743
|
-
and job_model is not None
|
|
1744
|
-
and JobModel.objects.check_perms(request.user, instance=job_model, action="approve")
|
|
1745
|
-
):
|
|
1746
|
-
messages.error(request, "You do not have permission to deny this request.")
|
|
1747
|
-
else:
|
|
1748
|
-
# Delete the scheduled_job instance
|
|
1749
|
-
publish_event_payload = {"data": serialize_object_v2(scheduled_job)}
|
|
1750
|
-
scheduled_job.delete()
|
|
1751
|
-
if request.user == scheduled_job.user:
|
|
1752
|
-
messages.error(request, f"Approval request for {scheduled_job.name} was revoked")
|
|
1753
|
-
else:
|
|
1754
|
-
messages.error(request, f"Approval of {scheduled_job.name} was denied")
|
|
1755
|
-
|
|
1756
|
-
publish_event(topic="nautobot.jobs.approval.denied", payload=publish_event_payload)
|
|
1757
|
-
|
|
1758
|
-
return redirect("extras:scheduledjob_approval_queue_list")
|
|
1759
|
-
|
|
1760
|
-
elif approve or force_approve:
|
|
1761
|
-
if job_model is None:
|
|
1762
|
-
messages.error(request, "There is no job associated with this request? Cannot run it!")
|
|
1763
|
-
elif not (
|
|
1764
|
-
self.queryset.check_perms(request.user, instance=scheduled_job, action="change")
|
|
1765
|
-
and JobModel.objects.check_perms(request.user, instance=job_model, action="approve")
|
|
1766
|
-
):
|
|
1767
|
-
messages.error(request, "You do not have permission to approve this request.")
|
|
1768
|
-
elif request.user == scheduled_job.user:
|
|
1769
|
-
# The requestor *cannot* approve their own job
|
|
1770
|
-
messages.error(request, "You cannot approve your own job request!")
|
|
1771
|
-
else:
|
|
1772
|
-
# Mark the scheduled_job as approved, allowing the schedular to schedule the job execution task
|
|
1773
|
-
if scheduled_job.one_off and scheduled_job.start_time < timezone.now() and not force_approve:
|
|
1774
|
-
return render(request, "extras/job_approval_confirmation.html", {"scheduled_job": scheduled_job})
|
|
1775
|
-
scheduled_job.approved_by_user = request.user
|
|
1776
|
-
scheduled_job.approved_at = timezone.now()
|
|
1777
|
-
scheduled_job.save()
|
|
1778
|
-
|
|
1779
|
-
publish_event_payload = {"data": serialize_object_v2(scheduled_job)}
|
|
1780
|
-
publish_event(topic="nautobot.jobs.approval.approved", payload=publish_event_payload)
|
|
1781
|
-
|
|
1782
|
-
messages.success(request, f"{scheduled_job.name} was approved and will now begin execution")
|
|
1783
|
-
|
|
1784
|
-
return redirect("extras:scheduledjob_approval_queue_list")
|
|
1785
|
-
|
|
1786
|
-
return render(
|
|
1787
|
-
request,
|
|
1788
|
-
self.get_template_name(),
|
|
1789
|
-
{
|
|
1790
|
-
"object": scheduled_job,
|
|
1791
|
-
**self.get_extra_context(request, scheduled_job),
|
|
1792
|
-
},
|
|
1793
|
-
)
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
2210
|
class JobQueueUIViewSet(NautobotUIViewSet):
|
|
1797
2211
|
bulk_update_form_class = forms.JobQueueBulkEditForm
|
|
1798
2212
|
filterset_form_class = forms.JobQueueFilterForm
|
|
@@ -2100,34 +2514,49 @@ class ScheduledJobBulkDeleteView(generic.BulkDeleteView):
|
|
|
2100
2514
|
filterset = filters.ScheduledJobFilterSet
|
|
2101
2515
|
|
|
2102
2516
|
|
|
2103
|
-
class ScheduledJobApprovalQueueListView(generic.ObjectListView):
|
|
2104
|
-
queryset = ScheduledJob.objects.needs_approved()
|
|
2105
|
-
table = tables.ScheduledJobApprovalQueueTable
|
|
2106
|
-
filterset = filters.ScheduledJobFilterSet
|
|
2107
|
-
filterset_form = forms.ScheduledJobFilterForm
|
|
2108
|
-
action_buttons = ()
|
|
2109
|
-
template_name = "extras/scheduled_jobs_approval_queue_list.html"
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
2517
|
class ScheduledJobView(generic.ObjectView):
|
|
2113
2518
|
queryset = ScheduledJob.objects.all()
|
|
2114
2519
|
|
|
2115
2520
|
def get_extra_context(self, request, instance):
|
|
2521
|
+
context = super().get_extra_context(request, instance)
|
|
2522
|
+
|
|
2523
|
+
# Add job class labels
|
|
2116
2524
|
job_class = get_job(instance.task)
|
|
2117
2525
|
labels = {}
|
|
2118
2526
|
if job_class is not None:
|
|
2119
2527
|
for name, var in job_class._get_vars().items():
|
|
2120
2528
|
field = var.as_field()
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2529
|
+
labels[name] = field.label or pretty_name(name)
|
|
2530
|
+
|
|
2531
|
+
context.update(
|
|
2532
|
+
{
|
|
2533
|
+
"labels": labels,
|
|
2534
|
+
"job_class_found": (job_class is not None),
|
|
2535
|
+
"default_time_zone": get_current_timezone(),
|
|
2536
|
+
}
|
|
2537
|
+
)
|
|
2538
|
+
|
|
2539
|
+
# Add approval workflow table
|
|
2540
|
+
approval_workflows = instance.associated_approval_workflows.all()
|
|
2541
|
+
approval_workflows_count = approval_workflows.count()
|
|
2542
|
+
approval_workflow_table = tables.ApprovalWorkflowTable(
|
|
2543
|
+
data=approval_workflows,
|
|
2544
|
+
user=request.user,
|
|
2545
|
+
exclude=["object_under_review", "object_under_review_content_type"],
|
|
2546
|
+
)
|
|
2547
|
+
|
|
2548
|
+
RequestConfig(
|
|
2549
|
+
request, paginate={"paginator_class": EnhancedPaginator, "per_page": get_paginate_count(request)}
|
|
2550
|
+
).configure(approval_workflow_table)
|
|
2551
|
+
|
|
2552
|
+
context.update(
|
|
2553
|
+
{
|
|
2554
|
+
"approval_workflows_count": approval_workflows_count,
|
|
2555
|
+
"approval_workflow_table": approval_workflow_table,
|
|
2556
|
+
}
|
|
2557
|
+
)
|
|
2558
|
+
|
|
2559
|
+
return context
|
|
2131
2560
|
|
|
2132
2561
|
|
|
2133
2562
|
class ScheduledJobDeleteView(generic.ObjectDeleteView):
|
|
@@ -2304,6 +2733,12 @@ class ObjectChangeUIViewSet(ObjectDetailViewMixin, ObjectListViewMixin):
|
|
|
2304
2733
|
table_class = tables.ObjectChangeTable
|
|
2305
2734
|
action_buttons = ("export",)
|
|
2306
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
|
+
|
|
2307
2742
|
# 2.0 TODO: Remove this remapping and solve it at the `BaseFilterSet` as it is addressing a breaking change.
|
|
2308
2743
|
def get(self, request, *args, **kwargs):
|
|
2309
2744
|
# Remappings below allow previous queries of time_before and time_after to use
|
|
@@ -2774,6 +3209,30 @@ class RoleUIViewSet(viewsets.NautobotUIViewSet):
|
|
|
2774
3209
|
vdc_table.columns.hide("role")
|
|
2775
3210
|
RequestConfig(request, paginate).configure(vdc_table)
|
|
2776
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
|
|
2777
3236
|
return context
|
|
2778
3237
|
|
|
2779
3238
|
|