nautobot 2.4.17__py3-none-any.whl → 2.4.19__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/ui.py +6 -0
- nautobot/apps/views.py +2 -0
- nautobot/circuits/tables.py +1 -1
- nautobot/circuits/templates/circuits/circuit_create.html +7 -7
- nautobot/circuits/templates/circuits/circuit_retrieve.html +1 -5
- nautobot/circuits/templates/circuits/circuittermination_create.html +26 -26
- nautobot/circuits/templates/circuits/circuittermination_retrieve.html +1 -8
- nautobot/circuits/templates/circuits/inc/circuit_termination.html +20 -20
- nautobot/circuits/templates/circuits/inc/circuit_termination_header_extra_content.html +3 -3
- nautobot/circuits/templates/circuits/inc/circuit_termination_speed_fragment.html +9 -0
- nautobot/circuits/templates/circuits/providernetwork_retrieve.html +1 -3
- nautobot/circuits/tests/integration/test_circuit.py +2 -2
- nautobot/circuits/views.py +49 -15
- nautobot/cloud/templates/cloud/cloudaccount_retrieve.html +1 -4
- nautobot/cloud/templates/cloud/cloudnetwork_retrieve.html +1 -7
- nautobot/cloud/templates/cloud/cloudresourcetype_retrieve.html +1 -3
- nautobot/cloud/templates/cloud/cloudservice_retrieve.html +1 -5
- nautobot/cloud/views.py +45 -0
- nautobot/core/filters.py +2 -2
- nautobot/core/graphql/generators.py +5 -2
- nautobot/core/jobs/bulk_actions.py +48 -85
- nautobot/core/models/querysets.py +2 -1
- nautobot/core/settings.py +1 -0
- nautobot/core/settings.yaml +9 -0
- nautobot/core/tables.py +21 -23
- nautobot/core/templates/40x.html +15 -15
- nautobot/core/templates/500.html +21 -21
- nautobot/core/templates/admin/app_index.html +8 -8
- nautobot/core/templates/admin/base.html +104 -104
- nautobot/core/templates/admin/change_form.html +65 -65
- nautobot/core/templates/admin/change_list.html +60 -60
- nautobot/core/templates/admin/change_list_results.html +39 -39
- nautobot/core/templates/admin/config/config.html +47 -47
- nautobot/core/templates/admin/delete_confirmation.html +47 -47
- nautobot/core/templates/admin/edit_inline/stacked.html +124 -124
- nautobot/core/templates/admin/edit_inline/tabular.html +60 -60
- nautobot/core/templates/admin/includes/fieldset.html +4 -4
- nautobot/core/templates/admin/index.html +60 -60
- nautobot/core/templates/admin/prepopulated_fields_js.html +18 -18
- nautobot/core/templates/admin/submit_line.html +4 -4
- nautobot/core/templates/base_django.html +46 -46
- nautobot/core/templates/buttons/consolidated_bulk_action_buttons.html +8 -8
- nautobot/core/templates/buttons/consolidated_detail_view_action_buttons.html +8 -8
- nautobot/core/templates/buttons/export.html +3 -3
- nautobot/core/templates/components/breadcrumbs.html +19 -0
- nautobot/core/templates/components/button/default.html +3 -3
- nautobot/core/templates/components/button/dropdown.html +7 -7
- nautobot/core/templates/components/button/formbutton.html +4 -4
- nautobot/core/templates/components/panel/body_content_data_table.html +1 -1
- nautobot/core/templates/components/panel/body_wrapper_generic_table.html +3 -0
- nautobot/core/templates/components/panel/footer_content_table.html +3 -1
- nautobot/core/templates/components/panel/header_extra_content_table.html +10 -1
- nautobot/core/templates/components/tab/content_wrapper.html +1 -1
- nautobot/core/templates/components/tab/label_wrapper.html +1 -1
- nautobot/core/templates/components/tab/label_wrapper_distinct_view.html +10 -3
- nautobot/core/templates/generic/object_bulk_add_component.html +40 -40
- nautobot/core/templates/generic/object_bulk_create.html +3 -3
- nautobot/core/templates/generic/object_bulk_destroy.html +6 -6
- nautobot/core/templates/generic/object_bulk_update.html +52 -52
- nautobot/core/templates/generic/object_changelog.html +0 -2
- nautobot/core/templates/generic/object_import.html +33 -33
- nautobot/core/templates/generic/object_list.html +271 -268
- nautobot/core/templates/generic/object_notes.html +0 -2
- nautobot/core/templates/generic/object_retrieve.html +264 -257
- nautobot/core/templates/graphene/graphiql.html +127 -127
- nautobot/core/templates/home.html +62 -62
- nautobot/core/templates/inc/computed_fields/panel_data.html +13 -13
- nautobot/core/templates/inc/created_updated.html +8 -8
- nautobot/core/templates/inc/custom_fields/panel_data.html +13 -13
- nautobot/core/templates/inc/dynamic_groups_panel.html +11 -11
- nautobot/core/templates/inc/footer.html +19 -19
- nautobot/core/templates/inc/javascript.html +1 -1
- nautobot/core/templates/inc/media.html +46 -46
- nautobot/core/templates/inc/nav_menu.html +1 -1
- nautobot/core/templates/inc/relationships_table_rows.html +22 -22
- nautobot/core/templates/inc/tenant_table_row.html +1 -1
- nautobot/core/templates/login.html +77 -77
- nautobot/core/templates/media_failure.html +38 -38
- nautobot/core/templates/panel_table.html +1 -1
- nautobot/core/templates/rest_framework/api.html +3 -3
- nautobot/core/templates/search.html +1 -1
- nautobot/core/templates/swagger_ui.html +9 -9
- nautobot/core/templates/utilities/confirmation_form.html +18 -18
- nautobot/core/templates/utilities/render_field.html +1 -1
- nautobot/core/templates/utilities/render_jinja2.html +43 -43
- nautobot/core/templates/utilities/templatetags/filter_form_modal.html +56 -56
- nautobot/core/templates/utilities/templatetags/utilization_graph.html +1 -1
- nautobot/core/templates/utilities/theme_preview.html +799 -799
- nautobot/core/templates/utilities/worker_status.html +122 -122
- nautobot/core/templates/widgets/clearable_file.html +3 -3
- nautobot/core/templates/widgets/sluginput.html +1 -1
- nautobot/core/templatetags/buttons.py +8 -2
- nautobot/core/templatetags/helpers.py +24 -0
- nautobot/core/templatetags/ui_framework.py +40 -5
- nautobot/core/testing/filters.py +37 -21
- nautobot/core/testing/integration.py +7 -4
- nautobot/core/testing/views.py +49 -5
- nautobot/core/tests/test_breadcrumbs.py +78 -4
- nautobot/core/tests/test_commands.py +7 -4
- nautobot/core/tests/test_graphql.py +20 -5
- nautobot/core/tests/test_jobs.py +34 -21
- nautobot/core/tests/test_tables.py +43 -6
- nautobot/core/tests/test_templatetags_ui_framework.py +146 -0
- nautobot/core/tests/test_titles.py +2 -2
- nautobot/core/tests/test_ui.py +188 -1
- nautobot/core/tests/test_utils.py +35 -0
- nautobot/core/tests/test_views.py +45 -0
- nautobot/core/tests/test_views_generic.py +43 -0
- nautobot/core/tests/test_views_utils.py +239 -5
- nautobot/core/ui/breadcrumbs.py +220 -28
- nautobot/core/ui/bulk_buttons.py +8 -0
- nautobot/core/ui/object_detail.py +181 -60
- nautobot/core/ui/titles.py +10 -5
- nautobot/core/utils/requests.py +27 -2
- nautobot/core/views/__init__.py +24 -3
- nautobot/core/views/generic.py +70 -35
- nautobot/core/views/mixins.py +226 -122
- nautobot/core/views/utils.py +270 -1
- nautobot/dcim/api/serializers.py +8 -2
- nautobot/dcim/constants.py +1 -0
- nautobot/dcim/factory.py +4 -3
- nautobot/dcim/filters/mixins.py +1 -2
- nautobot/dcim/forms.py +5 -1
- nautobot/dcim/migrations/0074_alter_rack_u_height.py +21 -0
- nautobot/dcim/models/devices.py +30 -1
- nautobot/dcim/models/racks.py +2 -2
- nautobot/dcim/tables/__init__.py +2 -0
- nautobot/dcim/tables/devices.py +24 -0
- nautobot/dcim/tables/power.py +2 -2
- nautobot/dcim/templates/dcim/cable.html +53 -53
- nautobot/dcim/templates/dcim/cable_connect.html +182 -182
- nautobot/dcim/templates/dcim/cable_trace.html +1 -1
- nautobot/dcim/templates/dcim/console_port_connection_list.html +5 -5
- nautobot/dcim/templates/dcim/consoleport.html +86 -86
- nautobot/dcim/templates/dcim/consoleserverport.html +86 -86
- nautobot/dcim/templates/dcim/controller_create.html +34 -34
- nautobot/dcim/templates/dcim/controllermanageddevicegroup_create.html +68 -68
- nautobot/dcim/templates/dcim/device/base.html +1 -114
- nautobot/dcim/templates/dcim/device/config.html +17 -17
- nautobot/dcim/templates/dcim/device/consoleports.html +1 -52
- nautobot/dcim/templates/dcim/device/consoleserverports.html +1 -52
- nautobot/dcim/templates/dcim/device/devicebays.html +1 -48
- nautobot/dcim/templates/dcim/device/frontports.html +1 -52
- nautobot/dcim/templates/dcim/device/interfaces.html +1 -56
- nautobot/dcim/templates/dcim/device/inventory.html +1 -48
- nautobot/dcim/templates/dcim/device/lldp_neighbors.html +64 -64
- nautobot/dcim/templates/dcim/device/modulebays.html +1 -48
- nautobot/dcim/templates/dcim/device/poweroutlets.html +1 -52
- nautobot/dcim/templates/dcim/device/powerports.html +1 -52
- nautobot/dcim/templates/dcim/device/rearports.html +1 -52
- nautobot/dcim/templates/dcim/device/status.html +66 -66
- nautobot/dcim/templates/dcim/device/wireless.html +1 -72
- nautobot/dcim/templates/dcim/device.html +4 -422
- nautobot/dcim/templates/dcim/device_component.html +0 -19
- nautobot/dcim/templates/dcim/device_component_add.html +25 -25
- nautobot/dcim/templates/dcim/device_create.html +229 -0
- nautobot/dcim/templates/dcim/device_edit.html +2 -227
- nautobot/dcim/templates/dcim/devicebay.html +41 -41
- nautobot/dcim/templates/dcim/devicebay_populate.html +32 -32
- nautobot/dcim/templates/dcim/devicetype_component_add.html +28 -28
- nautobot/dcim/templates/dcim/devicetype_retrieve.html +1 -3
- nautobot/dcim/templates/dcim/frontport.html +84 -84
- nautobot/dcim/templates/dcim/inc/cable_toggle_buttons.html +1 -1
- nautobot/dcim/templates/dcim/inc/device_interface_filter.html +8 -0
- nautobot/dcim/templates/dcim/inc/device_napalm_tabs.html +1 -15
- nautobot/dcim/templates/dcim/inc/location_hierarchy.html +22 -22
- nautobot/dcim/templates/dcim/interface.html +206 -206
- nautobot/dcim/templates/dcim/interface_connection_list.html +5 -5
- nautobot/dcim/templates/dcim/interfaceredundancygroupassociation_create.html +6 -6
- nautobot/dcim/templates/dcim/inventoryitem.html +44 -44
- nautobot/dcim/templates/dcim/inventoryitem_add.html +32 -32
- nautobot/dcim/templates/dcim/inventoryitem_edit.html +22 -22
- nautobot/dcim/templates/dcim/location_migrate_data_to_contact.html +46 -46
- nautobot/dcim/templates/dcim/location_retrieve.html +1 -7
- nautobot/dcim/templates/dcim/locationtype.html +1 -6
- nautobot/dcim/templates/dcim/locationtype_retrieve.html +1 -7
- nautobot/dcim/templates/dcim/module/base.html +85 -85
- nautobot/dcim/templates/dcim/module_interfaces.html +1 -1
- nautobot/dcim/templates/dcim/module_modulebays.html +1 -1
- nautobot/dcim/templates/dcim/module_retrieve.html +52 -52
- nautobot/dcim/templates/dcim/module_update.html +61 -61
- nautobot/dcim/templates/dcim/modulebay_destroy.html +1 -1
- nautobot/dcim/templates/dcim/modulebay_retrieve.html +83 -99
- nautobot/dcim/templates/dcim/modulebay_update.html +33 -33
- nautobot/dcim/templates/dcim/modulefamily_retrieve.html +1 -1
- nautobot/dcim/templates/dcim/moduletype_retrieve.html +140 -144
- nautobot/dcim/templates/dcim/platform_create.html +38 -38
- nautobot/dcim/templates/dcim/power_port_connection_list.html +5 -5
- nautobot/dcim/templates/dcim/powerfeed_retrieve.html +1 -8
- nautobot/dcim/templates/dcim/poweroutlet.html +85 -85
- nautobot/dcim/templates/dcim/powerpanel_retrieve.html +1 -8
- nautobot/dcim/templates/dcim/powerport.html +91 -91
- nautobot/dcim/templates/dcim/rack_elevation_list.html +18 -18
- nautobot/dcim/templates/dcim/rack_retrieve.html +264 -274
- nautobot/dcim/templates/dcim/rackreservation_retrieve.html +0 -3
- nautobot/dcim/templates/dcim/rearport.html +78 -78
- nautobot/dcim/templates/dcim/virtualchassis_retrieve.html +1 -50
- nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +1 -5
- nautobot/dcim/tests/integration/test_device_bulk_operations.py +3 -2
- nautobot/dcim/tests/integration/test_location_bulk_operations.py +6 -2
- nautobot/dcim/tests/test_api.py +33 -1
- nautobot/dcim/tests/test_views.py +189 -4
- nautobot/dcim/ui.py +29 -0
- nautobot/dcim/urls.py +1 -109
- nautobot/dcim/utils.py +30 -0
- nautobot/dcim/views.py +1149 -550
- nautobot/extras/filters/mixins.py +1 -1
- nautobot/extras/forms/forms.py +15 -0
- nautobot/extras/models/groups.py +10 -1
- nautobot/extras/models/jobs.py +2 -2
- nautobot/extras/plugins/views.py +18 -5
- nautobot/extras/tables.py +24 -2
- nautobot/extras/templates/extras/computedfield_edit.html +4 -4
- nautobot/extras/templates/extras/configcontext_update.html +1 -1
- nautobot/extras/templates/extras/configcontextschema_retrieve.html +32 -32
- nautobot/extras/templates/extras/customfield_retrieve.html +1 -128
- nautobot/extras/templates/extras/customfield_update.html +23 -23
- nautobot/extras/templates/extras/dynamicgroup.html +2 -99
- nautobot/extras/templates/extras/dynamicgroup_edit.html +2 -199
- nautobot/extras/templates/extras/dynamicgroup_retrieve.html +99 -0
- nautobot/extras/templates/extras/dynamicgroup_update.html +199 -0
- nautobot/extras/templates/extras/gitrepository.html +2 -82
- nautobot/extras/templates/extras/gitrepository_list.html +10 -10
- nautobot/extras/templates/extras/gitrepository_object_edit.html +2 -13
- nautobot/extras/templates/extras/gitrepository_retrieve.html +82 -0
- nautobot/extras/templates/extras/gitrepository_update.html +13 -0
- nautobot/extras/templates/extras/graphqlquery_retrieve.html +73 -73
- nautobot/extras/templates/extras/inc/configcontext_format.html +2 -2
- nautobot/extras/templates/extras/inc/job_table.html +10 -10
- nautobot/extras/templates/extras/inc/jobresult.html +21 -21
- nautobot/extras/templates/extras/inc/jobresult_js.html +6 -6
- nautobot/extras/templates/extras/inc/tags_panel.html +10 -10
- nautobot/extras/templates/extras/job.html +64 -64
- nautobot/extras/templates/extras/job_approval_request.html +9 -9
- nautobot/extras/templates/extras/job_bulk_edit.html +13 -13
- nautobot/extras/templates/extras/job_edit.html +45 -45
- nautobot/extras/templates/extras/job_list.html +4 -4
- nautobot/extras/templates/extras/jobresult_retrieve.html +0 -25
- nautobot/extras/templates/extras/marketplace.html +101 -101
- nautobot/extras/templates/extras/metadatatype_create.html +20 -20
- nautobot/extras/templates/extras/note_retrieve.html +0 -52
- nautobot/extras/templates/extras/object_assign_contact_or_team.html +18 -18
- nautobot/extras/templates/extras/object_configcontext.html +1 -3
- nautobot/extras/templates/extras/objectchange.html +2 -165
- nautobot/extras/templates/extras/objectchange_retrieve.html +165 -0
- nautobot/extras/templates/extras/plugin_detail.html +44 -48
- nautobot/extras/templates/extras/plugins_list.html +9 -11
- nautobot/extras/templates/extras/plugins_tiles.html +26 -26
- nautobot/extras/templates/extras/relationship_edit.html +4 -4
- nautobot/extras/templates/extras/role_retrieve.html +13 -13
- nautobot/extras/templates/extras/scheduled_jobs_approval_queue_list.html +21 -21
- nautobot/extras/templates/extras/scheduledjob.html +128 -128
- nautobot/extras/templates/extras/secret_create.html +53 -53
- nautobot/extras/templates/extras/secretsgroup_update.html +13 -13
- nautobot/extras/templates/extras/templatetags/plugin_object_detail_tabs.html +3 -3
- nautobot/extras/templates/extras/webhook.html +79 -79
- nautobot/extras/tests/integration/test_relationships.py +6 -6
- nautobot/extras/tests/test_dynamicgroups.py +73 -18
- nautobot/extras/tests/test_filters.py +1 -1
- nautobot/extras/tests/test_jobs.py +2 -0
- nautobot/extras/tests/test_views.py +8 -3
- nautobot/extras/urls.py +3 -97
- nautobot/extras/views.py +524 -456
- nautobot/ipam/filters.py +2 -2
- nautobot/ipam/migrations/0053_alter_vrfdeviceassignment_options_and_more.py +20 -0
- nautobot/ipam/models.py +34 -0
- nautobot/ipam/querysets.py +3 -3
- nautobot/ipam/signals.py +6 -1
- nautobot/ipam/tables.py +3 -1
- nautobot/ipam/templates/ipam/inc/prefix_header_extra_content_table.html +4 -0
- nautobot/ipam/templates/ipam/inc/toggle_available.html +8 -8
- nautobot/ipam/templates/ipam/inc/vlangroup_header.html +4 -4
- nautobot/ipam/templates/ipam/ipaddress.html +119 -123
- nautobot/ipam/templates/ipam/ipaddress_assign.html +10 -10
- nautobot/ipam/templates/ipam/ipaddress_edit.html +1 -1
- nautobot/ipam/templates/ipam/ipaddress_merge.html +180 -180
- nautobot/ipam/templates/ipam/ipaddresstointerface_retrieve.html +48 -48
- nautobot/ipam/templates/ipam/prefix.html +2 -115
- nautobot/ipam/templates/ipam/prefix_create.html +34 -0
- nautobot/ipam/templates/ipam/prefix_edit.html +1 -34
- nautobot/ipam/templates/ipam/prefix_retrieve.html +3 -0
- nautobot/ipam/templates/ipam/service_retrieve.html +1 -6
- nautobot/ipam/templates/ipam/vlan_retrieve.html +1 -7
- nautobot/ipam/templates/ipam/vrf_edit.html +1 -1
- nautobot/ipam/tests/test_api.py +5 -0
- nautobot/ipam/tests/test_models.py +387 -0
- nautobot/ipam/tests/test_querysets.py +46 -0
- nautobot/ipam/tests/test_views.py +34 -0
- nautobot/ipam/ui.py +145 -0
- nautobot/ipam/urls.py +1 -46
- nautobot/ipam/utils/__init__.py +26 -0
- nautobot/ipam/utils/migrations.py +1 -1
- nautobot/ipam/views.py +234 -112
- nautobot/project-static/docs/404.html +11 -11
- nautobot/project-static/docs/apps/index.html +11 -11
- nautobot/project-static/docs/apps/nautobot-apps.html +11 -11
- nautobot/project-static/docs/assets/javascripts/{bundle.92b07e13.min.js → bundle.f55a23d4.min.js} +2 -2
- nautobot/project-static/docs/assets/javascripts/{bundle.92b07e13.min.js.map → bundle.f55a23d4.min.js.map} +2 -2
- nautobot/project-static/docs/assets/stylesheets/{main.7e37652d.min.css → main.e53b48f4.min.css} +1 -1
- nautobot/project-static/docs/assets/stylesheets/{main.7e37652d.min.css.map → main.e53b48f4.min.css.map} +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +11 -11
- nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +11 -11
- nautobot/project-static/docs/code-reference/nautobot/apps/api.html +11 -11
- nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +11 -11
- nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +11 -11
- nautobot/project-static/docs/code-reference/nautobot/apps/config.html +11 -11
- nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +11 -11
- nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +11 -11
- nautobot/project-static/docs/code-reference/nautobot/apps/events.html +11 -11
- nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +11 -11
- nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +11 -11
- nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +11 -11
- nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +11 -11
- nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +11 -11
- nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +11 -11
- nautobot/project-static/docs/code-reference/nautobot/apps/models.html +11 -11
- nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +11 -11
- nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +11 -11
- nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +11 -11
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +83 -11
- nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +1265 -281
- nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +11 -11
- nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +12 -12
- nautobot/project-static/docs/code-reference/nautobot/apps/views.html +452 -29
- nautobot/project-static/docs/development/apps/api/configuration-view.html +11 -11
- nautobot/project-static/docs/development/apps/api/database-backend-config.html +11 -11
- nautobot/project-static/docs/development/apps/api/models/django-admin.html +11 -11
- nautobot/project-static/docs/development/apps/api/models/global-search.html +11 -11
- nautobot/project-static/docs/development/apps/api/models/graphql.html +11 -11
- nautobot/project-static/docs/development/apps/api/models/index.html +11 -11
- nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +12 -12
- nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +11 -11
- nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +11 -11
- nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +11 -11
- nautobot/project-static/docs/development/apps/api/platform-features/index.html +11 -11
- nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +11 -11
- nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +11 -11
- nautobot/project-static/docs/development/apps/api/platform-features/prepopulating-data.html +11 -11
- nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +11 -11
- nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +11 -11
- nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +11 -11
- nautobot/project-static/docs/development/apps/api/prometheus.html +11 -11
- nautobot/project-static/docs/development/apps/api/setup.html +11 -11
- nautobot/project-static/docs/development/apps/api/testing.html +11 -11
- nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +11 -11
- nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +11 -11
- nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +11 -11
- nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +11 -11
- nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +11 -11
- nautobot/project-static/docs/development/apps/api/views/base-template.html +11 -11
- nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +11 -11
- nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +11 -11
- nautobot/project-static/docs/development/apps/api/views/help-documentation.html +11 -11
- nautobot/project-static/docs/development/apps/api/views/index.html +11 -11
- nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +11 -11
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +11 -11
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +11 -11
- nautobot/project-static/docs/development/apps/api/views/notes.html +11 -11
- nautobot/project-static/docs/development/apps/api/views/rest-api.html +11 -11
- nautobot/project-static/docs/development/apps/api/views/urls.html +11 -11
- nautobot/project-static/docs/development/apps/index.html +11 -11
- nautobot/project-static/docs/development/apps/migration/code-updates.html +11 -11
- nautobot/project-static/docs/development/apps/migration/dependency-updates.html +11 -11
- nautobot/project-static/docs/development/apps/migration/from-v1.html +11 -11
- nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +11 -11
- nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +11 -11
- nautobot/project-static/docs/development/apps/migration/model-updates/global.html +11 -11
- nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +11 -11
- nautobot/project-static/docs/development/apps/migration/ui-component-framework/best-practices.html +11 -11
- nautobot/project-static/docs/development/apps/migration/ui-component-framework/breadcrumbs-titles.html +11 -11
- nautobot/project-static/docs/development/apps/migration/ui-component-framework/custom-content.html +11 -11
- nautobot/project-static/docs/development/apps/migration/ui-component-framework/index.html +11 -11
- nautobot/project-static/docs/development/apps/migration/ui-component-framework/migration-steps.html +11 -11
- nautobot/project-static/docs/development/apps/porting-from-netbox.html +11 -11
- nautobot/project-static/docs/development/core/application-registry.html +11 -11
- nautobot/project-static/docs/development/core/best-practices.html +11 -11
- nautobot/project-static/docs/development/core/bootstrap-ui.html +11 -11
- nautobot/project-static/docs/development/core/caching.html +11 -11
- nautobot/project-static/docs/development/core/controllers.html +11 -11
- nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +11 -11
- nautobot/project-static/docs/development/core/generic-views.html +11 -11
- nautobot/project-static/docs/development/core/getting-started.html +50 -63
- nautobot/project-static/docs/development/core/homepage.html +11 -11
- nautobot/project-static/docs/development/core/index.html +11 -11
- nautobot/project-static/docs/development/core/minikube-dev-environment-for-k8s-jobs.html +11 -11
- nautobot/project-static/docs/development/core/model-checklist.html +11 -11
- nautobot/project-static/docs/development/core/model-features.html +11 -11
- nautobot/project-static/docs/development/core/natural-keys.html +11 -11
- nautobot/project-static/docs/development/core/navigation-menu.html +11 -11
- nautobot/project-static/docs/development/core/release-checklist.html +11 -11
- nautobot/project-static/docs/development/core/role-internals.html +11 -11
- nautobot/project-static/docs/development/core/settings.html +11 -11
- nautobot/project-static/docs/development/core/style-guide.html +15 -11
- nautobot/project-static/docs/development/core/templates.html +11 -11
- nautobot/project-static/docs/development/core/testing.html +11 -11
- nautobot/project-static/docs/development/core/ui-component-framework.html +17 -22
- nautobot/project-static/docs/development/core/user-preferences.html +11 -11
- nautobot/project-static/docs/development/index.html +11 -11
- nautobot/project-static/docs/development/jobs/getting-started.html +11 -11
- nautobot/project-static/docs/development/jobs/index.html +11 -11
- nautobot/project-static/docs/development/jobs/installation.html +11 -11
- nautobot/project-static/docs/development/jobs/job-extensions.html +11 -11
- nautobot/project-static/docs/development/jobs/job-logging.html +11 -11
- nautobot/project-static/docs/development/jobs/job-patterns.html +11 -11
- nautobot/project-static/docs/development/jobs/job-structure.html +11 -11
- nautobot/project-static/docs/development/jobs/migration/from-v1.html +11 -11
- nautobot/project-static/docs/development/jobs/testing.html +11 -11
- nautobot/project-static/docs/index.html +11 -11
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/overview/application_stack.html +11 -11
- nautobot/project-static/docs/overview/design_philosophy.html +11 -11
- nautobot/project-static/docs/release-notes/index.html +11 -11
- nautobot/project-static/docs/release-notes/version-1.0.html +11 -11
- nautobot/project-static/docs/release-notes/version-1.1.html +11 -11
- nautobot/project-static/docs/release-notes/version-1.2.html +11 -11
- nautobot/project-static/docs/release-notes/version-1.3.html +11 -11
- nautobot/project-static/docs/release-notes/version-1.4.html +11 -11
- nautobot/project-static/docs/release-notes/version-1.5.html +11 -11
- nautobot/project-static/docs/release-notes/version-1.6.html +11 -11
- nautobot/project-static/docs/release-notes/version-2.0.html +11 -11
- nautobot/project-static/docs/release-notes/version-2.1.html +11 -11
- nautobot/project-static/docs/release-notes/version-2.2.html +11 -11
- nautobot/project-static/docs/release-notes/version-2.3.html +11 -11
- nautobot/project-static/docs/release-notes/version-2.4.html +418 -11
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +300 -300
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +11 -11
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +11 -11
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +11 -11
- nautobot/project-static/docs/user-guide/administration/configuration/index.html +11 -11
- nautobot/project-static/docs/user-guide/administration/configuration/redis.html +11 -11
- nautobot/project-static/docs/user-guide/administration/configuration/settings.html +38 -11
- nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +11 -11
- nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +89 -14
- nautobot/project-static/docs/user-guide/administration/guides/docker.html +11 -11
- nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +11 -11
- nautobot/project-static/docs/user-guide/administration/guides/permissions.html +11 -11
- nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +11 -11
- nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +11 -11
- nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +11 -11
- nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +11 -11
- nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +11 -11
- nautobot/project-static/docs/user-guide/administration/installation/app-install.html +11 -11
- nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +11 -11
- nautobot/project-static/docs/user-guide/administration/installation/http-server.html +11 -11
- nautobot/project-static/docs/user-guide/administration/installation/index.html +11 -11
- nautobot/project-static/docs/user-guide/administration/installation/install_system.html +11 -11
- nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +11 -11
- nautobot/project-static/docs/user-guide/administration/installation/services.html +11 -11
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +11 -11
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +11 -11
- nautobot/project-static/docs/user-guide/administration/security/index.html +11 -11
- nautobot/project-static/docs/user-guide/administration/security/notices.html +11 -11
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +11 -11
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +11 -11
- nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +11 -11
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +11 -11
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +11 -11
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +11 -11
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +11 -11
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +11 -11
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +11 -11
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +11 -11
- nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulefamily.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualdevicecontext.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/wireless/index.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/wireless/radioprofile.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/wireless/supporteddatarate.html +11 -11
- nautobot/project-static/docs/user-guide/core-data-model/wireless/wirelessnetwork.html +11 -11
- nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +11 -11
- nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +11 -11
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +11 -11
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +11 -11
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +11 -11
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +11 -11
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +11 -11
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +11 -11
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +11 -11
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +11 -11
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +11 -11
- nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +11 -11
- nautobot/project-static/docs/user-guide/feature-guides/graphql.html +11 -11
- nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +11 -11
- nautobot/project-static/docs/user-guide/feature-guides/relationships.html +11 -11
- nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +11 -11
- nautobot/project-static/docs/user-guide/feature-guides/wireless-networks-and-controllers.html +11 -11
- nautobot/project-static/docs/user-guide/index.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/events.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/managing-jobs.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/note.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/rendering-jinja-templates.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/role.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/secret.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/status.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/tag.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +11 -11
- nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +11 -11
- nautobot/project-static/img/nautobot_icon.svg +32 -34
- nautobot/project-static/js/table_sorting_indicator.js +0 -2
- nautobot/tenancy/templates/tenancy/tenant.html +1 -7
- nautobot/tenancy/views.py +13 -0
- nautobot/users/templates/users/api_tokens.html +4 -4
- nautobot/users/templates/users/base.html +28 -28
- nautobot/virtualization/templates/virtualization/cluster.html +64 -64
- nautobot/virtualization/templates/virtualization/inc/virtualmachine_vminterface_filter.html +8 -0
- nautobot/virtualization/templates/virtualization/virtualmachine_component_add.html +25 -25
- nautobot/virtualization/templates/virtualization/virtualmachine_retrieve.html +1 -251
- nautobot/virtualization/templates/virtualization/vminterface.html +70 -70
- nautobot/virtualization/urls.py +0 -12
- nautobot/virtualization/views.py +158 -54
- nautobot/wireless/templates/wireless/wirelessnetwork_create.html +13 -13
- nautobot/wireless/tests/integration/test_radio_profile.py +1 -1
- {nautobot-2.4.17.dist-info → nautobot-2.4.19.dist-info}/METADATA +4 -4
- {nautobot-2.4.17.dist-info → nautobot-2.4.19.dist-info}/RECORD +623 -607
- nautobot/core/templates/inc/breadcrumbs.html +0 -14
- nautobot/ipam/templates/ipam/prefix_ipaddresses.html +0 -11
- nautobot/ipam/templates/ipam/prefix_prefixes.html +0 -11
- nautobot/project-static/docs/requirements.txt +0 -14
- {nautobot-2.4.17.dist-info → nautobot-2.4.19.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.17.dist-info → nautobot-2.4.19.dist-info}/NOTICE +0 -0
- {nautobot-2.4.17.dist-info → nautobot-2.4.19.dist-info}/WHEEL +0 -0
- {nautobot-2.4.17.dist-info → nautobot-2.4.19.dist-info}/entry_points.txt +0 -0
nautobot/extras/views.py
CHANGED
|
@@ -7,10 +7,11 @@ from django.contrib import messages
|
|
|
7
7
|
from django.contrib.contenttypes.models import ContentType
|
|
8
8
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
|
9
9
|
from django.db import IntegrityError, transaction
|
|
10
|
-
from django.db.models import
|
|
10
|
+
from django.db.models import Q
|
|
11
11
|
from django.forms.utils import pretty_name
|
|
12
12
|
from django.http import Http404, HttpResponse, HttpResponseForbidden
|
|
13
13
|
from django.shortcuts import get_object_or_404, redirect, render
|
|
14
|
+
from django.template.defaultfilters import urlencode
|
|
14
15
|
from django.template.loader import get_template, TemplateDoesNotExist
|
|
15
16
|
from django.urls import reverse
|
|
16
17
|
from django.urls.exceptions import NoReverseMatch
|
|
@@ -25,7 +26,6 @@ from jsonschema.validators import Draft7Validator
|
|
|
25
26
|
from rest_framework.decorators import action
|
|
26
27
|
from rest_framework.permissions import IsAuthenticated
|
|
27
28
|
|
|
28
|
-
from nautobot.apps.ui import BaseTextPanel
|
|
29
29
|
from nautobot.core.choices import ButtonActionColorChoices
|
|
30
30
|
from nautobot.core.constants import PAGINATE_COUNT_DEFAULT
|
|
31
31
|
from nautobot.core.events import publish_event
|
|
@@ -36,8 +36,16 @@ from nautobot.core.models.utils import pretty_print_query, serialize_object_v2
|
|
|
36
36
|
from nautobot.core.tables import ButtonsColumn
|
|
37
37
|
from nautobot.core.templatetags import helpers
|
|
38
38
|
from nautobot.core.ui import object_detail
|
|
39
|
+
from nautobot.core.ui.breadcrumbs import (
|
|
40
|
+
BaseBreadcrumbItem,
|
|
41
|
+
Breadcrumbs,
|
|
42
|
+
context_object_attr,
|
|
43
|
+
InstanceParentBreadcrumbItem,
|
|
44
|
+
ModelBreadcrumbItem,
|
|
45
|
+
ViewNameBreadcrumbItem,
|
|
46
|
+
)
|
|
39
47
|
from nautobot.core.ui.choices import SectionChoices
|
|
40
|
-
from nautobot.core.ui.
|
|
48
|
+
from nautobot.core.ui.titles import Titles
|
|
41
49
|
from nautobot.core.utils.config import get_settings_or_config
|
|
42
50
|
from nautobot.core.utils.lookup import (
|
|
43
51
|
get_filterset_for_model,
|
|
@@ -46,11 +54,9 @@ from nautobot.core.utils.lookup import (
|
|
|
46
54
|
get_table_class_string_from_view_name,
|
|
47
55
|
get_table_for_model,
|
|
48
56
|
)
|
|
49
|
-
from nautobot.core.utils.permissions import get_permission_for_model
|
|
50
57
|
from nautobot.core.utils.requests import is_single_choice_field, normalize_querydict
|
|
51
58
|
from nautobot.core.views import generic, viewsets
|
|
52
59
|
from nautobot.core.views.mixins import (
|
|
53
|
-
GetReturnURLMixin,
|
|
54
60
|
ObjectBulkCreateViewMixin,
|
|
55
61
|
ObjectBulkDestroyViewMixin,
|
|
56
62
|
ObjectBulkUpdateViewMixin,
|
|
@@ -63,7 +69,7 @@ from nautobot.core.views.mixins import (
|
|
|
63
69
|
ObjectPermissionRequiredMixin,
|
|
64
70
|
)
|
|
65
71
|
from nautobot.core.views.paginator import EnhancedPaginator, get_paginate_count
|
|
66
|
-
from nautobot.core.views.utils import prepare_cloned_fields
|
|
72
|
+
from nautobot.core.views.utils import get_obj_from_context, prepare_cloned_fields
|
|
67
73
|
from nautobot.core.views.viewsets import NautobotUIViewSet
|
|
68
74
|
from nautobot.dcim.models import Controller, Device, Interface, Module, Rack, VirtualDeviceContext
|
|
69
75
|
from nautobot.dcim.tables import (
|
|
@@ -160,12 +166,12 @@ class ComputedFieldUIViewSet(NautobotUIViewSet):
|
|
|
160
166
|
fields="__all__",
|
|
161
167
|
exclude_fields=["template"],
|
|
162
168
|
),
|
|
163
|
-
ObjectTextPanel(
|
|
169
|
+
object_detail.ObjectTextPanel(
|
|
164
170
|
label="Template",
|
|
165
171
|
section=SectionChoices.FULL_WIDTH,
|
|
166
172
|
weight=100,
|
|
167
173
|
object_field="template",
|
|
168
|
-
render_as=ObjectTextPanel.RenderOptions.CODE,
|
|
174
|
+
render_as=object_detail.ObjectTextPanel.RenderOptions.CODE,
|
|
169
175
|
),
|
|
170
176
|
),
|
|
171
177
|
)
|
|
@@ -576,6 +582,59 @@ class CustomFieldUIViewSet(NautobotUIViewSet):
|
|
|
576
582
|
template_name = "extras/customfield_update.html"
|
|
577
583
|
action_buttons = ("add",)
|
|
578
584
|
|
|
585
|
+
class CustomFieldObjectFieldsPanel(object_detail.ObjectFieldsPanel):
|
|
586
|
+
def render_value(self, key, value, context):
|
|
587
|
+
obj = get_obj_from_context(context, self.context_object_key)
|
|
588
|
+
_type = getattr(obj, "type", None)
|
|
589
|
+
|
|
590
|
+
if key == "default":
|
|
591
|
+
if not value:
|
|
592
|
+
return helpers.HTML_NONE
|
|
593
|
+
if _type == "markdown":
|
|
594
|
+
return helpers.render_markdown(value)
|
|
595
|
+
elif _type == "json":
|
|
596
|
+
return helpers.render_json(value)
|
|
597
|
+
else:
|
|
598
|
+
return helpers.placeholder(value)
|
|
599
|
+
return super().render_value(key, value, context)
|
|
600
|
+
|
|
601
|
+
object_detail_content = object_detail.ObjectDetailContent(
|
|
602
|
+
panels=[
|
|
603
|
+
CustomFieldObjectFieldsPanel(
|
|
604
|
+
weight=100,
|
|
605
|
+
section=SectionChoices.LEFT_HALF,
|
|
606
|
+
fields="__all__",
|
|
607
|
+
exclude_fields=["content_types", "validation_minimum", "validation_maximum", "validation_regex"],
|
|
608
|
+
),
|
|
609
|
+
object_detail.DataTablePanel(
|
|
610
|
+
weight=200,
|
|
611
|
+
section=SectionChoices.LEFT_HALF,
|
|
612
|
+
label="Custom Field Choices",
|
|
613
|
+
context_data_key="choices_data",
|
|
614
|
+
context_columns_key="columns",
|
|
615
|
+
context_column_headers_key="header",
|
|
616
|
+
),
|
|
617
|
+
object_detail.ObjectFieldsPanel(
|
|
618
|
+
section=SectionChoices.RIGHT_HALF,
|
|
619
|
+
weight=100,
|
|
620
|
+
label="Assignment",
|
|
621
|
+
fields=[
|
|
622
|
+
"content_types",
|
|
623
|
+
],
|
|
624
|
+
key_transforms={"content_types": "Content Types"},
|
|
625
|
+
),
|
|
626
|
+
object_detail.ObjectFieldsPanel(
|
|
627
|
+
section=SectionChoices.RIGHT_HALF,
|
|
628
|
+
weight=200,
|
|
629
|
+
label="Validation Rules",
|
|
630
|
+
fields=["validation_minimum", "validation_maximum", "validation_regex"],
|
|
631
|
+
value_transforms={
|
|
632
|
+
"validation_regex": [lambda val: None if val == "" else val, helpers.pre_tag],
|
|
633
|
+
},
|
|
634
|
+
),
|
|
635
|
+
]
|
|
636
|
+
)
|
|
637
|
+
|
|
579
638
|
def get_extra_context(self, request, instance):
|
|
580
639
|
context = super().get_extra_context(request, instance)
|
|
581
640
|
|
|
@@ -585,6 +644,16 @@ class CustomFieldUIViewSet(NautobotUIViewSet):
|
|
|
585
644
|
else:
|
|
586
645
|
context["choices"] = forms.CustomFieldChoiceFormSet(instance=instance)
|
|
587
646
|
|
|
647
|
+
if self.action == "retrieve":
|
|
648
|
+
choices_data = []
|
|
649
|
+
|
|
650
|
+
for choice in instance.custom_field_choices.all():
|
|
651
|
+
choices_data.append({"value": choice.value, "weight": choice.weight})
|
|
652
|
+
|
|
653
|
+
context["columns"] = ["value", "weight"]
|
|
654
|
+
context["header"] = ["Value", "Weight"]
|
|
655
|
+
context["choices_data"] = choices_data
|
|
656
|
+
|
|
588
657
|
return context
|
|
589
658
|
|
|
590
659
|
def form_save(self, form, **kwargs):
|
|
@@ -613,9 +682,9 @@ class CustomLinkUIViewSet(NautobotUIViewSet):
|
|
|
613
682
|
serializer_class = serializers.CustomLinkSerializer
|
|
614
683
|
table_class = tables.CustomLinkTable
|
|
615
684
|
|
|
616
|
-
object_detail_content = ObjectDetailContent(
|
|
685
|
+
object_detail_content = object_detail.ObjectDetailContent(
|
|
617
686
|
panels=[
|
|
618
|
-
ObjectFieldsPanel(
|
|
687
|
+
object_detail.ObjectFieldsPanel(
|
|
619
688
|
label="Custom Link",
|
|
620
689
|
section=SectionChoices.LEFT_HALF,
|
|
621
690
|
weight=100,
|
|
@@ -649,212 +718,263 @@ class CustomLinkUIViewSet(NautobotUIViewSet):
|
|
|
649
718
|
#
|
|
650
719
|
|
|
651
720
|
|
|
652
|
-
class
|
|
721
|
+
class DynamicGroupUIViewSet(NautobotUIViewSet):
|
|
722
|
+
bulk_update_form_class = forms.DynamicGroupBulkEditForm
|
|
723
|
+
filterset_class = filters.DynamicGroupFilterSet
|
|
724
|
+
filterset_form_class = forms.DynamicGroupFilterForm
|
|
725
|
+
form_class = forms.DynamicGroupForm
|
|
653
726
|
queryset = DynamicGroup.objects.all()
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
filterset_form = forms.DynamicGroupFilterForm
|
|
727
|
+
serializer_class = serializers.DynamicGroupSerializer
|
|
728
|
+
table_class = tables.DynamicGroupTable
|
|
657
729
|
action_buttons = ("add",)
|
|
658
730
|
|
|
659
|
-
|
|
660
|
-
class DynamicGroupView(generic.ObjectView):
|
|
661
|
-
queryset = DynamicGroup.objects.all()
|
|
662
|
-
|
|
663
731
|
def get_extra_context(self, request, instance):
|
|
664
732
|
context = super().get_extra_context(request, instance)
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
if table_class is not None:
|
|
676
|
-
# Members table (for display on Members nav tab)
|
|
677
|
-
if hasattr(members, "without_tree_fields"):
|
|
678
|
-
members = members.without_tree_fields()
|
|
679
|
-
members_table = table_class(
|
|
680
|
-
members.restrict(request.user, "view"),
|
|
681
|
-
orderable=False,
|
|
682
|
-
exclude=["dynamic_group_count"],
|
|
683
|
-
hide_hierarchy_ui=True,
|
|
684
|
-
)
|
|
685
|
-
paginate = {
|
|
686
|
-
"paginator_class": EnhancedPaginator,
|
|
687
|
-
"per_page": get_paginate_count(request),
|
|
688
|
-
}
|
|
689
|
-
RequestConfig(request, paginate).configure(members_table)
|
|
690
|
-
|
|
691
|
-
# Descendants table
|
|
692
|
-
descendants_memberships = instance.membership_tree()
|
|
693
|
-
descendants_table = tables.NestedDynamicGroupDescendantsTable(
|
|
694
|
-
descendants_memberships,
|
|
695
|
-
orderable=False,
|
|
696
|
-
)
|
|
697
|
-
descendants_tree = {m.pk: m.depth for m in descendants_memberships}
|
|
733
|
+
if self.action in ("create", "update"):
|
|
734
|
+
filterform_class = instance.generate_filter_form()
|
|
735
|
+
if filterform_class is None:
|
|
736
|
+
context["filter_form"] = None
|
|
737
|
+
elif request.POST:
|
|
738
|
+
context["filter_form"] = filterform_class(data=request.POST)
|
|
739
|
+
else:
|
|
740
|
+
initial = instance.get_initial()
|
|
741
|
+
context["filter_form"] = filterform_class(initial=initial)
|
|
698
742
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
743
|
+
formset_kwargs = {"instance": instance}
|
|
744
|
+
if request.POST:
|
|
745
|
+
formset_kwargs["data"] = request.POST
|
|
746
|
+
context["children"] = forms.DynamicGroupMembershipFormSet(**formset_kwargs)
|
|
703
747
|
|
|
748
|
+
elif self.action == "retrieve":
|
|
749
|
+
model = instance.model
|
|
750
|
+
table_class = get_table_for_model(model)
|
|
704
751
|
if instance.group_type != DynamicGroupTypeChoices.TYPE_STATIC:
|
|
705
|
-
|
|
706
|
-
|
|
752
|
+
# Ensure that members cache is up-to-date for this specific group
|
|
753
|
+
members = instance.update_cached_members()
|
|
754
|
+
messages.success(request, f"Refreshed cached members list for {instance}")
|
|
707
755
|
else:
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
756
|
+
members = instance.members
|
|
757
|
+
if table_class is not None:
|
|
758
|
+
if hasattr(members, "without_tree_fields"):
|
|
759
|
+
members = members.without_tree_fields()
|
|
760
|
+
|
|
761
|
+
members_table = table_class(
|
|
762
|
+
members.restrict(request.user, "view"),
|
|
763
|
+
orderable=False,
|
|
764
|
+
exclude=["dynamic_group_count"],
|
|
765
|
+
hide_hierarchy_ui=True,
|
|
766
|
+
)
|
|
767
|
+
paginate = {
|
|
768
|
+
"paginator_class": EnhancedPaginator,
|
|
769
|
+
"per_page": get_paginate_count(request),
|
|
770
|
+
}
|
|
771
|
+
RequestConfig(request, paginate).configure(members_table)
|
|
719
772
|
|
|
720
|
-
|
|
773
|
+
# Descendants table
|
|
774
|
+
descendants_memberships = instance.membership_tree()
|
|
775
|
+
descendants_table = tables.NestedDynamicGroupDescendantsTable(
|
|
776
|
+
descendants_memberships,
|
|
777
|
+
orderable=False,
|
|
778
|
+
)
|
|
779
|
+
descendants_tree = {m.pk: m.depth for m in descendants_memberships}
|
|
721
780
|
|
|
781
|
+
# Ancestors table
|
|
782
|
+
ancestors = instance.get_ancestors()
|
|
783
|
+
ancestors_table = tables.NestedDynamicGroupAncestorsTable(
|
|
784
|
+
ancestors,
|
|
785
|
+
orderable=False,
|
|
786
|
+
)
|
|
787
|
+
ancestors_tree = instance.flatten_ancestors_tree(instance.ancestors_tree())
|
|
788
|
+
if instance.group_type != DynamicGroupTypeChoices.TYPE_STATIC:
|
|
789
|
+
context["raw_query"] = pretty_print_query(instance.generate_query())
|
|
790
|
+
context["members_list_url"] = None
|
|
791
|
+
else:
|
|
792
|
+
context["raw_query"] = None
|
|
793
|
+
try:
|
|
794
|
+
context["members_list_url"] = reverse(get_route_for_model(instance.model, "list"))
|
|
795
|
+
except NoReverseMatch:
|
|
796
|
+
context["members_list_url"] = None
|
|
797
|
+
|
|
798
|
+
context.update(
|
|
799
|
+
{
|
|
800
|
+
"members_verbose_name_plural": instance.model._meta.verbose_name_plural,
|
|
801
|
+
"members_table": members_table,
|
|
802
|
+
"ancestors_table": ancestors_table,
|
|
803
|
+
"ancestors_tree": ancestors_tree,
|
|
804
|
+
"descendants_table": descendants_table,
|
|
805
|
+
"descendants_tree": descendants_tree,
|
|
806
|
+
}
|
|
807
|
+
)
|
|
722
808
|
|
|
723
|
-
|
|
724
|
-
queryset = DynamicGroup.objects.all()
|
|
725
|
-
model_form = forms.DynamicGroupForm
|
|
726
|
-
template_name = "extras/dynamicgroup_edit.html"
|
|
809
|
+
return context
|
|
727
810
|
|
|
728
|
-
def
|
|
729
|
-
|
|
811
|
+
def form_save(self, form, **kwargs):
|
|
812
|
+
obj = form.save(commit=False)
|
|
813
|
+
context = self.get_extra_context(self.request, obj)
|
|
814
|
+
|
|
815
|
+
# Save filters
|
|
816
|
+
if obj.group_type == DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER:
|
|
817
|
+
filter_form = context.get("filter_form")
|
|
818
|
+
if not filter_form or not filter_form.is_valid():
|
|
819
|
+
form.add_error(None, "Errors encountered when saving Dynamic Group associations. See below.")
|
|
820
|
+
raise ValidationError("invalid dynamic group filter_form")
|
|
821
|
+
try:
|
|
822
|
+
obj.set_filter(filter_form.cleaned_data)
|
|
823
|
+
except ValidationError as err:
|
|
824
|
+
form.add_error(None, "Invalid filter detected in existing DynamicGroup filter data.")
|
|
825
|
+
for msg in getattr(err, "messages", [str(err)]):
|
|
826
|
+
if msg:
|
|
827
|
+
form.add_error(None, msg)
|
|
828
|
+
raise
|
|
829
|
+
|
|
830
|
+
# After filters have been set, now we save the object to the database.
|
|
831
|
+
obj.save()
|
|
832
|
+
# Save m2m fields, such as Tags https://docs.djangoproject.com/en/3.2/topics/forms/modelforms/#the-save-method
|
|
833
|
+
form.save_m2m()
|
|
834
|
+
|
|
835
|
+
# Process the formsets for children
|
|
836
|
+
children = context.get("children")
|
|
837
|
+
if children and not children.is_valid():
|
|
838
|
+
form.add_error(None, "Errors encountered when saving Dynamic Group associations. See below.")
|
|
839
|
+
# dedupe only non-field errors to avoid duplicates in the banner
|
|
840
|
+
added_errors = set()
|
|
841
|
+
for f in children.forms:
|
|
842
|
+
for msg in f.non_field_errors():
|
|
843
|
+
if msg not in added_errors:
|
|
844
|
+
form.add_error(None, msg)
|
|
845
|
+
added_errors.add(msg)
|
|
846
|
+
raise ValidationError("invalid DynamicGroupMembershipFormSet")
|
|
847
|
+
|
|
848
|
+
if children:
|
|
849
|
+
children.save()
|
|
730
850
|
|
|
731
|
-
|
|
851
|
+
return obj
|
|
732
852
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
filter_form = filterform_class(data=request.POST)
|
|
737
|
-
else:
|
|
738
|
-
initial = instance.get_initial()
|
|
739
|
-
filter_form = filterform_class(initial=initial)
|
|
853
|
+
# Suppress the global top banner when ValidationError happens
|
|
854
|
+
def _handle_validation_error(self, e):
|
|
855
|
+
self.has_error = True
|
|
740
856
|
|
|
741
|
-
|
|
857
|
+
@action(
|
|
858
|
+
detail=False,
|
|
859
|
+
methods=["GET", "POST"],
|
|
860
|
+
url_path="assign-members",
|
|
861
|
+
url_name="bulk_assign",
|
|
862
|
+
custom_view_base_action="add",
|
|
863
|
+
custom_view_additional_permissions=[
|
|
864
|
+
"extras.add_staticgroupassociation",
|
|
865
|
+
],
|
|
866
|
+
)
|
|
867
|
+
def bulk_assign(self, request):
|
|
868
|
+
"""
|
|
869
|
+
Update the static group assignments of the provided `pk_list` (or `_all`) of the given `content_type`.
|
|
742
870
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
871
|
+
Unlike BulkEditView, this takes a single POST rather than two to perform its operation as
|
|
872
|
+
there's no separate confirmation step involved.
|
|
873
|
+
"""
|
|
874
|
+
if request.method == "GET":
|
|
875
|
+
return redirect(reverse("extras:staticgroupassociation_list"))
|
|
746
876
|
|
|
747
|
-
|
|
877
|
+
# TODO more error handling - content-type doesn't exist, model_class not found, filterset missing, etc.
|
|
878
|
+
content_type = ContentType.objects.get(pk=request.POST.get("content_type"))
|
|
879
|
+
model = content_type.model_class()
|
|
880
|
+
self.default_return_url = get_route_for_model(model, "list")
|
|
881
|
+
filterset_class = get_filterset_for_model(model)
|
|
748
882
|
|
|
749
|
-
|
|
883
|
+
if request.POST.get("_all"):
|
|
884
|
+
if filterset_class:
|
|
885
|
+
pk_list = list(filterset_class(request.GET, model.objects.only("pk")).qs.values_list("pk", flat=True))
|
|
886
|
+
else:
|
|
887
|
+
pk_list = list(model.objects.values_list("pk", flat=True))
|
|
888
|
+
else:
|
|
889
|
+
pk_list = request.POST.getlist("pk")
|
|
750
890
|
|
|
751
|
-
|
|
752
|
-
obj = self.alter_obj(self.get_object(kwargs), request, args, kwargs)
|
|
753
|
-
form = self.model_form(data=request.POST, files=request.FILES, instance=obj)
|
|
891
|
+
form = forms.DynamicGroupBulkAssignForm(model, request.POST)
|
|
754
892
|
restrict_form_fields(form, request.user)
|
|
755
893
|
|
|
756
894
|
if form.is_valid():
|
|
757
895
|
logger.debug("Form validation was successful")
|
|
758
|
-
|
|
759
896
|
try:
|
|
760
897
|
with transaction.atomic():
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
if obj.group_type == DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER:
|
|
767
|
-
# Process the filter form and save the query filters to `obj.filter`.
|
|
768
|
-
filter_form = ctx["filter_form"]
|
|
769
|
-
if filter_form.is_valid():
|
|
770
|
-
obj.set_filter(filter_form.cleaned_data)
|
|
898
|
+
add_to_groups = list(form.cleaned_data["add_to_groups"])
|
|
899
|
+
new_group_name = form.cleaned_data["create_and_assign_to_new_group_name"]
|
|
900
|
+
if new_group_name:
|
|
901
|
+
if not request.user.has_perm("extras.add_dynamicgroup"):
|
|
902
|
+
raise DynamicGroup.DoesNotExist
|
|
771
903
|
else:
|
|
772
|
-
|
|
904
|
+
new_group = DynamicGroup(
|
|
905
|
+
name=new_group_name,
|
|
906
|
+
content_type=content_type,
|
|
907
|
+
group_type=DynamicGroupTypeChoices.TYPE_STATIC,
|
|
908
|
+
)
|
|
909
|
+
new_group.validated_save()
|
|
910
|
+
# Check permissions
|
|
911
|
+
DynamicGroup.objects.restrict(request.user, "add").get(pk=new_group.pk)
|
|
773
912
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
self.queryset.get(pk=obj.pk)
|
|
913
|
+
add_to_groups.append(new_group)
|
|
914
|
+
msg = "Created dynamic group"
|
|
915
|
+
logger.info(f"{msg} {new_group} (PK: {new_group.pk})")
|
|
916
|
+
msg = format_html('{} <a href="{}">{}</a>', msg, new_group.get_absolute_url(), new_group)
|
|
917
|
+
messages.success(request, msg)
|
|
780
918
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
messages.success(request, msg)
|
|
919
|
+
with deferred_change_logging_for_bulk_operation():
|
|
920
|
+
associations = []
|
|
921
|
+
for pk in pk_list:
|
|
922
|
+
for dynamic_group in add_to_groups:
|
|
923
|
+
association, created = StaticGroupAssociation.objects.get_or_create(
|
|
924
|
+
dynamic_group=dynamic_group,
|
|
925
|
+
associated_object_type_id=content_type.id,
|
|
926
|
+
associated_object_id=pk,
|
|
927
|
+
)
|
|
928
|
+
association.validated_save()
|
|
929
|
+
associations.append(association)
|
|
930
|
+
if created:
|
|
931
|
+
logger.debug("Created %s", association)
|
|
795
932
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
933
|
+
# Enforce object-level permissions
|
|
934
|
+
permitted_associations = StaticGroupAssociation.objects.restrict(request.user, "add")
|
|
935
|
+
if permitted_associations.filter(pk__in=[assoc.pk for assoc in associations]).count() != len(
|
|
936
|
+
associations
|
|
937
|
+
):
|
|
938
|
+
raise StaticGroupAssociation.DoesNotExist
|
|
801
939
|
|
|
802
|
-
|
|
940
|
+
if associations:
|
|
941
|
+
msg = (
|
|
942
|
+
f"Added {len(pk_list)} {model._meta.verbose_name_plural} "
|
|
943
|
+
f"to {len(add_to_groups)} dynamic group(s)."
|
|
944
|
+
)
|
|
945
|
+
logger.info(msg)
|
|
946
|
+
messages.success(request, msg)
|
|
803
947
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
948
|
+
if form.cleaned_data["remove_from_groups"]:
|
|
949
|
+
for dynamic_group in form.cleaned_data["remove_from_groups"]:
|
|
950
|
+
(
|
|
951
|
+
StaticGroupAssociation.objects.restrict(request.user, "delete")
|
|
952
|
+
.filter(
|
|
953
|
+
dynamic_group=dynamic_group,
|
|
954
|
+
associated_object_type=content_type,
|
|
955
|
+
associated_object_id__in=pk_list,
|
|
956
|
+
)
|
|
957
|
+
.delete()
|
|
958
|
+
)
|
|
809
959
|
|
|
960
|
+
msg = (
|
|
961
|
+
f"Removed {len(pk_list)} {model._meta.verbose_name_plural} from "
|
|
962
|
+
f"{len(form.cleaned_data['remove_from_groups'])} dynamic group(s)."
|
|
963
|
+
)
|
|
964
|
+
logger.info(msg)
|
|
965
|
+
messages.success(request, msg)
|
|
966
|
+
except ValidationError as e:
|
|
967
|
+
messages.error(request, e)
|
|
810
968
|
except ObjectDoesNotExist:
|
|
811
|
-
msg = "
|
|
812
|
-
logger.
|
|
813
|
-
|
|
814
|
-
except RuntimeError:
|
|
815
|
-
msg = "Errors encountered when saving Dynamic Group associations. See below."
|
|
816
|
-
logger.debug(msg)
|
|
817
|
-
form.add_error(None, msg)
|
|
818
|
-
except ProtectedError as err:
|
|
819
|
-
# e.g. Trying to delete a something that is in use.
|
|
820
|
-
err_msg = err.args[0]
|
|
821
|
-
protected_obj = err.protected_objects[0]
|
|
822
|
-
msg = f"{protected_obj.value}: {err_msg} Please cancel this edit and start again."
|
|
823
|
-
logger.debug(msg)
|
|
824
|
-
form.add_error(None, msg)
|
|
825
|
-
except ValidationError as err:
|
|
826
|
-
msg = "Invalid filter detected in existing DynamicGroup filter data."
|
|
827
|
-
logger.debug(msg)
|
|
828
|
-
err_messages = err.args[0].split("\n")
|
|
829
|
-
for message in err_messages:
|
|
830
|
-
if message:
|
|
831
|
-
form.add_error(None, message)
|
|
969
|
+
msg = "Static group association failed due to object-level permissions violation"
|
|
970
|
+
logger.warning(msg)
|
|
971
|
+
messages.error(request, msg)
|
|
832
972
|
|
|
833
973
|
else:
|
|
834
974
|
logger.debug("Form validation failed")
|
|
975
|
+
messages.error(request, form.errors)
|
|
835
976
|
|
|
836
|
-
return
|
|
837
|
-
request,
|
|
838
|
-
self.template_name,
|
|
839
|
-
{
|
|
840
|
-
"obj": obj,
|
|
841
|
-
"obj_type": self.queryset.model._meta.verbose_name,
|
|
842
|
-
"form": form,
|
|
843
|
-
"return_url": self.get_return_url(request, obj),
|
|
844
|
-
"editing": obj.present_in_database,
|
|
845
|
-
**self.get_extra_context(request, obj),
|
|
846
|
-
},
|
|
847
|
-
)
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
class DynamicGroupDeleteView(generic.ObjectDeleteView):
|
|
851
|
-
queryset = DynamicGroup.objects.all()
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
class DynamicGroupBulkDeleteView(generic.BulkDeleteView):
|
|
855
|
-
queryset = DynamicGroup.objects.all()
|
|
856
|
-
table = tables.DynamicGroupTable
|
|
857
|
-
filterset = filters.DynamicGroupFilterSet
|
|
977
|
+
return redirect(self.get_return_url(request))
|
|
858
978
|
|
|
859
979
|
|
|
860
980
|
class ObjectDynamicGroupsView(generic.GenericView):
|
|
@@ -870,6 +990,8 @@ class ObjectDynamicGroupsView(generic.GenericView):
|
|
|
870
990
|
"""
|
|
871
991
|
|
|
872
992
|
base_template: Optional[str] = None
|
|
993
|
+
breadcrumbs = Breadcrumbs()
|
|
994
|
+
view_titles = Titles()
|
|
873
995
|
|
|
874
996
|
def get(self, request, model, **kwargs):
|
|
875
997
|
# Handle QuerySet restriction of parent object if needed
|
|
@@ -903,6 +1025,9 @@ class ObjectDynamicGroupsView(generic.GenericView):
|
|
|
903
1025
|
"table": dynamicgroups_table,
|
|
904
1026
|
"base_template": base_template,
|
|
905
1027
|
"active_tab": "dynamic-groups",
|
|
1028
|
+
"breadcrumbs": self.breadcrumbs,
|
|
1029
|
+
"view_titles": self.view_titles,
|
|
1030
|
+
"detail": True,
|
|
906
1031
|
},
|
|
907
1032
|
)
|
|
908
1033
|
|
|
@@ -921,26 +1046,26 @@ class ExportTemplateUIViewSet(NautobotUIViewSet):
|
|
|
921
1046
|
serializer_class = serializers.ExportTemplateSerializer
|
|
922
1047
|
table_class = tables.ExportTemplateTable
|
|
923
1048
|
|
|
924
|
-
object_detail_content = ObjectDetailContent(
|
|
1049
|
+
object_detail_content = object_detail.ObjectDetailContent(
|
|
925
1050
|
panels=[
|
|
926
|
-
ObjectFieldsPanel(
|
|
1051
|
+
object_detail.ObjectFieldsPanel(
|
|
927
1052
|
label="Details",
|
|
928
1053
|
section=SectionChoices.LEFT_HALF,
|
|
929
1054
|
weight=100,
|
|
930
1055
|
fields=["name", "owner", "description"],
|
|
931
1056
|
),
|
|
932
|
-
ObjectFieldsPanel(
|
|
1057
|
+
object_detail.ObjectFieldsPanel(
|
|
933
1058
|
label="Template",
|
|
934
1059
|
section=SectionChoices.LEFT_HALF,
|
|
935
1060
|
weight=200,
|
|
936
1061
|
fields=["content_type", "mime_type", "file_extension"],
|
|
937
1062
|
),
|
|
938
|
-
ObjectTextPanel(
|
|
1063
|
+
object_detail.ObjectTextPanel(
|
|
939
1064
|
label="Code Template",
|
|
940
1065
|
section=SectionChoices.RIGHT_HALF,
|
|
941
1066
|
weight=100,
|
|
942
1067
|
object_field="template_code",
|
|
943
|
-
render_as=ObjectTextPanel.RenderOptions.CODE,
|
|
1068
|
+
render_as=object_detail.ObjectTextPanel.RenderOptions.CODE,
|
|
944
1069
|
),
|
|
945
1070
|
]
|
|
946
1071
|
)
|
|
@@ -987,97 +1112,6 @@ class ExternalIntegrationUIViewSet(NautobotUIViewSet):
|
|
|
987
1112
|
#
|
|
988
1113
|
|
|
989
1114
|
|
|
990
|
-
class GitRepositoryListView(generic.ObjectListView):
|
|
991
|
-
queryset = GitRepository.objects.all()
|
|
992
|
-
filterset = filters.GitRepositoryFilterSet
|
|
993
|
-
filterset_form = forms.GitRepositoryFilterForm
|
|
994
|
-
table = tables.GitRepositoryTable
|
|
995
|
-
template_name = "extras/gitrepository_list.html"
|
|
996
|
-
|
|
997
|
-
def extra_context(self):
|
|
998
|
-
# Get the newest results for each repository name
|
|
999
|
-
results = {
|
|
1000
|
-
r.task_kwargs["repository"]: r
|
|
1001
|
-
for r in JobResult.objects.filter(
|
|
1002
|
-
task_name__startswith="nautobot.core.jobs.GitRepository",
|
|
1003
|
-
task_kwargs__repository__isnull=False,
|
|
1004
|
-
status__in=JobResultStatusChoices.READY_STATES,
|
|
1005
|
-
)
|
|
1006
|
-
.order_by("date_done")
|
|
1007
|
-
.defer("result")
|
|
1008
|
-
}
|
|
1009
|
-
return {
|
|
1010
|
-
"job_results": results,
|
|
1011
|
-
"datasource_contents": get_datasource_contents("extras.gitrepository"),
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
class GitRepositoryView(generic.ObjectView):
|
|
1016
|
-
queryset = GitRepository.objects.all()
|
|
1017
|
-
|
|
1018
|
-
def get_extra_context(self, request, instance):
|
|
1019
|
-
return {
|
|
1020
|
-
"datasource_contents": get_datasource_contents("extras.gitrepository"),
|
|
1021
|
-
**super().get_extra_context(request, instance),
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
class GitRepositoryEditView(generic.ObjectEditView):
|
|
1026
|
-
queryset = GitRepository.objects.all()
|
|
1027
|
-
model_form = forms.GitRepositoryForm
|
|
1028
|
-
template_name = "extras/gitrepository_object_edit.html"
|
|
1029
|
-
|
|
1030
|
-
# TODO(jathan): Align with changes for v2 where we're not stashing the user on the instance for
|
|
1031
|
-
# magical calls and instead discretely calling `repo.sync(user=user, dry_run=dry_run)`, but
|
|
1032
|
-
# again, this will be moved to the API calls, so just something to keep in mind.
|
|
1033
|
-
def alter_obj(self, obj, request, url_args, url_kwargs):
|
|
1034
|
-
# A GitRepository needs to know the originating request when it's saved so that it can enqueue using it
|
|
1035
|
-
obj.user = request.user
|
|
1036
|
-
return super().alter_obj(obj, request, url_args, url_kwargs)
|
|
1037
|
-
|
|
1038
|
-
def get_return_url(self, request, obj=None, default_return_url=None):
|
|
1039
|
-
if request.method == "POST":
|
|
1040
|
-
return reverse("extras:gitrepository_result", kwargs={"pk": obj.pk})
|
|
1041
|
-
return super().get_return_url(request, obj=obj, default_return_url=default_return_url)
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
class GitRepositoryDeleteView(generic.ObjectDeleteView):
|
|
1045
|
-
queryset = GitRepository.objects.all()
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
class GitRepositoryBulkImportView(generic.BulkImportView): # 3.0 TODO: remove, unused
|
|
1049
|
-
queryset = GitRepository.objects.all()
|
|
1050
|
-
table = tables.GitRepositoryBulkTable
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
class GitRepositoryBulkEditView(generic.BulkEditView):
|
|
1054
|
-
queryset = GitRepository.objects.select_related("secrets_group")
|
|
1055
|
-
filterset = filters.GitRepositoryFilterSet
|
|
1056
|
-
table = tables.GitRepositoryBulkTable
|
|
1057
|
-
form = forms.GitRepositoryBulkEditForm
|
|
1058
|
-
|
|
1059
|
-
def alter_obj(self, obj, request, url_args, url_kwargs):
|
|
1060
|
-
# A GitRepository needs to know the originating request when it's saved so that it can enqueue using it
|
|
1061
|
-
obj.request = request
|
|
1062
|
-
return super().alter_obj(obj, request, url_args, url_kwargs)
|
|
1063
|
-
|
|
1064
|
-
def extra_context(self):
|
|
1065
|
-
return {
|
|
1066
|
-
"datasource_contents": get_datasource_contents("extras.gitrepository"),
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
class GitRepositoryBulkDeleteView(generic.BulkDeleteView):
|
|
1071
|
-
queryset = GitRepository.objects.all()
|
|
1072
|
-
table = tables.GitRepositoryBulkTable
|
|
1073
|
-
filterset = filters.GitRepositoryFilterSet
|
|
1074
|
-
|
|
1075
|
-
def extra_context(self):
|
|
1076
|
-
return {
|
|
1077
|
-
"datasource_contents": get_datasource_contents("extras.gitrepository"),
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
1115
|
def check_and_call_git_repository_function(request, pk, func):
|
|
1082
1116
|
"""Helper for checking Git permissions and worker availability, then calling provided function if all is well
|
|
1083
1117
|
Args:
|
|
@@ -1102,40 +1136,88 @@ def check_and_call_git_repository_function(request, pk, func):
|
|
|
1102
1136
|
return redirect(job_result.get_absolute_url())
|
|
1103
1137
|
|
|
1104
1138
|
|
|
1105
|
-
class
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1139
|
+
class GitRepositoryUIViewSet(NautobotUIViewSet):
|
|
1140
|
+
bulk_update_form_class = forms.GitRepositoryBulkEditForm
|
|
1141
|
+
filterset_form_class = forms.GitRepositoryFilterForm
|
|
1142
|
+
queryset = GitRepository.objects.all()
|
|
1143
|
+
form_class = forms.GitRepositoryForm
|
|
1144
|
+
filterset_class = filters.GitRepositoryFilterSet
|
|
1145
|
+
serializer_class = serializers.GitRepositorySerializer
|
|
1146
|
+
table_class = tables.GitRepositoryTable
|
|
1113
1147
|
|
|
1148
|
+
def get_extra_context(self, request, instance=None):
|
|
1149
|
+
context = super().get_extra_context(request, instance)
|
|
1150
|
+
context["datasource_contents"] = get_datasource_contents("extras.gitrepository")
|
|
1151
|
+
|
|
1152
|
+
if self.action in ("list", "bulk_update", "bulk_destroy"):
|
|
1153
|
+
results = {
|
|
1154
|
+
r.task_kwargs["repository"]: r
|
|
1155
|
+
for r in JobResult.objects.filter(
|
|
1156
|
+
task_name__startswith="nautobot.core.jobs.GitRepository",
|
|
1157
|
+
task_kwargs__repository__isnull=False,
|
|
1158
|
+
status__in=JobResultStatusChoices.READY_STATES,
|
|
1159
|
+
)
|
|
1160
|
+
.order_by("date_done")
|
|
1161
|
+
.defer("result")
|
|
1162
|
+
}
|
|
1163
|
+
context["job_results"] = results
|
|
1114
1164
|
|
|
1115
|
-
|
|
1116
|
-
"""
|
|
1117
|
-
Display a JobResult and its Job data.
|
|
1118
|
-
"""
|
|
1165
|
+
return context
|
|
1119
1166
|
|
|
1120
|
-
|
|
1121
|
-
|
|
1167
|
+
def form_valid(self, form):
|
|
1168
|
+
if hasattr(form, "instance") and form.instance is not None:
|
|
1169
|
+
form.instance.user = self.request.user
|
|
1170
|
+
form.instance.request = self.request
|
|
1171
|
+
return super().form_valid(form)
|
|
1122
1172
|
|
|
1123
|
-
def
|
|
1124
|
-
|
|
1173
|
+
def get_return_url(self, request, obj=None, default_return_url=None):
|
|
1174
|
+
# Only redirect to result if object exists and action is not deletion
|
|
1175
|
+
if request.method == "POST" and obj is not None and self.action != "destroy":
|
|
1176
|
+
return reverse("extras:gitrepository_result", kwargs={"pk": obj.pk})
|
|
1177
|
+
return super().get_return_url(request, obj=obj, default_return_url=default_return_url)
|
|
1125
1178
|
|
|
1126
|
-
|
|
1179
|
+
@action(
|
|
1180
|
+
detail=True,
|
|
1181
|
+
url_path="result",
|
|
1182
|
+
url_name="result",
|
|
1183
|
+
custom_view_base_action="view",
|
|
1184
|
+
)
|
|
1185
|
+
def result(self, request, pk=None):
|
|
1186
|
+
instance = self.get_object()
|
|
1127
1187
|
job_result = instance.get_latest_sync()
|
|
1128
1188
|
|
|
1129
|
-
|
|
1130
|
-
job_result
|
|
1131
|
-
|
|
1132
|
-
return {
|
|
1133
|
-
"result": job_result,
|
|
1189
|
+
context = {
|
|
1190
|
+
"result": job_result or {},
|
|
1134
1191
|
"base_template": "extras/gitrepository.html",
|
|
1135
1192
|
"object": instance,
|
|
1136
1193
|
"active_tab": "result",
|
|
1194
|
+
"verbose_name": instance._meta.verbose_name,
|
|
1137
1195
|
}
|
|
1138
1196
|
|
|
1197
|
+
return render(request, "extras/gitrepository_result.html", context)
|
|
1198
|
+
|
|
1199
|
+
@action(
|
|
1200
|
+
detail=True,
|
|
1201
|
+
methods=["post"],
|
|
1202
|
+
url_path="sync",
|
|
1203
|
+
url_name="sync",
|
|
1204
|
+
custom_view_base_action="change",
|
|
1205
|
+
custom_view_additional_permissions=["extras.change_gitrepository"],
|
|
1206
|
+
)
|
|
1207
|
+
def sync(self, request, pk=None):
|
|
1208
|
+
return check_and_call_git_repository_function(request, pk, enqueue_pull_git_repository_and_refresh_data)
|
|
1209
|
+
|
|
1210
|
+
@action(
|
|
1211
|
+
detail=True,
|
|
1212
|
+
methods=["post"],
|
|
1213
|
+
url_path="dry-run",
|
|
1214
|
+
url_name="dryrun",
|
|
1215
|
+
custom_view_base_action="change",
|
|
1216
|
+
custom_view_additional_permissions=["extras.change_gitrepository"],
|
|
1217
|
+
)
|
|
1218
|
+
def dry_run(self, request, pk=None):
|
|
1219
|
+
return check_and_call_git_repository_function(request, pk, enqueue_git_repository_diff_origin_and_local)
|
|
1220
|
+
|
|
1139
1221
|
|
|
1140
1222
|
#
|
|
1141
1223
|
# Saved GraphQL queries
|
|
@@ -2036,9 +2118,9 @@ class JobHookUIViewSet(NautobotUIViewSet):
|
|
|
2036
2118
|
table_class = tables.JobHookTable
|
|
2037
2119
|
queryset = JobHook.objects.all()
|
|
2038
2120
|
|
|
2039
|
-
object_detail_content = ObjectDetailContent(
|
|
2121
|
+
object_detail_content = object_detail.ObjectDetailContent(
|
|
2040
2122
|
panels=(
|
|
2041
|
-
ObjectFieldsPanel(
|
|
2123
|
+
object_detail.ObjectFieldsPanel(
|
|
2042
2124
|
weight=100,
|
|
2043
2125
|
section=SectionChoices.LEFT_HALF,
|
|
2044
2126
|
fields="__all__",
|
|
@@ -2064,6 +2146,37 @@ class JobResultUIViewSet(
|
|
|
2064
2146
|
table_class = tables.JobResultTable
|
|
2065
2147
|
queryset = JobResult.objects.all()
|
|
2066
2148
|
action_buttons = ()
|
|
2149
|
+
breadcrumbs = Breadcrumbs(
|
|
2150
|
+
items={
|
|
2151
|
+
"detail": [
|
|
2152
|
+
ModelBreadcrumbItem(),
|
|
2153
|
+
# if result.job_model is not None
|
|
2154
|
+
BaseBreadcrumbItem(
|
|
2155
|
+
label=context_object_attr("job_model.grouping", context_key="result"),
|
|
2156
|
+
should_render=lambda c: c["result"].job_model is not None,
|
|
2157
|
+
),
|
|
2158
|
+
InstanceParentBreadcrumbItem(
|
|
2159
|
+
instance_key="result",
|
|
2160
|
+
parent_key="job_model",
|
|
2161
|
+
parent_lookup_key="name",
|
|
2162
|
+
should_render=lambda c: c["result"].job_model is not None,
|
|
2163
|
+
),
|
|
2164
|
+
# elif job in context
|
|
2165
|
+
ViewNameBreadcrumbItem(
|
|
2166
|
+
view_name="extras:jobresult_list",
|
|
2167
|
+
label=context_object_attr("class_path", context_key="job"),
|
|
2168
|
+
reverse_query_params=lambda c: {"name": urlencode(c["job"].class_path)},
|
|
2169
|
+
should_render=lambda c: c["result"].job_model is None and c["job"] is not None,
|
|
2170
|
+
),
|
|
2171
|
+
# else
|
|
2172
|
+
BaseBreadcrumbItem(
|
|
2173
|
+
label=context_object_attr("name", context_key="result"),
|
|
2174
|
+
should_render=lambda c: c["result"].job_model is None and c["job"] is None,
|
|
2175
|
+
),
|
|
2176
|
+
]
|
|
2177
|
+
},
|
|
2178
|
+
detail_item_label=context_object_attr("date_created"),
|
|
2179
|
+
)
|
|
2067
2180
|
|
|
2068
2181
|
def get_extra_context(self, request, instance):
|
|
2069
2182
|
context = super().get_extra_context(request, instance)
|
|
@@ -2134,9 +2247,9 @@ class JobButtonUIViewSet(NautobotUIViewSet):
|
|
|
2134
2247
|
queryset = JobButton.objects.all()
|
|
2135
2248
|
serializer_class = serializers.JobButtonSerializer
|
|
2136
2249
|
table_class = tables.JobButtonTable
|
|
2137
|
-
object_detail_content = ObjectDetailContent(
|
|
2250
|
+
object_detail_content = object_detail.ObjectDetailContent(
|
|
2138
2251
|
panels=(
|
|
2139
|
-
ObjectFieldsPanel(
|
|
2252
|
+
object_detail.ObjectFieldsPanel(
|
|
2140
2253
|
weight=100,
|
|
2141
2254
|
section=SectionChoices.LEFT_HALF,
|
|
2142
2255
|
fields="__all__",
|
|
@@ -2153,18 +2266,16 @@ class JobButtonUIViewSet(NautobotUIViewSet):
|
|
|
2153
2266
|
#
|
|
2154
2267
|
# Change logging
|
|
2155
2268
|
#
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2269
|
+
class ObjectChangeUIViewSet(ObjectDetailViewMixin, ObjectListViewMixin):
|
|
2270
|
+
filterset_class = filters.ObjectChangeFilterSet
|
|
2271
|
+
filterset_form_class = forms.ObjectChangeFilterForm
|
|
2159
2272
|
queryset = ObjectChange.objects.all()
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
table = tables.ObjectChangeTable
|
|
2163
|
-
template_name = "extras/objectchange_list.html"
|
|
2273
|
+
serializer_class = serializers.ObjectChangeSerializer
|
|
2274
|
+
table_class = tables.ObjectChangeTable
|
|
2164
2275
|
action_buttons = ("export",)
|
|
2165
2276
|
|
|
2166
2277
|
# 2.0 TODO: Remove this remapping and solve it at the `BaseFilterSet` as it is addressing a breaking change.
|
|
2167
|
-
def get(self, request, **kwargs):
|
|
2278
|
+
def get(self, request, *args, **kwargs):
|
|
2168
2279
|
# Remappings below allow previous queries of time_before and time_after to use
|
|
2169
2280
|
# newer methods specifying the lookup method.
|
|
2170
2281
|
|
|
@@ -2180,26 +2291,34 @@ class ObjectChangeListView(generic.ObjectListView):
|
|
|
2180
2291
|
request.GET.update({"time__lte": request.GET.get("time_before")})
|
|
2181
2292
|
request.GET._mutable = False
|
|
2182
2293
|
|
|
2183
|
-
return super().get(request=request, **kwargs)
|
|
2294
|
+
return super().get(request=request, *args, **kwargs)
|
|
2184
2295
|
|
|
2296
|
+
def get_extra_context(self, request, instance):
|
|
2297
|
+
"""
|
|
2298
|
+
Adds snapshot diff and related changes table for the object change detail view.
|
|
2299
|
+
"""
|
|
2300
|
+
context = super().get_extra_context(request, instance)
|
|
2185
2301
|
|
|
2186
|
-
|
|
2187
|
-
|
|
2302
|
+
if self.action == "retrieve":
|
|
2303
|
+
related_changes = instance.get_related_changes(user=request.user).filter(request_id=instance.request_id)
|
|
2304
|
+
related_changes_table = tables.ObjectChangeTable(
|
|
2305
|
+
data=related_changes[:50], # Limit for performance
|
|
2306
|
+
orderable=False,
|
|
2307
|
+
)
|
|
2308
|
+
snapshots = instance.get_snapshots()
|
|
2188
2309
|
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2310
|
+
context.update(
|
|
2311
|
+
{
|
|
2312
|
+
"diff_added": snapshots["differences"]["added"],
|
|
2313
|
+
"diff_removed": snapshots["differences"]["removed"],
|
|
2314
|
+
"next_change": instance.get_next_change(request.user),
|
|
2315
|
+
"prev_change": instance.get_prev_change(request.user),
|
|
2316
|
+
"related_changes_table": related_changes_table,
|
|
2317
|
+
"related_changes_count": related_changes.count(),
|
|
2318
|
+
}
|
|
2319
|
+
)
|
|
2192
2320
|
|
|
2193
|
-
|
|
2194
|
-
return {
|
|
2195
|
-
"diff_added": snapshots["differences"]["added"],
|
|
2196
|
-
"diff_removed": snapshots["differences"]["removed"],
|
|
2197
|
-
"next_change": instance.get_next_change(request.user),
|
|
2198
|
-
"prev_change": instance.get_prev_change(request.user),
|
|
2199
|
-
"related_changes_table": related_changes_table,
|
|
2200
|
-
"related_changes_count": related_changes.count(),
|
|
2201
|
-
**super().get_extra_context(request, instance),
|
|
2202
|
-
}
|
|
2321
|
+
return context
|
|
2203
2322
|
|
|
2204
2323
|
|
|
2205
2324
|
class ObjectChangeLogView(generic.GenericView):
|
|
@@ -2251,6 +2370,10 @@ class ObjectChangeLogView(generic.GenericView):
|
|
|
2251
2370
|
"table": objectchanges_table,
|
|
2252
2371
|
"base_template": base_template,
|
|
2253
2372
|
"active_tab": "changelog",
|
|
2373
|
+
"breadcrumbs": self.get_breadcrumbs(obj, view_type=""),
|
|
2374
|
+
"view_titles": self.get_view_titles(obj, view_type=""),
|
|
2375
|
+
"detail": True,
|
|
2376
|
+
"view_action": "changelog",
|
|
2254
2377
|
},
|
|
2255
2378
|
)
|
|
2256
2379
|
|
|
@@ -2344,9 +2467,68 @@ class NoteUIViewSet(
|
|
|
2344
2467
|
serializer_class = serializers.NoteSerializer
|
|
2345
2468
|
table_class = tables.NoteTable
|
|
2346
2469
|
action_buttons = ()
|
|
2470
|
+
breadcrumbs = Breadcrumbs(
|
|
2471
|
+
items={
|
|
2472
|
+
"detail": [
|
|
2473
|
+
ModelBreadcrumbItem(model=Note),
|
|
2474
|
+
ModelBreadcrumbItem(
|
|
2475
|
+
model=lambda c: c["object"].assigned_object,
|
|
2476
|
+
action="notes",
|
|
2477
|
+
reverse_kwargs=lambda c: {"pk": c["object"].assigned_object.pk},
|
|
2478
|
+
label=lambda c: c["object"].assigned_object,
|
|
2479
|
+
should_render=lambda c: c["object"].assigned_object,
|
|
2480
|
+
),
|
|
2481
|
+
]
|
|
2482
|
+
}
|
|
2483
|
+
)
|
|
2484
|
+
|
|
2485
|
+
object_detail_content = object_detail.ObjectDetailContent(
|
|
2486
|
+
panels=(
|
|
2487
|
+
object_detail.ObjectFieldsPanel(
|
|
2488
|
+
weight=100,
|
|
2489
|
+
section=SectionChoices.LEFT_HALF,
|
|
2490
|
+
fields=["user", "assigned_object_type", "assigned_object"],
|
|
2491
|
+
),
|
|
2492
|
+
object_detail.ObjectTextPanel(
|
|
2493
|
+
label="Text",
|
|
2494
|
+
section=SectionChoices.LEFT_HALF,
|
|
2495
|
+
weight=200,
|
|
2496
|
+
object_field="note",
|
|
2497
|
+
render_as=object_detail.ObjectTextPanel.RenderOptions.MARKDOWN,
|
|
2498
|
+
),
|
|
2499
|
+
),
|
|
2500
|
+
)
|
|
2501
|
+
|
|
2502
|
+
def form_save(self, form, commit=True, *args, **kwargs):
|
|
2503
|
+
"""
|
|
2504
|
+
Save the form instance while ensuring the Note's `user` and `user_name` fields
|
|
2505
|
+
are correctly populated.
|
|
2506
|
+
|
|
2507
|
+
Args:
|
|
2508
|
+
form (Form): The validated form instance to be saved.
|
|
2509
|
+
commit (bool): If True, save the instance to the database immediately.
|
|
2510
|
+
*args, **kwargs: Additional arguments to maintain compatibility with
|
|
2511
|
+
the parent method signature.
|
|
2512
|
+
|
|
2513
|
+
Returns:
|
|
2514
|
+
Note: The saved or unsaved Note instance with `user` and `user_name` set.
|
|
2515
|
+
|
|
2516
|
+
Behavior:
|
|
2517
|
+
- Sets `user` to the currently authenticated user.
|
|
2518
|
+
- Sets `user_name` to the username of the authenticated user.
|
|
2519
|
+
- Saves the instance if `commit=True`.
|
|
2520
|
+
"""
|
|
2521
|
+
# Get instance without committing to DB
|
|
2522
|
+
obj = super().form_save(form, commit=False, *args, **kwargs)
|
|
2523
|
+
|
|
2524
|
+
# Assign user info (only authenticated users can create notes)
|
|
2525
|
+
obj.user = self.request.user
|
|
2526
|
+
obj.user_name = self.request.user.get_username()
|
|
2527
|
+
|
|
2528
|
+
# Save to DB if commit is True
|
|
2529
|
+
if commit:
|
|
2530
|
+
obj.save()
|
|
2347
2531
|
|
|
2348
|
-
def alter_obj(self, obj, request, url_args, url_kwargs):
|
|
2349
|
-
obj.user = request.user
|
|
2350
2532
|
return obj
|
|
2351
2533
|
|
|
2352
2534
|
|
|
@@ -2396,6 +2578,10 @@ class ObjectNotesView(generic.GenericView):
|
|
|
2396
2578
|
"base_template": base_template,
|
|
2397
2579
|
"active_tab": "notes",
|
|
2398
2580
|
"form": notes_form,
|
|
2581
|
+
"breadcrumbs": self.get_breadcrumbs(obj, view_type=""),
|
|
2582
|
+
"view_titles": self.get_view_titles(obj, view_type=""),
|
|
2583
|
+
"detail": True,
|
|
2584
|
+
"view_action": "notes",
|
|
2399
2585
|
},
|
|
2400
2586
|
)
|
|
2401
2587
|
|
|
@@ -2414,9 +2600,9 @@ class RelationshipUIViewSet(NautobotUIViewSet):
|
|
|
2414
2600
|
table_class = tables.RelationshipTable
|
|
2415
2601
|
queryset = Relationship.objects.all()
|
|
2416
2602
|
|
|
2417
|
-
object_detail_content = ObjectDetailContent(
|
|
2603
|
+
object_detail_content = object_detail.ObjectDetailContent(
|
|
2418
2604
|
panels=(
|
|
2419
|
-
ObjectFieldsPanel(
|
|
2605
|
+
object_detail.ObjectFieldsPanel(
|
|
2420
2606
|
label="Relationship",
|
|
2421
2607
|
section=SectionChoices.LEFT_HALF,
|
|
2422
2608
|
weight=100,
|
|
@@ -2432,13 +2618,13 @@ class RelationshipUIViewSet(NautobotUIViewSet):
|
|
|
2432
2618
|
"destination_filter",
|
|
2433
2619
|
],
|
|
2434
2620
|
),
|
|
2435
|
-
ObjectFieldsPanel(
|
|
2621
|
+
object_detail.ObjectFieldsPanel(
|
|
2436
2622
|
label="Source Attributes",
|
|
2437
2623
|
section=SectionChoices.RIGHT_HALF,
|
|
2438
2624
|
weight=100,
|
|
2439
2625
|
fields=["source_type", "source_label", "source_hidden", "source_filter"],
|
|
2440
2626
|
),
|
|
2441
|
-
ObjectFieldsPanel(
|
|
2627
|
+
object_detail.ObjectFieldsPanel(
|
|
2442
2628
|
label="Destination Attributes",
|
|
2443
2629
|
section=SectionChoices.RIGHT_HALF,
|
|
2444
2630
|
weight=200,
|
|
@@ -2597,6 +2783,7 @@ class SecretUIViewSet(
|
|
|
2597
2783
|
table_title="Groups containing this secret",
|
|
2598
2784
|
table_class=tables.SecretsGroupTable,
|
|
2599
2785
|
table_attribute="secrets_groups",
|
|
2786
|
+
distinct=True,
|
|
2600
2787
|
related_field_name="secrets",
|
|
2601
2788
|
footer_content_template_path=None,
|
|
2602
2789
|
),
|
|
@@ -2644,7 +2831,7 @@ class SecretsGroupUIViewSet(NautobotUIViewSet):
|
|
|
2644
2831
|
table_class = tables.SecretsGroupTable
|
|
2645
2832
|
queryset = SecretsGroup.objects.all()
|
|
2646
2833
|
|
|
2647
|
-
object_detail_content = ObjectDetailContent(
|
|
2834
|
+
object_detail_content = object_detail.ObjectDetailContent(
|
|
2648
2835
|
panels=(
|
|
2649
2836
|
object_detail.ObjectFieldsPanel(
|
|
2650
2837
|
label="Secrets Group Details",
|
|
@@ -2712,125 +2899,6 @@ class StaticGroupAssociationUIViewSet(
|
|
|
2712
2899
|
return queryset
|
|
2713
2900
|
|
|
2714
2901
|
|
|
2715
|
-
class DynamicGroupBulkAssignView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|
2716
|
-
queryset = StaticGroupAssociation.objects.all()
|
|
2717
|
-
form_class = forms.DynamicGroupBulkAssignForm
|
|
2718
|
-
|
|
2719
|
-
def get_required_permission(self):
|
|
2720
|
-
return get_permission_for_model(self.queryset.model, "add")
|
|
2721
|
-
|
|
2722
|
-
def get(self, request):
|
|
2723
|
-
return redirect(self.get_return_url(request))
|
|
2724
|
-
|
|
2725
|
-
def post(self, request, **kwargs):
|
|
2726
|
-
"""
|
|
2727
|
-
Update the static group assignments of the provided `pk_list` (or `_all`) of the given `content_type`.
|
|
2728
|
-
|
|
2729
|
-
Unlike BulkEditView, this takes a single POST rather than two to perform its operation as
|
|
2730
|
-
there's no separate confirmation step involved.
|
|
2731
|
-
"""
|
|
2732
|
-
# TODO more error handling - content-type doesn't exist, model_class not found, filterset missing, etc.
|
|
2733
|
-
content_type = ContentType.objects.get(pk=request.POST.get("content_type"))
|
|
2734
|
-
model = content_type.model_class()
|
|
2735
|
-
self.default_return_url = get_route_for_model(model, "list")
|
|
2736
|
-
filterset_class = get_filterset_for_model(model)
|
|
2737
|
-
|
|
2738
|
-
if request.POST.get("_all"):
|
|
2739
|
-
if filterset_class is not None:
|
|
2740
|
-
pk_list = list(filterset_class(request.GET, model.objects.only("pk")).qs.values_list("pk", flat=True))
|
|
2741
|
-
else:
|
|
2742
|
-
pk_list = list(model.objects.all().values_list("pk", flat=True))
|
|
2743
|
-
else:
|
|
2744
|
-
pk_list = request.POST.getlist("pk")
|
|
2745
|
-
|
|
2746
|
-
form = self.form_class(model, request.POST)
|
|
2747
|
-
restrict_form_fields(form, request.user)
|
|
2748
|
-
|
|
2749
|
-
if form.is_valid():
|
|
2750
|
-
logger.debug("Form validation was successful")
|
|
2751
|
-
try:
|
|
2752
|
-
with transaction.atomic():
|
|
2753
|
-
add_to_groups = list(form.cleaned_data["add_to_groups"])
|
|
2754
|
-
new_group_name = form.cleaned_data["create_and_assign_to_new_group_name"]
|
|
2755
|
-
if new_group_name:
|
|
2756
|
-
if not request.user.has_perm("extras.add_dynamicgroup"):
|
|
2757
|
-
raise DynamicGroup.DoesNotExist
|
|
2758
|
-
else:
|
|
2759
|
-
new_group = DynamicGroup(
|
|
2760
|
-
name=new_group_name,
|
|
2761
|
-
content_type=content_type,
|
|
2762
|
-
group_type=DynamicGroupTypeChoices.TYPE_STATIC,
|
|
2763
|
-
)
|
|
2764
|
-
new_group.validated_save()
|
|
2765
|
-
# Check permissions
|
|
2766
|
-
DynamicGroup.objects.restrict(request.user, "add").get(pk=new_group.pk)
|
|
2767
|
-
|
|
2768
|
-
add_to_groups.append(new_group)
|
|
2769
|
-
msg = "Created dynamic group"
|
|
2770
|
-
logger.info(f"{msg} {new_group} (PK: {new_group.pk})")
|
|
2771
|
-
msg = format_html('{} <a href="{}">{}</a>', msg, new_group.get_absolute_url(), new_group)
|
|
2772
|
-
messages.success(self.request, msg)
|
|
2773
|
-
|
|
2774
|
-
with deferred_change_logging_for_bulk_operation():
|
|
2775
|
-
associations = []
|
|
2776
|
-
for pk in pk_list:
|
|
2777
|
-
for dynamic_group in add_to_groups:
|
|
2778
|
-
association, created = StaticGroupAssociation.objects.get_or_create(
|
|
2779
|
-
dynamic_group=dynamic_group,
|
|
2780
|
-
associated_object_type_id=content_type.id,
|
|
2781
|
-
associated_object_id=pk,
|
|
2782
|
-
)
|
|
2783
|
-
association.validated_save()
|
|
2784
|
-
associations.append(association)
|
|
2785
|
-
if created:
|
|
2786
|
-
logger.debug("Created %s", association)
|
|
2787
|
-
|
|
2788
|
-
# Enforce object-level permissions
|
|
2789
|
-
if self.queryset.filter(pk__in=[assoc.pk for assoc in associations]).count() != len(
|
|
2790
|
-
associations
|
|
2791
|
-
):
|
|
2792
|
-
raise StaticGroupAssociation.DoesNotExist
|
|
2793
|
-
|
|
2794
|
-
if associations:
|
|
2795
|
-
msg = (
|
|
2796
|
-
f"Added {len(pk_list)} {model._meta.verbose_name_plural} "
|
|
2797
|
-
f"to {len(add_to_groups)} dynamic group(s)."
|
|
2798
|
-
)
|
|
2799
|
-
logger.info(msg)
|
|
2800
|
-
messages.success(self.request, msg)
|
|
2801
|
-
|
|
2802
|
-
if form.cleaned_data["remove_from_groups"]:
|
|
2803
|
-
for dynamic_group in form.cleaned_data["remove_from_groups"]:
|
|
2804
|
-
(
|
|
2805
|
-
StaticGroupAssociation.objects.restrict(request.user, "delete")
|
|
2806
|
-
.filter(
|
|
2807
|
-
dynamic_group=dynamic_group,
|
|
2808
|
-
associated_object_type=content_type,
|
|
2809
|
-
associated_object_id__in=pk_list,
|
|
2810
|
-
)
|
|
2811
|
-
.delete()
|
|
2812
|
-
)
|
|
2813
|
-
|
|
2814
|
-
msg = (
|
|
2815
|
-
f"Removed {len(pk_list)} {model._meta.verbose_name_plural} from "
|
|
2816
|
-
f"{len(form.cleaned_data['remove_from_groups'])} dynamic group(s)."
|
|
2817
|
-
)
|
|
2818
|
-
logger.info(msg)
|
|
2819
|
-
messages.success(self.request, msg)
|
|
2820
|
-
except ValidationError as e:
|
|
2821
|
-
messages.error(self.request, e)
|
|
2822
|
-
except ObjectDoesNotExist:
|
|
2823
|
-
msg = "Static group association failed due to object-level permissions violation"
|
|
2824
|
-
logger.warning(msg)
|
|
2825
|
-
messages.error(self.request, msg)
|
|
2826
|
-
|
|
2827
|
-
else:
|
|
2828
|
-
logger.debug("Form validation failed")
|
|
2829
|
-
messages.error(self.request, form.errors)
|
|
2830
|
-
|
|
2831
|
-
return redirect(self.get_return_url(request))
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
2902
|
#
|
|
2835
2903
|
# Custom statuses
|
|
2836
2904
|
#
|
|
@@ -2955,33 +3023,33 @@ class WebhookUIViewSet(NautobotUIViewSet):
|
|
|
2955
3023
|
serializer_class = serializers.WebhookSerializer
|
|
2956
3024
|
table_class = tables.WebhookTable
|
|
2957
3025
|
|
|
2958
|
-
object_detail_content = ObjectDetailContent(
|
|
3026
|
+
object_detail_content = object_detail.ObjectDetailContent(
|
|
2959
3027
|
panels=[
|
|
2960
|
-
ObjectFieldsPanel(
|
|
3028
|
+
object_detail.ObjectFieldsPanel(
|
|
2961
3029
|
label="Webhook",
|
|
2962
3030
|
section=SectionChoices.LEFT_HALF,
|
|
2963
3031
|
weight=100,
|
|
2964
3032
|
fields=("name", "content_types", "type_create", "type_update", "type_delete", "enabled"),
|
|
2965
3033
|
),
|
|
2966
|
-
ObjectFieldsPanel(
|
|
3034
|
+
object_detail.ObjectFieldsPanel(
|
|
2967
3035
|
label="HTTP",
|
|
2968
3036
|
section=SectionChoices.LEFT_HALF,
|
|
2969
3037
|
weight=100,
|
|
2970
3038
|
fields=("http_method", "http_content_type", "payload_url", "additional_headers"),
|
|
2971
3039
|
value_transforms={"additional_headers": [helpers.pre_tag]},
|
|
2972
3040
|
),
|
|
2973
|
-
ObjectFieldsPanel(
|
|
3041
|
+
object_detail.ObjectFieldsPanel(
|
|
2974
3042
|
label="Security",
|
|
2975
3043
|
section=SectionChoices.LEFT_HALF,
|
|
2976
3044
|
weight=100,
|
|
2977
3045
|
fields=("secret", "ssl_verification", "ca_file_path"),
|
|
2978
3046
|
),
|
|
2979
|
-
ObjectTextPanel(
|
|
3047
|
+
object_detail.ObjectTextPanel(
|
|
2980
3048
|
label="Body Template",
|
|
2981
3049
|
section=SectionChoices.RIGHT_HALF,
|
|
2982
3050
|
weight=100,
|
|
2983
3051
|
object_field="body_template",
|
|
2984
|
-
render_as=BaseTextPanel.RenderOptions.CODE,
|
|
3052
|
+
render_as=object_detail.BaseTextPanel.RenderOptions.CODE,
|
|
2985
3053
|
),
|
|
2986
3054
|
]
|
|
2987
3055
|
)
|