nautobot 3.0.0a2__py3-none-any.whl → 3.0.0a3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of nautobot might be problematic. Click here for more details.
- nautobot/apps/choices.py +0 -2
- nautobot/apps/filters.py +7 -9
- nautobot/apps/models.py +2 -2
- nautobot/apps/ui.py +9 -1
- nautobot/circuits/filters.py +3 -2
- nautobot/circuits/navigation.py +3 -2
- nautobot/circuits/templates/circuits/circuit.html +1 -1
- nautobot/circuits/templates/circuits/circuit_create.html +3 -3
- nautobot/circuits/templates/circuits/circuittermination.html +1 -1
- nautobot/circuits/templates/circuits/circuittermination_create.html +9 -24
- nautobot/circuits/templates/circuits/circuittype.html +1 -1
- nautobot/circuits/templates/circuits/inc/circuit_termination_cable_fragment.html +6 -6
- nautobot/circuits/templates/circuits/inc/speed_widget.html +12 -12
- nautobot/circuits/templates/circuits/providernetwork.html +1 -1
- nautobot/circuits/tests/integration/test_circuit.py +10 -13
- nautobot/cloud/filters.py +1 -1
- nautobot/cloud/navigation.py +3 -2
- nautobot/core/api/schema.py +1 -1
- nautobot/core/api/serializers.py +6 -1
- nautobot/core/api/urls.py +1 -0
- nautobot/core/api/views.py +8 -0
- nautobot/core/apps/__init__.py +11 -10
- nautobot/core/celery/__init__.py +3 -5
- nautobot/core/checks.py +46 -0
- nautobot/core/cli/bootstrap_v3_to_v5.py +70 -1
- nautobot/core/cli/migrate_deprecated_templates.py +200 -0
- nautobot/core/constants.py +3 -0
- nautobot/core/context_processors.py +9 -1
- nautobot/core/forms/forms.py +1 -1
- nautobot/core/jobs/__init__.py +6 -3
- nautobot/core/jobs/groups.py +31 -1
- nautobot/core/management/commands/generate_test_data.py +28 -9
- nautobot/core/models/generics.py +9 -1
- nautobot/core/models/tree_queries.py +10 -5
- nautobot/core/settings.py +18 -12
- nautobot/core/settings.yaml +13 -7
- nautobot/core/signals.py +12 -1
- nautobot/core/tables.py +13 -6
- nautobot/core/templates/40x.html +1 -1
- nautobot/core/templates/500.html +2 -2
- nautobot/core/templates/admin/config/config.html +12 -12
- nautobot/core/templates/admin/index.html +3 -3
- nautobot/core/templates/buttons/export.html +1 -1
- nautobot/core/templates/components/button/dropdown.html +5 -3
- nautobot/core/templates/components/panel/body_wrapper_generic_table.html +1 -1
- nautobot/core/templates/components/panel/panel.html +3 -3
- nautobot/core/templates/components/tab/content_wrapper.html +2 -3
- nautobot/core/templates/components/tab/label_wrapper_distinct_view.html +1 -1
- nautobot/core/templates/echarts/echarts.html +1 -1
- nautobot/core/templates/generic/object_bulk_add_component.html +2 -1
- nautobot/core/templates/generic/object_bulk_create.html +4 -3
- nautobot/core/templates/generic/object_bulk_destroy.html +3 -3
- nautobot/core/templates/generic/object_bulk_remove.html +2 -2
- nautobot/core/templates/generic/object_bulk_update.html +5 -4
- nautobot/core/templates/generic/object_create.html +5 -4
- nautobot/core/templates/generic/object_import.html +2 -1
- nautobot/core/templates/generic/object_list.html +12 -4
- nautobot/core/templates/generic/object_notes.html +5 -3
- nautobot/core/templates/generic/object_retrieve.html +2 -3
- nautobot/core/templates/graphene/graphiql.html +7 -7
- nautobot/core/templates/home.html +1 -1
- nautobot/core/templates/import_success.html +2 -1
- nautobot/core/templates/inc/computed_fields/panel_data.html +1 -1
- nautobot/core/templates/inc/created_updated.html +7 -3
- nautobot/core/templates/inc/custom_fields/panel_data.html +1 -1
- nautobot/core/templates/inc/form_static_field.html +6 -0
- nautobot/core/templates/inc/header.html +1 -1
- nautobot/core/templates/inc/image_attachments.html +2 -1
- nautobot/core/templates/inc/nav_menu.html +2 -1
- nautobot/core/templates/inc/search_panel.html +4 -4
- nautobot/core/templates/login.html +4 -2
- nautobot/core/templates/nautobot_config.py.j2 +6 -5
- nautobot/core/templates/redoc_ui.html +7 -0
- nautobot/core/templates/search.html +1 -1
- nautobot/core/templates/swagger_ui.html +17 -3
- nautobot/core/templates/system_jobs/import_objects.html +1 -2
- nautobot/core/templates/utilities/confirmation_form.html +2 -2
- nautobot/core/templates/utilities/obj_table.html +10 -2
- nautobot/core/templates/utilities/render_field.html +7 -7
- nautobot/core/templates/utilities/render_jinja2.html +2 -2
- nautobot/core/templates/utilities/templatetags/filter_form_drawer.html +4 -4
- nautobot/core/templates/utilities/theme_preview.html +16 -3
- nautobot/core/templates/widgets/selectwithdisabled_option.html +3 -1
- nautobot/core/templatetags/helpers.py +52 -6
- nautobot/core/testing/api.py +68 -9
- nautobot/core/testing/filters.py +0 -23
- nautobot/core/testing/integration.py +23 -10
- nautobot/core/testing/mixins.py +2 -0
- nautobot/core/testing/views.py +4 -0
- nautobot/core/tests/integration/test_app_home.py +34 -30
- nautobot/core/tests/integration/test_app_navbar.py +3 -0
- nautobot/core/tests/nautobot_config_without_example_apps.py +4 -0
- nautobot/core/tests/runner.py +9 -1
- nautobot/core/tests/test_api.py +5 -3
- nautobot/core/tests/test_breadcrumbs.py +6 -7
- nautobot/core/tests/test_checks.py +28 -0
- nautobot/core/tests/test_cli.py +40 -0
- nautobot/core/tests/test_config.py +2 -1
- nautobot/core/tests/test_forms.py +55 -13
- nautobot/core/tests/test_jobs.py +75 -1
- nautobot/core/tests/test_nautobot_server.py +2 -0
- nautobot/core/tests/test_navigations.py +76 -1
- nautobot/core/tests/test_patch_social_django.py +42 -0
- nautobot/core/tests/test_tables.py +3 -1
- nautobot/core/tests/test_templatetags_helpers.py +53 -13
- nautobot/core/tests/test_templatetags_ui_framework.py +4 -4
- nautobot/core/tests/test_tree_queries.py +14 -1
- nautobot/core/tests/test_ui.py +1 -1
- nautobot/core/tests/test_utils.py +31 -4
- nautobot/core/tests/test_views.py +159 -31
- nautobot/core/ui/breadcrumbs.py +2 -12
- nautobot/core/ui/choices.py +142 -10
- nautobot/core/ui/constants.py +76 -12
- nautobot/core/ui/object_detail.py +92 -12
- nautobot/core/urls.py +12 -1
- nautobot/core/utils/cache.py +2 -1
- nautobot/core/utils/filtering.py +17 -17
- nautobot/core/utils/lookup.py +3 -8
- nautobot/core/utils/module_loading.py +21 -0
- nautobot/core/utils/patch_social_django.py +128 -0
- nautobot/core/views/__init__.py +38 -1
- nautobot/core/views/generic.py +3 -3
- nautobot/core/views/mixins.py +15 -3
- nautobot/core/views/renderers.py +2 -0
- nautobot/core/views/viewsets.py +2 -1
- nautobot/data_validation/apps.py +1 -5
- nautobot/data_validation/custom_validators.py +4 -4
- nautobot/data_validation/filters.py +1 -1
- nautobot/data_validation/forms.py +40 -0
- nautobot/data_validation/migrations/0001_initial.py +0 -7
- nautobot/data_validation/migrations/0002_data_migration_from_app.py +0 -12
- nautobot/data_validation/models.py +16 -7
- nautobot/data_validation/navigation.py +8 -1
- nautobot/data_validation/tables.py +12 -5
- nautobot/data_validation/templates/data_validation/datacompliance_tab.html +1 -0
- nautobot/data_validation/templates/data_validation/device_constraints.html +61 -0
- nautobot/data_validation/tests/__init__.py +2 -2
- nautobot/data_validation/tests/migrations/test_migrations.py +83 -3
- nautobot/data_validation/tests/test_data_compliance_rules.py +12 -7
- nautobot/data_validation/tests/test_filters.py +8 -6
- nautobot/data_validation/tests/test_models.py +15 -0
- nautobot/data_validation/tests/test_views.py +190 -32
- nautobot/data_validation/urls.py +2 -5
- nautobot/data_validation/views.py +73 -40
- nautobot/dcim/api/serializers.py +0 -13
- 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} +2 -35
- nautobot/dcim/forms.py +1 -1
- 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/device_components.py +81 -68
- nautobot/dcim/models/devices.py +13 -16
- nautobot/dcim/navigation.py +7 -6
- nautobot/dcim/tables/devices.py +3 -0
- nautobot/dcim/tables/template_code.py +14 -14
- nautobot/dcim/templates/dcim/cable.html +2 -61
- nautobot/dcim/templates/dcim/cable_connect.html +28 -112
- nautobot/dcim/templates/dcim/cable_edit.html +2 -5
- nautobot/dcim/templates/dcim/cable_retrieve.html +61 -0
- nautobot/dcim/templates/dcim/cable_trace.html +1 -3
- nautobot/dcim/templates/dcim/cable_update.html +5 -0
- nautobot/dcim/templates/dcim/consoleport.html +6 -5
- nautobot/dcim/templates/dcim/consoleserverport.html +6 -5
- nautobot/dcim/templates/dcim/device/config.html +2 -2
- nautobot/dcim/templates/dcim/device/consoleports.html +1 -1
- nautobot/dcim/templates/dcim/device/consoleserverports.html +1 -1
- nautobot/dcim/templates/dcim/device/devicebays.html +1 -1
- nautobot/dcim/templates/dcim/device/frontports.html +1 -1
- nautobot/dcim/templates/dcim/device/interfaces.html +1 -1
- nautobot/dcim/templates/dcim/device/inventory.html +1 -1
- nautobot/dcim/templates/dcim/device/lldp_neighbors.html +1 -1
- nautobot/dcim/templates/dcim/device/modulebays.html +1 -1
- nautobot/dcim/templates/dcim/device/poweroutlets.html +1 -1
- nautobot/dcim/templates/dcim/device/powerports.html +1 -1
- nautobot/dcim/templates/dcim/device/rearports.html +1 -1
- nautobot/dcim/templates/dcim/device/status.html +8 -8
- nautobot/dcim/templates/dcim/device/wireless.html +1 -1
- nautobot/dcim/templates/dcim/device.html +1 -1
- nautobot/dcim/templates/dcim/device_component_add.html +2 -2
- nautobot/dcim/templates/dcim/device_create.html +5 -3
- nautobot/dcim/templates/dcim/device_interface_delete.html +1 -1
- nautobot/dcim/templates/dcim/device_list.html +73 -10
- nautobot/dcim/templates/dcim/devicebay_populate.html +2 -2
- nautobot/dcim/templates/dcim/devicetype.html +1 -1
- nautobot/dcim/templates/dcim/devicetype_component_add.html +2 -2
- nautobot/dcim/templates/dcim/footer_convert_to_contact_or_team_record.html +14 -0
- nautobot/dcim/templates/dcim/frontport.html +9 -8
- nautobot/dcim/templates/dcim/inc/edit_form_softwareversion_js.html +2 -2
- nautobot/dcim/templates/dcim/interface.html +26 -6
- nautobot/dcim/templates/dcim/interface_bulk_delete.html +1 -1
- nautobot/dcim/templates/dcim/inventoryitem_add.html +3 -1
- nautobot/dcim/templates/dcim/inventoryitem_bulk_delete.html +1 -1
- nautobot/dcim/templates/dcim/inventoryitem_edit.html +3 -1
- nautobot/dcim/templates/dcim/location_retrieve.html +1 -242
- nautobot/dcim/templates/dcim/module/base.html +49 -9
- nautobot/dcim/templates/dcim/module_list.html +57 -8
- nautobot/dcim/templates/dcim/modulefamily_retrieve.html +1 -1
- nautobot/dcim/templates/dcim/moduletype_retrieve.html +49 -9
- nautobot/dcim/templates/dcim/platform_create.html +1 -1
- nautobot/dcim/templates/dcim/powerfeed.html +1 -1
- nautobot/dcim/templates/dcim/powerpanel.html +1 -1
- nautobot/dcim/templates/dcim/powerport.html +5 -4
- nautobot/dcim/templates/dcim/rack_elevation_list.html +16 -4
- nautobot/dcim/templates/dcim/rack_retrieve.html +33 -15
- nautobot/dcim/templates/dcim/rearport.html +7 -6
- nautobot/dcim/templates/dcim/virtualchassis.html +1 -1
- nautobot/dcim/templates/dcim/virtualchassis_add_member.html +16 -14
- nautobot/dcim/templates/dcim/virtualchassis_update.html +14 -6
- nautobot/dcim/tests/integration/test_controller.py +1 -0
- nautobot/dcim/tests/test_api.py +8 -0
- nautobot/dcim/tests/test_custom_validators.py +229 -0
- nautobot/dcim/tests/test_filters.py +12 -6
- nautobot/dcim/tests/test_models.py +63 -4
- nautobot/dcim/tests/test_views.py +63 -22
- nautobot/dcim/urls.py +64 -21
- nautobot/dcim/utils.py +3 -3
- nautobot/dcim/views.py +547 -273
- nautobot/extras/api/views.py +9 -1
- nautobot/extras/choices.py +2 -13
- nautobot/extras/{filters/mixins.py → filter_mixins.py} +1 -1
- nautobot/extras/{filters/customfields.py → filter_mixins_customfields.py} +42 -6
- nautobot/extras/{filters/__init__.py → filters.py} +14 -46
- nautobot/extras/forms/forms.py +5 -13
- nautobot/extras/forms/mixins.py +0 -41
- nautobot/extras/management/__init__.py +9 -0
- nautobot/extras/migrations/0127_approval_workflow_models.py +6 -6
- 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 +1 -2
- nautobot/extras/models/approvals.py +22 -13
- nautobot/extras/models/contacts.py +2 -0
- nautobot/extras/models/groups.py +44 -5
- nautobot/extras/models/jobs.py +59 -1
- nautobot/extras/models/mixins.py +28 -0
- nautobot/extras/models/models.py +13 -0
- nautobot/extras/models/secrets.py +1 -0
- nautobot/extras/models/statuses.py +0 -15
- nautobot/extras/navigation.py +13 -9
- nautobot/extras/plugins/__init__.py +33 -55
- nautobot/extras/plugins/tables.py +3 -3
- nautobot/extras/plugins/urls.py +2 -21
- nautobot/extras/plugins/utils.py +1 -33
- nautobot/extras/plugins/views.py +0 -4
- nautobot/extras/signals.py +20 -19
- nautobot/extras/tables.py +52 -68
- nautobot/extras/templates/extras/approval_dashboard.html +7 -5
- nautobot/extras/templates/extras/approvalworkflowdefinition_update.html +4 -2
- nautobot/extras/templates/extras/approvalworkflowstage_retrieve.html +20 -12
- nautobot/extras/templates/extras/computedfield.html +1 -1
- nautobot/extras/templates/extras/configcontext.html +1 -1
- nautobot/extras/templates/extras/configcontextschema_validation.html +2 -2
- nautobot/extras/templates/extras/customfield.html +1 -1
- nautobot/extras/templates/extras/dynamicgroup_retrieve.html +11 -5
- nautobot/extras/templates/extras/dynamicgroup_update.html +1 -1
- nautobot/extras/templates/extras/gitrepository_result.html +0 -2
- nautobot/extras/templates/extras/graphqlquery_retrieve.html +1 -96
- nautobot/extras/templates/extras/inc/approval_buttons_column.html +20 -6
- nautobot/extras/templates/extras/inc/bulk_edit_overridable_field.html +8 -7
- nautobot/extras/templates/extras/inc/configcontext_format.html +10 -3
- nautobot/extras/templates/extras/inc/graphqlquery_execute.html +71 -0
- nautobot/extras/templates/extras/inc/job_tiles.html +15 -3
- nautobot/extras/templates/extras/inc/json_format.html +10 -3
- nautobot/extras/templates/extras/inc/overridable_field.html +13 -12
- nautobot/extras/templates/extras/job.html +29 -12
- nautobot/extras/templates/extras/job_bulk_edit.html +18 -0
- nautobot/extras/templates/extras/job_edit.html +52 -46
- nautobot/extras/templates/extras/job_list.html +29 -25
- nautobot/extras/templates/extras/marketplace.html +5 -9
- nautobot/extras/templates/extras/object_configcontext.html +1 -1
- nautobot/extras/templates/extras/object_dynamicgroups.html +2 -2
- nautobot/extras/templates/extras/objectchange_retrieve.html +19 -37
- nautobot/extras/templates/extras/plugin_detail.html +26 -21
- nautobot/extras/templates/extras/plugins_list.html +16 -26
- nautobot/extras/templates/extras/role_retrieve.html +64 -0
- nautobot/extras/templates/extras/scheduledjob.html +4 -2
- nautobot/extras/templates/extras/secretsgroup.html +1 -1
- nautobot/extras/templates/extras/tag.html +1 -1
- 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_configcontextschema.py +27 -26
- nautobot/extras/tests/integration/test_customfields.py +8 -7
- nautobot/extras/tests/integration/test_dynamicgroups.py +5 -1
- nautobot/extras/tests/integration/test_plugin_banner.py +3 -0
- nautobot/extras/tests/integration/test_plugins.py +18 -6
- nautobot/extras/tests/test_api.py +27 -18
- nautobot/extras/tests/test_approvals.py +38 -38
- nautobot/extras/tests/test_changelog.py +35 -3
- nautobot/extras/tests/test_customfields.py +22 -13
- nautobot/extras/tests/test_customfields_filters.py +479 -0
- nautobot/extras/tests/test_dynamicgroups.py +39 -1
- nautobot/extras/tests/test_filters.py +21 -19
- nautobot/extras/tests/test_forms.py +18 -21
- nautobot/extras/tests/test_jobs.py +25 -4
- nautobot/extras/tests/test_migrations.py +1 -0
- nautobot/extras/tests/test_models.py +13 -31
- nautobot/extras/tests/test_plugins.py +36 -10
- nautobot/extras/tests/test_views.py +31 -30
- nautobot/extras/views.py +81 -19
- nautobot/ipam/factory.py +7 -0
- nautobot/ipam/filter_mixins.py +38 -0
- nautobot/ipam/filters.py +27 -38
- 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 -2
- nautobot/ipam/signals.py +71 -0
- nautobot/ipam/tables.py +13 -6
- nautobot/ipam/templates/ipam/inc/toggle_available.html +10 -10
- nautobot/ipam/templates/ipam/inc/vlangroup_header.html +1 -0
- nautobot/ipam/templates/ipam/ipaddress.html +14 -0
- nautobot/ipam/templates/ipam/ipaddress_merge.html +3 -3
- nautobot/ipam/templates/ipam/ipaddresstointerface_retrieve.html +1 -0
- nautobot/ipam/templates/ipam/namespace_update.html +15 -0
- nautobot/ipam/templates/ipam/prefix_delete.html +1 -1
- nautobot/ipam/templates/ipam/prefix_list.html +14 -13
- nautobot/ipam/templates/ipam/service.html +1 -1
- nautobot/ipam/templates/ipam/vlan.html +1 -1
- nautobot/ipam/templates/ipam/vlan_interfaces.html +1 -1
- nautobot/ipam/templates/ipam/vlan_vminterfaces.html +1 -1
- nautobot/ipam/tests/migration/test_migrations.py +89 -0
- nautobot/ipam/tests/test_api.py +13 -6
- nautobot/ipam/tests/test_filters.py +10 -0
- nautobot/ipam/tests/test_forms.py +1 -1
- nautobot/ipam/tests/test_models.py +43 -1
- nautobot/ipam/tests/test_tables.py +1 -2
- nautobot/ipam/tests/test_utils.py +1 -1
- nautobot/ipam/tests/test_views.py +13 -14
- nautobot/ipam/ui.py +0 -17
- nautobot/ipam/utils/migrations.py +16 -2
- nautobot/ipam/utils/testing.py +9 -3
- nautobot/ipam/views.py +46 -6
- nautobot/project-static/dist/css/nautobot.css +1 -1
- nautobot/project-static/dist/css/nautobot.css.map +1 -1
- nautobot/project-static/dist/js/nautobot.js +1 -1
- nautobot/project-static/dist/js/nautobot.js.map +1 -1
- nautobot/project-static/js/cabletrace.js +1 -1
- nautobot/project-static/js/interface_filtering.js +20 -16
- nautobot/project-static/nautobot-icons/battery-3.svg +3 -0
- nautobot/project-static/nautobot-icons/cloud.svg +1 -1
- nautobot/project-static/nautobot-icons/control-panel.svg +1 -1
- nautobot/project-static/nautobot-icons/device-lifecycle.svg +1 -1
- nautobot/project-static/nautobot-icons/elements.svg +1 -1
- nautobot/project-static/nautobot-icons/extensibility.svg +3 -0
- nautobot/project-static/nautobot-icons/hammer.svg +1 -1
- nautobot/project-static/nautobot-icons/organization.svg +3 -0
- nautobot/project-static/nautobot-icons/secrets.svg +1 -1
- nautobot/project-static/nautobot-icons/security.svg +3 -0
- nautobot/project-static/nautobot-icons/server.svg +1 -1
- nautobot/project-static/nautobot-icons/star-filled.svg +1 -1
- nautobot/project-static/nautobot-icons/star.svg +1 -1
- 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/tests/test_filters.py +0 -2
- nautobot/tenancy/views.py +2 -1
- nautobot/ui/src/js/collapse.js +3 -3
- nautobot/ui/src/js/nautobot.js +16 -0
- nautobot/ui/src/scss/colors.scss +1 -1
- nautobot/ui/src/scss/nautobot.scss +61 -28
- nautobot/users/templates/users/profile.html +45 -12
- nautobot/users/templates/users/sessionkey_delete.html +1 -1
- nautobot/users/tests/test_api.py +4 -0
- nautobot/users/views.py +4 -2
- nautobot/virtualization/models.py +1 -68
- nautobot/virtualization/navigation.py +3 -2
- nautobot/virtualization/templates/virtualization/virtual_machine_vminterface_delete.html +1 -1
- nautobot/virtualization/templates/virtualization/virtualmachine.html +1 -1
- nautobot/virtualization/templates/virtualization/virtualmachine_list.html +2 -2
- nautobot/virtualization/templates/virtualization/virtualmachine_update.html +3 -1
- nautobot/virtualization/tests/test_api.py +3 -0
- nautobot/virtualization/tests/test_models.py +44 -4
- 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/navigation.py +3 -2
- nautobot/wireless/tests/integration/test_radio_profile.py +1 -5
- nautobot/wireless/tests/test_api.py +1 -1
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/METADATA +14 -14
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/RECORD +417 -366
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/entry_points.txt +1 -0
- nautobot/data_validation/template_content.py +0 -42
- nautobot/dcim/filters/mixins.py +0 -354
- nautobot/ipam/templates/ipam/inc/prefix_header_extra_content_table.html +0 -4
- /nautobot/tenancy/{filters/mixins.py → filter_mixins.py} +0 -0
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/LICENSE.txt +0 -0
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/NOTICE +0 -0
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/WHEEL +0 -0
|
@@ -5,9 +5,10 @@ import inspect
|
|
|
5
5
|
from logging import getLogger
|
|
6
6
|
|
|
7
7
|
from django.conf import settings
|
|
8
|
+
from django.conf.urls import include
|
|
8
9
|
from django.core.exceptions import ValidationError
|
|
9
10
|
from django.template.loader import get_template
|
|
10
|
-
from django.urls import get_resolver, URLPattern
|
|
11
|
+
from django.urls import clear_url_caches, get_resolver, path, URLPattern
|
|
11
12
|
from packaging import version
|
|
12
13
|
|
|
13
14
|
from nautobot.core.apps import (
|
|
@@ -17,10 +18,9 @@ from nautobot.core.apps import (
|
|
|
17
18
|
register_menu_items,
|
|
18
19
|
)
|
|
19
20
|
from nautobot.core.signals import nautobot_database_ready
|
|
20
|
-
from nautobot.core.utils.
|
|
21
|
+
from nautobot.core.utils.module_loading import import_string_optional
|
|
21
22
|
from nautobot.extras.choices import BannerClassChoices
|
|
22
23
|
from nautobot.extras.plugins.exceptions import PluginImproperlyConfigured
|
|
23
|
-
from nautobot.extras.plugins.utils import import_object
|
|
24
24
|
from nautobot.extras.registry import register_datasource_contents, registry
|
|
25
25
|
from nautobot.extras.secrets import register_secrets_provider
|
|
26
26
|
|
|
@@ -105,9 +105,24 @@ class NautobotAppConfig(NautobotConfig):
|
|
|
105
105
|
"""Callback after plugin app is loaded."""
|
|
106
106
|
# We don't call super().ready here because we don't need or use the on-ready behavior of a core Nautobot app
|
|
107
107
|
|
|
108
|
+
from nautobot.extras.plugins.urls import BASE_URL_TO_APP_LABEL, plugin_api_patterns, plugin_patterns
|
|
109
|
+
|
|
108
110
|
# Introspect URL patterns and models to make available to the installed-plugins detail UI view.
|
|
109
|
-
urlpatterns =
|
|
110
|
-
api_urlpatterns =
|
|
111
|
+
urlpatterns = import_string_optional(f"{self.__module__}.urls.urlpatterns")
|
|
112
|
+
api_urlpatterns = import_string_optional(f"{self.__module__}.api.urls.urlpatterns")
|
|
113
|
+
|
|
114
|
+
base_url = self.base_url or self.label
|
|
115
|
+
|
|
116
|
+
if urlpatterns is not None:
|
|
117
|
+
plugin_patterns.append(path(f"{base_url}/", include((urlpatterns, self.label))))
|
|
118
|
+
|
|
119
|
+
if api_urlpatterns is not None:
|
|
120
|
+
plugin_api_patterns.append(path(f"{base_url}/", include((api_urlpatterns, f"{self.label}-api"))))
|
|
121
|
+
|
|
122
|
+
if any([urlpatterns, api_urlpatterns]):
|
|
123
|
+
clear_url_caches()
|
|
124
|
+
|
|
125
|
+
BASE_URL_TO_APP_LABEL[base_url] = self.label
|
|
111
126
|
|
|
112
127
|
self.features = {
|
|
113
128
|
"api_urlpatterns": sorted(
|
|
@@ -123,36 +138,31 @@ class NautobotAppConfig(NautobotConfig):
|
|
|
123
138
|
}
|
|
124
139
|
|
|
125
140
|
# Register banner function (if defined)
|
|
126
|
-
banner_function
|
|
127
|
-
if banner_function is not None:
|
|
141
|
+
if banner_function := import_string_optional(f"{self.__module__}.{self.banner_function}"):
|
|
128
142
|
register_banner_function(banner_function)
|
|
129
143
|
self.features["banner"] = True
|
|
130
144
|
|
|
131
145
|
# Register model validators (if defined)
|
|
132
|
-
validators
|
|
133
|
-
if validators is not None:
|
|
146
|
+
if validators := import_string_optional(f"{self.__module__}.{self.custom_validators}"):
|
|
134
147
|
register_custom_validators(validators)
|
|
135
148
|
self.features["custom_validators"] = sorted(set(validator.model for validator in validators))
|
|
136
149
|
|
|
137
150
|
# Register datasource contents (if defined)
|
|
138
|
-
datasource_contents
|
|
139
|
-
if datasource_contents is not None:
|
|
151
|
+
if datasource_contents := import_string_optional(f"{self.__module__}.{self.datasource_contents}"):
|
|
140
152
|
register_datasource_contents(datasource_contents)
|
|
141
153
|
self.features["datasource_contents"] = datasource_contents
|
|
142
154
|
|
|
143
155
|
# Register GraphQL types (if defined)
|
|
144
|
-
graphql_types
|
|
145
|
-
if graphql_types is not None:
|
|
156
|
+
if graphql_types := import_string_optional(f"{self.__module__}.{self.graphql_types}"):
|
|
146
157
|
register_graphql_types(graphql_types)
|
|
147
158
|
|
|
148
159
|
# Import jobs (if present)
|
|
149
160
|
# Note that we do *not* auto-call `register_jobs()` - the App is responsible for doing so when imported.
|
|
150
|
-
jobs
|
|
151
|
-
if jobs is not None:
|
|
161
|
+
if jobs := import_string_optional(f"{self.__module__}.{self.jobs}"):
|
|
152
162
|
self.features["jobs"] = jobs
|
|
153
163
|
|
|
154
164
|
# Import metrics (if present)
|
|
155
|
-
metrics =
|
|
165
|
+
metrics = import_string_optional(f"{self.__module__}.{self.metrics}")
|
|
156
166
|
if metrics is not None and self.name not in settings.METRICS_DISABLED_APPS:
|
|
157
167
|
register_metrics(metrics)
|
|
158
168
|
self.features["metrics"] = [] # Initialize as empty, to be filled by the signal handler
|
|
@@ -161,19 +171,16 @@ class NautobotAppConfig(NautobotConfig):
|
|
|
161
171
|
nautobot_database_ready.connect(signal_callback, sender=self)
|
|
162
172
|
|
|
163
173
|
# Register plugin navigation menu items (if defined)
|
|
164
|
-
menu_items
|
|
165
|
-
if menu_items is not None:
|
|
174
|
+
if menu_items := import_string_optional(f"{self.__module__}.{self.menu_items}"):
|
|
166
175
|
register_plugin_menu_items(self.verbose_name, menu_items)
|
|
167
176
|
self.features["nav_menu"] = menu_items
|
|
168
177
|
|
|
169
|
-
homepage_layout
|
|
170
|
-
if homepage_layout is not None:
|
|
178
|
+
if homepage_layout := import_string_optional(f"{self.__module__}.{self.homepage_layout}"):
|
|
171
179
|
register_homepage_panels(self.path, self.label, homepage_layout)
|
|
172
180
|
self.features["home_page"] = homepage_layout
|
|
173
181
|
|
|
174
182
|
# Register template content (if defined)
|
|
175
|
-
template_extensions
|
|
176
|
-
if template_extensions is not None:
|
|
183
|
+
if template_extensions := import_string_optional(f"{self.__module__}.{self.template_extensions}"):
|
|
177
184
|
register_template_extensions(template_extensions)
|
|
178
185
|
self.features["template_extensions"] = sorted(set(extension.model for extension in template_extensions))
|
|
179
186
|
|
|
@@ -185,15 +192,13 @@ class NautobotAppConfig(NautobotConfig):
|
|
|
185
192
|
pass
|
|
186
193
|
|
|
187
194
|
# Register secrets providers (if any)
|
|
188
|
-
secrets_providers
|
|
189
|
-
if secrets_providers is not None:
|
|
195
|
+
if secrets_providers := import_string_optional(f"{self.__module__}.{self.secrets_providers}"):
|
|
190
196
|
for secrets_provider in secrets_providers:
|
|
191
197
|
register_secrets_provider(secrets_provider)
|
|
192
198
|
self.features["secrets_providers"] = secrets_providers
|
|
193
199
|
|
|
194
200
|
# Register custom filters (if any)
|
|
195
|
-
filter_extensions
|
|
196
|
-
if filter_extensions is not None:
|
|
201
|
+
if filter_extensions := import_string_optional(f"{self.__module__}.{self.filter_extensions}"):
|
|
197
202
|
register_filter_extensions(filter_extensions, self.name)
|
|
198
203
|
self.features["filter_extensions"] = {"filterset_fields": [], "filterform_fields": []}
|
|
199
204
|
for filter_extension in filter_extensions:
|
|
@@ -207,8 +212,7 @@ class NautobotAppConfig(NautobotConfig):
|
|
|
207
212
|
)
|
|
208
213
|
|
|
209
214
|
# Register override view (if any)
|
|
210
|
-
override_views
|
|
211
|
-
if override_views is not None:
|
|
215
|
+
if override_views := import_string_optional(f"{self.__module__}.{self.override_views}"):
|
|
212
216
|
for qualified_view_name, view in override_views.items():
|
|
213
217
|
view_class_name = view.view_class.__name__ if hasattr(view, "view_class") else view.cls.__name__
|
|
214
218
|
self.features.setdefault("overridden_views", []).append(
|
|
@@ -272,17 +276,11 @@ class NautobotAppConfig(NautobotConfig):
|
|
|
272
276
|
|
|
273
277
|
def _register_table_extensions(self):
|
|
274
278
|
"""Register tables extensions (if any)."""
|
|
275
|
-
table_extensions
|
|
276
|
-
if table_extensions is not None:
|
|
279
|
+
if table_extensions := import_string_optional(f"{self.__module__}.{self.table_extensions}"):
|
|
277
280
|
register_table_extensions(table_extensions, self.name)
|
|
278
281
|
self.features["table_extensions"] = get_table_extension_features(table_extensions)
|
|
279
282
|
|
|
280
283
|
|
|
281
|
-
@class_deprecated_in_favor_of(NautobotAppConfig)
|
|
282
|
-
class PluginConfig(NautobotAppConfig):
|
|
283
|
-
pass
|
|
284
|
-
|
|
285
|
-
|
|
286
284
|
#
|
|
287
285
|
# Template content injection
|
|
288
286
|
#
|
|
@@ -413,11 +411,6 @@ class TemplateExtension:
|
|
|
413
411
|
raise NotImplementedError
|
|
414
412
|
|
|
415
413
|
|
|
416
|
-
@class_deprecated_in_favor_of(TemplateExtension)
|
|
417
|
-
class PluginTemplateExtension(TemplateExtension):
|
|
418
|
-
pass
|
|
419
|
-
|
|
420
|
-
|
|
421
414
|
def register_template_extensions(class_list):
|
|
422
415
|
"""
|
|
423
416
|
Register a list of TemplateExtension classes
|
|
@@ -444,11 +437,6 @@ class Banner:
|
|
|
444
437
|
self.banner_class = banner_class
|
|
445
438
|
|
|
446
439
|
|
|
447
|
-
@class_deprecated_in_favor_of(Banner)
|
|
448
|
-
class PluginBanner(Banner):
|
|
449
|
-
pass
|
|
450
|
-
|
|
451
|
-
|
|
452
440
|
def register_banner_function(function):
|
|
453
441
|
"""
|
|
454
442
|
Register a function that may return a Banner object.
|
|
@@ -494,11 +482,6 @@ class FilterExtension:
|
|
|
494
482
|
filterform_fields = {}
|
|
495
483
|
|
|
496
484
|
|
|
497
|
-
@class_deprecated_in_favor_of(FilterExtension)
|
|
498
|
-
class PluginFilterExtension(FilterExtension):
|
|
499
|
-
pass
|
|
500
|
-
|
|
501
|
-
|
|
502
485
|
def register_filter_extensions(filter_extensions, plugin_name):
|
|
503
486
|
"""
|
|
504
487
|
Register a list of FilterExtension classes
|
|
@@ -789,11 +772,6 @@ class CustomValidator:
|
|
|
789
772
|
raise NotImplementedError
|
|
790
773
|
|
|
791
774
|
|
|
792
|
-
@class_deprecated_in_favor_of(CustomValidator)
|
|
793
|
-
class PluginCustomValidator(CustomValidator):
|
|
794
|
-
pass
|
|
795
|
-
|
|
796
|
-
|
|
797
775
|
def register_custom_validators(class_list):
|
|
798
776
|
"""
|
|
799
777
|
Register a list of CustomValidator classes
|
|
@@ -20,21 +20,21 @@ class InstalledAppsTable(tables.Table):
|
|
|
20
20
|
{% if record.home_url %}
|
|
21
21
|
<a href="{% url record.home_url %}" class="btn btn-primary btn-xs" title="Home">
|
|
22
22
|
{% else %}
|
|
23
|
-
<a
|
|
23
|
+
<a class="btn btn-primary btn-xs disabled" aria-disabled="true" title="No home link provided">
|
|
24
24
|
{% endif %}
|
|
25
25
|
<i class="mdi mdi-home"></i>
|
|
26
26
|
</a>
|
|
27
27
|
{% if record.config_url %}
|
|
28
28
|
<a href="{% url record.config_url %}" class="btn btn-warning btn-xs" title="Configure">
|
|
29
29
|
{% else %}
|
|
30
|
-
<a
|
|
30
|
+
<a class="btn btn-warning btn-xs disabled" aria-disabled="true" title="No configuration link provided">
|
|
31
31
|
{% endif %}
|
|
32
32
|
<i class="mdi mdi-cog"></i>
|
|
33
33
|
</a>
|
|
34
34
|
{% if record.docs_url %}
|
|
35
35
|
<a href="{% url record.docs_url %}" class="btn btn-info btn-xs" title="Docs">
|
|
36
36
|
{% else %}
|
|
37
|
-
<a
|
|
37
|
+
<a class="btn btn-info btn-xs disabled" aria-disabled="true" title="No docs provided">
|
|
38
38
|
{% endif %}
|
|
39
39
|
<i class="mdi mdi-book-open-page-variant"></i>
|
|
40
40
|
</a>
|
nautobot/extras/plugins/urls.py
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
from django.apps import apps
|
|
2
|
-
from django.conf import settings
|
|
3
|
-
from django.conf.urls import include
|
|
4
1
|
from django.urls import path
|
|
5
2
|
|
|
6
|
-
from nautobot.extras.plugins.utils import import_object
|
|
7
|
-
|
|
8
3
|
from . import views
|
|
9
4
|
|
|
5
|
+
BASE_URL_TO_APP_LABEL = {}
|
|
6
|
+
|
|
10
7
|
# Initialize URL base, API, and admin URL patterns for plugins
|
|
11
8
|
apps_patterns = [
|
|
12
9
|
path("installed-apps/", views.InstalledAppsView.as_view(), name="apps_list"),
|
|
@@ -30,19 +27,3 @@ plugin_api_patterns = [
|
|
|
30
27
|
),
|
|
31
28
|
]
|
|
32
29
|
plugin_admin_patterns = []
|
|
33
|
-
|
|
34
|
-
# Register base/API URL patterns for each plugin
|
|
35
|
-
for plugin_path in settings.PLUGINS:
|
|
36
|
-
plugin_name = plugin_path.split(".")[-1]
|
|
37
|
-
app = apps.get_app_config(plugin_name)
|
|
38
|
-
base_url = app.base_url or app.label
|
|
39
|
-
|
|
40
|
-
# Check if the plugin specifies any base URLs
|
|
41
|
-
urlpatterns = import_object(f"{plugin_path}.urls.urlpatterns")
|
|
42
|
-
if urlpatterns is not None:
|
|
43
|
-
plugin_patterns.append(path(f"{base_url}/", include((urlpatterns, app.label))))
|
|
44
|
-
|
|
45
|
-
# Check if the plugin specifies any API URLs
|
|
46
|
-
urlpatterns = import_object(f"{plugin_path}.api.urls.urlpatterns")
|
|
47
|
-
if urlpatterns is not None:
|
|
48
|
-
plugin_api_patterns.append(path(f"{base_url}/", include((urlpatterns, f"{app.label}-api"))))
|
nautobot/extras/plugins/utils.py
CHANGED
|
@@ -4,7 +4,6 @@ Plugin utilities.
|
|
|
4
4
|
|
|
5
5
|
import importlib.util
|
|
6
6
|
import logging
|
|
7
|
-
import sys
|
|
8
7
|
|
|
9
8
|
from django.core.exceptions import ImproperlyConfigured
|
|
10
9
|
from django.utils.module_loading import import_string
|
|
@@ -17,37 +16,6 @@ from .exceptions import PluginImproperlyConfigured, PluginNotFound
|
|
|
17
16
|
logger = logging.getLogger(__name__)
|
|
18
17
|
|
|
19
18
|
|
|
20
|
-
def import_object(module_and_object):
|
|
21
|
-
"""
|
|
22
|
-
Import a specific object from a specific module by name, such as "nautobot.extras.plugins.utils.import_object".
|
|
23
|
-
|
|
24
|
-
Returns the imported object, or None if it doesn't exist.
|
|
25
|
-
"""
|
|
26
|
-
target_module_name, object_name = module_and_object.rsplit(".", 1)
|
|
27
|
-
module_hierarchy = target_module_name.split(".")
|
|
28
|
-
|
|
29
|
-
# Iterate through the module hierarchy, checking for the existence of each successive submodule.
|
|
30
|
-
# We have to do this rather than jumping directly to calling find_spec(target_module_name)
|
|
31
|
-
# because find_spec will raise a ModuleNotFoundError if any parent module of target_module_name does not exist.
|
|
32
|
-
module_name = ""
|
|
33
|
-
for module_component in module_hierarchy:
|
|
34
|
-
module_name = f"{module_name}.{module_component}" if module_name else module_component
|
|
35
|
-
spec = importlib.util.find_spec(module_name)
|
|
36
|
-
if spec is None:
|
|
37
|
-
# No such module
|
|
38
|
-
return None
|
|
39
|
-
|
|
40
|
-
# Okay, target_module_name exists. Load it if not already loaded
|
|
41
|
-
if target_module_name in sys.modules:
|
|
42
|
-
module = sys.modules[target_module_name]
|
|
43
|
-
else:
|
|
44
|
-
module = importlib.util.module_from_spec(spec)
|
|
45
|
-
sys.modules[target_module_name] = module
|
|
46
|
-
spec.loader.exec_module(module)
|
|
47
|
-
|
|
48
|
-
return getattr(module, object_name, None)
|
|
49
|
-
|
|
50
|
-
|
|
51
19
|
def load_plugins(settings):
|
|
52
20
|
"""Process plugins and log errors if they can't be loaded."""
|
|
53
21
|
for plugin_name in settings.PLUGINS:
|
|
@@ -77,7 +45,7 @@ def load_plugin(plugin_name, settings):
|
|
|
77
45
|
except AttributeError as err:
|
|
78
46
|
raise PluginImproperlyConfigured(
|
|
79
47
|
f"Plugin {plugin_name} does not provide a 'config' variable. This should be defined in the plugin's "
|
|
80
|
-
f"__init__.py file and point to the
|
|
48
|
+
f"__init__.py file and point to the NautobotAppConfig subclass."
|
|
81
49
|
) from err
|
|
82
50
|
|
|
83
51
|
# Validate user-provided configuration settings and assign defaults. Plugin
|
nautobot/extras/plugins/views.py
CHANGED
|
@@ -86,7 +86,6 @@ class InstalledAppsView(GenericView):
|
|
|
86
86
|
"""
|
|
87
87
|
|
|
88
88
|
table = InstalledAppsTable
|
|
89
|
-
breadcrumbs = Breadcrumbs(items={"*": [ViewNameBreadcrumbItem(view_name="apps:apps_list", label="Installed Apps")]})
|
|
90
89
|
view_titles = Titles(titles={"*": "Installed Apps"})
|
|
91
90
|
|
|
92
91
|
def get(self, request):
|
|
@@ -250,9 +249,6 @@ class MarketplaceView(GenericView):
|
|
|
250
249
|
View for listing all available Apps.
|
|
251
250
|
"""
|
|
252
251
|
|
|
253
|
-
breadcrumbs = Breadcrumbs(
|
|
254
|
-
items={"generic": [ViewNameBreadcrumbItem(view_name="apps:apps_marketplace", label="Apps Marketplace")]}
|
|
255
|
-
)
|
|
256
252
|
view_titles = Titles(titles={"generic": "Apps Marketplace"})
|
|
257
253
|
|
|
258
254
|
def get(self, request):
|
nautobot/extras/signals.py
CHANGED
|
@@ -24,7 +24,7 @@ from nautobot.core.celery import app, import_jobs
|
|
|
24
24
|
from nautobot.core.models import BaseModel
|
|
25
25
|
from nautobot.core.utils.cache import construct_cache_key
|
|
26
26
|
from nautobot.core.utils.logging import sanitize
|
|
27
|
-
from nautobot.extras.choices import JobResultStatusChoices, ObjectChangeActionChoices
|
|
27
|
+
from nautobot.extras.choices import ButtonClassChoices, JobResultStatusChoices, ObjectChangeActionChoices
|
|
28
28
|
from nautobot.extras.constants import CHANGELOG_MAX_CHANGE_CONTEXT_DETAIL
|
|
29
29
|
from nautobot.extras.models import (
|
|
30
30
|
ComputedField,
|
|
@@ -557,24 +557,6 @@ m2m_changed.connect(dynamic_group_children_changed, sender=DynamicGroup.children
|
|
|
557
557
|
pre_save.connect(dynamic_group_membership_created, sender=DynamicGroupMembership)
|
|
558
558
|
|
|
559
559
|
|
|
560
|
-
def dynamic_group_update_cached_members(sender, instance, **kwargs):
|
|
561
|
-
"""
|
|
562
|
-
When a DynamicGroup or DynamicGroupMembership is updated, update the cache of members for it and any parent groups.
|
|
563
|
-
"""
|
|
564
|
-
if isinstance(instance, DynamicGroupMembership):
|
|
565
|
-
group = instance.parent_group
|
|
566
|
-
else:
|
|
567
|
-
group = instance
|
|
568
|
-
|
|
569
|
-
group.update_cached_members()
|
|
570
|
-
for ancestor in group.get_ancestors():
|
|
571
|
-
ancestor.update_cached_members()
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
post_save.connect(dynamic_group_update_cached_members, sender=DynamicGroup)
|
|
575
|
-
post_save.connect(dynamic_group_update_cached_members, sender=DynamicGroupMembership)
|
|
576
|
-
|
|
577
|
-
|
|
578
560
|
#
|
|
579
561
|
# Jobs
|
|
580
562
|
#
|
|
@@ -659,6 +641,25 @@ def refresh_job_models(sender, *, apps, **kwargs):
|
|
|
659
641
|
job_model.installed = False
|
|
660
642
|
job_model.save()
|
|
661
643
|
|
|
644
|
+
# Wire up the JobButton for Dynamic Group member refresh
|
|
645
|
+
JobButton = apps.get_model("extras", "JobButton")
|
|
646
|
+
ContentType = apps.get_model("contenttypes", "ContentType") # pylint: disable=redefined-outer-name
|
|
647
|
+
DynamicGroup = apps.get_model("extras", "DynamicGroup") # pylint: disable=redefined-outer-name
|
|
648
|
+
|
|
649
|
+
dg_job_button, _ = JobButton.objects.get_or_create(
|
|
650
|
+
name="Refresh Dynamic Group Members Cache",
|
|
651
|
+
job=Job.objects.get(
|
|
652
|
+
module_name="nautobot.core.jobs.groups", job_class_name="RefreshDynamicGroupCacheJobButtonReceiver"
|
|
653
|
+
),
|
|
654
|
+
defaults={
|
|
655
|
+
"enabled": True,
|
|
656
|
+
"text": "Refresh Members",
|
|
657
|
+
"button_class": ButtonClassChoices.CLASS_WARNING,
|
|
658
|
+
"confirmation": True,
|
|
659
|
+
},
|
|
660
|
+
)
|
|
661
|
+
dg_job_button.content_types.add(ContentType.objects.get_for_model(DynamicGroup))
|
|
662
|
+
|
|
662
663
|
|
|
663
664
|
#
|
|
664
665
|
# Metadata
|
nautobot/extras/tables.py
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from textwrap import dedent
|
|
3
|
+
|
|
1
4
|
from django.conf import settings
|
|
2
|
-
from django.
|
|
5
|
+
from django.contrib.contenttypes.models import ContentType
|
|
3
6
|
from django.utils.html import format_html, format_html_join
|
|
4
7
|
import django_tables2 as tables
|
|
5
|
-
from django_tables2.data import TableData
|
|
6
|
-
from django_tables2.rows import BoundRows
|
|
7
8
|
from django_tables2.utils import Accessor
|
|
8
9
|
from jsonschema.exceptions import ValidationError as JSONSchemaValidationError
|
|
9
10
|
|
|
10
|
-
from nautobot.core.models.querysets import count_related
|
|
11
11
|
from nautobot.core.tables import (
|
|
12
12
|
ApprovalButtonsColumn,
|
|
13
13
|
BaseTable,
|
|
@@ -24,7 +24,7 @@ from nautobot.core.tables import (
|
|
|
24
24
|
from nautobot.core.templatetags.helpers import HTML_NONE, render_boolean, render_json, render_markdown
|
|
25
25
|
from nautobot.tenancy.tables import TenantColumn
|
|
26
26
|
|
|
27
|
-
from .choices import
|
|
27
|
+
from .choices import JobResultStatusChoices, MetadataTypeDataTypeChoices
|
|
28
28
|
from .models import (
|
|
29
29
|
ApprovalWorkflow,
|
|
30
30
|
ApprovalWorkflowDefinition,
|
|
@@ -73,6 +73,8 @@ from .models import (
|
|
|
73
73
|
)
|
|
74
74
|
from .registry import registry
|
|
75
75
|
|
|
76
|
+
logger = logging.getLogger(__name__)
|
|
77
|
+
|
|
76
78
|
APPROVAL_WORKFLOW_OBJECT = """
|
|
77
79
|
{% if record.object_under_review and record.object_under_review.get_absolute_url %}
|
|
78
80
|
<a href="{{ record.object_under_review.get_absolute_url }}">{{ record.object_under_review }}</a>
|
|
@@ -129,7 +131,7 @@ GITREPOSITORY_BUTTONS = """
|
|
|
129
131
|
<button
|
|
130
132
|
data-url="{% url 'extras:gitrepository_sync' pk=record.pk %}"
|
|
131
133
|
type="submit"
|
|
132
|
-
class="dropdown-item sync-repository{% if perms.extras.change_gitrepository %} text-primary"{% else %}" disabled
|
|
134
|
+
class="dropdown-item sync-repository{% if perms.extras.change_gitrepository %} text-primary"{% else %}" disabled{% endif %}
|
|
133
135
|
>
|
|
134
136
|
<span class="mdi mdi-source-branch-sync" aria-hidden="true"></span>
|
|
135
137
|
Sync
|
|
@@ -168,7 +170,7 @@ JOB_RESULT_BUTTONS = """
|
|
|
168
170
|
</li>
|
|
169
171
|
{% else %}
|
|
170
172
|
<li>
|
|
171
|
-
<a
|
|
173
|
+
<a class="dropdown-item disabled" aria-disabled="true">
|
|
172
174
|
<span class="mdi mdi-repeat-off" aria-hidden="true"></span>
|
|
173
175
|
Job is not available, cannot be re-run
|
|
174
176
|
</a>
|
|
@@ -220,7 +222,7 @@ SCHEDULED_JOB_APPROVAL_QUEUE_BUTTONS = """
|
|
|
220
222
|
<button
|
|
221
223
|
type="button"
|
|
222
224
|
onClick="handleDetailPostAction('{% url 'extras:scheduledjob_approval_request_view' pk=record.pk %}', '_dry_run')"
|
|
223
|
-
class="dropdown-item{% if perms.extras.run_job and record.job_model.supports_dryrun %} text-primary"{% else %}" disabled
|
|
225
|
+
class="dropdown-item{% if perms.extras.run_job and record.job_model.supports_dryrun %} text-primary"{% else %}" disabled{% endif %}
|
|
224
226
|
>
|
|
225
227
|
<span class="mdi mdi-play" aria-hidden="true"></span>
|
|
226
228
|
Dry Run
|
|
@@ -230,7 +232,7 @@ SCHEDULED_JOB_APPROVAL_QUEUE_BUTTONS = """
|
|
|
230
232
|
<button
|
|
231
233
|
type="button"
|
|
232
234
|
onClick="handleDetailPostAction('{% url 'extras:scheduledjob_approval_request_view' pk=record.pk %}', '_approve')"
|
|
233
|
-
class="dropdown-item{% if perms.extras.run_job %} text-success"{% else %}" disabled
|
|
235
|
+
class="dropdown-item{% if perms.extras.run_job %} text-success"{% else %}" disabled{% endif %}
|
|
234
236
|
>
|
|
235
237
|
<span class="mdi mdi-check" aria-hidden="true"></span>
|
|
236
238
|
Approve
|
|
@@ -240,7 +242,7 @@ SCHEDULED_JOB_APPROVAL_QUEUE_BUTTONS = """
|
|
|
240
242
|
<button
|
|
241
243
|
type="button"
|
|
242
244
|
onClick="handleDetailPostAction('{% url 'extras:scheduledjob_approval_request_view' pk=record.pk %}', '_deny')"
|
|
243
|
-
class="dropdown-item{% if perms.extras.run_job %} text-danger"{% else %}" disabled
|
|
245
|
+
class="dropdown-item{% if perms.extras.run_job %} text-danger"{% else %}" disabled{% endif %}
|
|
244
246
|
>
|
|
245
247
|
<span class="mdi mdi-close" aria-hidden="true"></span>
|
|
246
248
|
Deny
|
|
@@ -294,7 +296,7 @@ class ApprovalWorkflowStageDefinitionTable(BaseTable):
|
|
|
294
296
|
fields = (
|
|
295
297
|
"pk",
|
|
296
298
|
"approval_workflow_definition",
|
|
297
|
-
"
|
|
299
|
+
"sequence",
|
|
298
300
|
"name",
|
|
299
301
|
"min_approvers",
|
|
300
302
|
"denial_message",
|
|
@@ -303,7 +305,7 @@ class ApprovalWorkflowStageDefinitionTable(BaseTable):
|
|
|
303
305
|
default_columns = (
|
|
304
306
|
"pk",
|
|
305
307
|
"approval_workflow_definition",
|
|
306
|
-
"
|
|
308
|
+
"sequence",
|
|
307
309
|
"name",
|
|
308
310
|
"min_approvers",
|
|
309
311
|
"denial_message",
|
|
@@ -1065,8 +1067,8 @@ def log_object_link(value, record):
|
|
|
1065
1067
|
|
|
1066
1068
|
def log_entry_color_css(record):
|
|
1067
1069
|
if record.log_level.lower() in ("failure", "error", "critical"):
|
|
1068
|
-
return "danger"
|
|
1069
|
-
return record.log_level.lower()
|
|
1070
|
+
return "table-danger"
|
|
1071
|
+
return "table-" + record.log_level.lower()
|
|
1070
1072
|
|
|
1071
1073
|
|
|
1072
1074
|
class JobTable(BaseTable):
|
|
@@ -1270,62 +1272,20 @@ class JobResultTable(BaseTable):
|
|
|
1270
1272
|
duration = tables.Column(orderable=False)
|
|
1271
1273
|
actions = ButtonsColumn(JobResult, buttons=("delete",), prepend_template=JOB_RESULT_BUTTONS)
|
|
1272
1274
|
|
|
1273
|
-
def __init__(self, *args, **kwargs):
|
|
1274
|
-
super().__init__(*args, **kwargs)
|
|
1275
|
-
# Only calculate log counts for "summary" column if it's actually visible.
|
|
1276
|
-
if "summary" in self.columns and self.columns["summary"].visible and isinstance(self.data.data, QuerySet):
|
|
1277
|
-
self.data = TableData.from_data(
|
|
1278
|
-
self.data.data.annotate(
|
|
1279
|
-
debug_log_count=count_related(
|
|
1280
|
-
JobLogEntry, "job_result", filter_dict={"log_level": LogLevelChoices.LOG_DEBUG}
|
|
1281
|
-
),
|
|
1282
|
-
success_log_count=count_related(
|
|
1283
|
-
JobLogEntry, "job_result", filter_dict={"log_level": LogLevelChoices.LOG_SUCCESS}
|
|
1284
|
-
),
|
|
1285
|
-
info_log_count=count_related(
|
|
1286
|
-
JobLogEntry, "job_result", filter_dict={"log_level": LogLevelChoices.LOG_INFO}
|
|
1287
|
-
),
|
|
1288
|
-
warning_log_count=count_related(
|
|
1289
|
-
JobLogEntry, "job_result", filter_dict={"log_level": LogLevelChoices.LOG_WARNING}
|
|
1290
|
-
),
|
|
1291
|
-
error_log_count=count_related(
|
|
1292
|
-
JobLogEntry,
|
|
1293
|
-
"job_result",
|
|
1294
|
-
filter_dict={
|
|
1295
|
-
"log_level__in": [
|
|
1296
|
-
LogLevelChoices.LOG_FAILURE,
|
|
1297
|
-
LogLevelChoices.LOG_ERROR,
|
|
1298
|
-
LogLevelChoices.LOG_CRITICAL,
|
|
1299
|
-
],
|
|
1300
|
-
},
|
|
1301
|
-
),
|
|
1302
|
-
)
|
|
1303
|
-
)
|
|
1304
|
-
self.data.set_table(self)
|
|
1305
|
-
self.rows = BoundRows(data=self.data, table=self, pinned_data=self.pinned_data)
|
|
1306
|
-
|
|
1307
1275
|
def render_summary(self, record):
|
|
1308
1276
|
"""
|
|
1309
1277
|
Define custom rendering for the summary column.
|
|
1310
1278
|
"""
|
|
1311
|
-
#
|
|
1312
|
-
#
|
|
1313
|
-
if not
|
|
1314
|
-
record.debug_log_count
|
|
1315
|
-
|
|
1316
|
-
record.
|
|
1317
|
-
|
|
1318
|
-
record.
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
if not hasattr(record, "error_log_count"):
|
|
1322
|
-
record.error_log_count = record.job_log_entries.filter(
|
|
1323
|
-
log_level__in=[
|
|
1324
|
-
LogLevelChoices.LOG_FAILURE,
|
|
1325
|
-
LogLevelChoices.LOG_ERROR,
|
|
1326
|
-
LogLevelChoices.LOG_CRITICAL,
|
|
1327
|
-
]
|
|
1328
|
-
).count()
|
|
1279
|
+
# The *_log_count attributes will be calculated and updated at the end of a Job run when JobResult is saved.
|
|
1280
|
+
# If the values are not present due to a running Job or are missing in any field, skip display.
|
|
1281
|
+
if record.status not in JobResultStatusChoices.READY_STATES or None in [
|
|
1282
|
+
record.debug_log_count,
|
|
1283
|
+
record.success_log_count,
|
|
1284
|
+
record.info_log_count,
|
|
1285
|
+
record.warning_log_count,
|
|
1286
|
+
record.error_log_count,
|
|
1287
|
+
]:
|
|
1288
|
+
return ""
|
|
1329
1289
|
|
|
1330
1290
|
return format_html(
|
|
1331
1291
|
"""<label class="badge bg-secondary">{}</label>
|
|
@@ -1364,6 +1324,7 @@ class JobResultTable(BaseTable):
|
|
|
1364
1324
|
"job_model",
|
|
1365
1325
|
"user",
|
|
1366
1326
|
"status",
|
|
1327
|
+
"summary",
|
|
1367
1328
|
"actions",
|
|
1368
1329
|
)
|
|
1369
1330
|
|
|
@@ -1588,8 +1549,31 @@ class ObjectChangeTable(BaseTable):
|
|
|
1588
1549
|
|
|
1589
1550
|
def __init__(self, *args, **kwargs):
|
|
1590
1551
|
super().__init__(*args, **kwargs)
|
|
1591
|
-
#
|
|
1592
|
-
|
|
1552
|
+
# Only prefetch if all content types are valid
|
|
1553
|
+
if all(ct.model_class() is not None for ct in ContentType.objects.all()):
|
|
1554
|
+
self.add_conditional_prefetch("object_repr", "changed_object")
|
|
1555
|
+
else:
|
|
1556
|
+
error_message = dedent("""\
|
|
1557
|
+
One or more ContentType entries in the database are invalid.
|
|
1558
|
+
This will likely cause performance degradation when viewing the Object Change log.
|
|
1559
|
+
An administrator can follow these steps to resolve common issues:
|
|
1560
|
+
- Run `nautobot-server remove_stale_contenttypes`
|
|
1561
|
+
- Run `nautobot-server migrate <app_label> zero` for any app labels which no longer exist
|
|
1562
|
+
- Manually dropping tables for any models which have been removed from Nautobot or its plugins from your database
|
|
1563
|
+
- Run ```
|
|
1564
|
+
from django.contrib.contenttypes.models import ContentType
|
|
1565
|
+
qs = ContentType.objects.filter(
|
|
1566
|
+
app_label__in=[
|
|
1567
|
+
"<app_label_of_removed_plugin_1>",
|
|
1568
|
+
"<app_label_of_removed_plugin_2>",
|
|
1569
|
+
]
|
|
1570
|
+
) | ContentType.objects.filter(model__icontains="<name_of_removed_model_1>")
|
|
1571
|
+
# Review the queryset before running delete
|
|
1572
|
+
qs.delete()
|
|
1573
|
+
```
|
|
1574
|
+
Please ensure you fully understand the implications of these actions before proceeding.
|
|
1575
|
+
""")
|
|
1576
|
+
logger.warning(error_message)
|
|
1593
1577
|
|
|
1594
1578
|
|
|
1595
1579
|
#
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
{% load helpers %}
|
|
3
3
|
|
|
4
4
|
{% block buttons %}
|
|
5
|
-
<div class="btn-group">
|
|
6
|
-
<a class="btn {% if approval_view %}
|
|
7
|
-
|
|
5
|
+
<div class="btn-group" role="group">
|
|
6
|
+
<a class="btn btn-primary{% if approval_view %} bg-primary nb-text-body-bg{% endif %}"
|
|
7
|
+
href="{% url 'extras:approver_dashboard' %}">
|
|
8
|
+
<span class="mdi mdi-checkbox-multiple-outline" aria-hidden="true"></span> My Approvals
|
|
8
9
|
</a>
|
|
9
|
-
<a class="btn {% if not approval_view %}
|
|
10
|
-
|
|
10
|
+
<a class="btn btn-primary{% if not approval_view %} bg-primary nb-text-body-bg{% endif %}"
|
|
11
|
+
href="{% url 'extras:approvee_dashboard' %}">
|
|
12
|
+
<span class="mdi mdi-help-box-multiple-outline" aria-hidden="true"></span> My Requests
|
|
11
13
|
</a>
|
|
12
14
|
</div>
|
|
13
15
|
{% endblock %}
|