nautobot 2.2.8__py3-none-any.whl → 2.3.0__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/forms.py +4 -0
- nautobot/apps/models.py +10 -1
- nautobot/circuits/__init__.py +0 -1
- nautobot/circuits/apps.py +1 -0
- nautobot/circuits/factory.py +15 -3
- nautobot/circuits/filters.py +13 -0
- nautobot/circuits/forms.py +13 -0
- nautobot/circuits/migrations/0021_alter_circuit_status_alter_circuittermination__path.py +32 -0
- nautobot/circuits/migrations/0022_circuittermination_cloud_network.py +25 -0
- nautobot/circuits/models.py +16 -3
- nautobot/circuits/tables.py +16 -2
- nautobot/circuits/templates/circuits/circuittermination_create.html +10 -2
- nautobot/circuits/templates/circuits/circuittermination_retrieve.html +6 -0
- nautobot/circuits/templates/circuits/inc/circuit_termination.html +6 -1
- nautobot/circuits/tests/test_api.py +7 -5
- nautobot/circuits/tests/test_filters.py +12 -5
- nautobot/circuits/tests/test_models.py +33 -2
- nautobot/circuits/views.py +2 -3
- nautobot/cloud/__init__.py +0 -0
- nautobot/cloud/api/__init__.py +0 -0
- nautobot/cloud/api/serializers.py +54 -0
- nautobot/cloud/api/urls.py +16 -0
- nautobot/cloud/api/views.py +48 -0
- nautobot/cloud/apps.py +13 -0
- nautobot/cloud/factory.py +113 -0
- nautobot/cloud/filters.py +187 -0
- nautobot/cloud/forms.py +339 -0
- nautobot/cloud/homepage.py +43 -0
- nautobot/cloud/migrations/0001_initial.py +304 -0
- nautobot/cloud/migrations/__init__.py +0 -0
- nautobot/cloud/models.py +246 -0
- nautobot/cloud/navigation.py +85 -0
- nautobot/cloud/tables.py +157 -0
- nautobot/cloud/templates/cloud/cloudaccount_retrieve.html +43 -0
- nautobot/cloud/templates/cloud/cloudnetwork_retrieve.html +122 -0
- nautobot/cloud/templates/cloud/cloudnetwork_update.html +33 -0
- nautobot/cloud/templates/cloud/cloudresourcetype_retrieve.html +111 -0
- nautobot/cloud/templates/cloud/cloudservice_retrieve.html +69 -0
- nautobot/cloud/templates/cloud/cloudservice_update.html +25 -0
- nautobot/cloud/tests/__init__.py +0 -0
- nautobot/cloud/tests/test_api.py +248 -0
- nautobot/cloud/tests/test_filters.py +125 -0
- nautobot/cloud/tests/test_models.py +43 -0
- nautobot/cloud/tests/test_views.py +153 -0
- nautobot/cloud/urls.py +14 -0
- nautobot/cloud/views.py +181 -0
- nautobot/core/__init__.py +0 -3
- nautobot/core/api/metadata.py +1 -0
- nautobot/core/api/parsers.py +7 -1
- nautobot/core/api/urls.py +1 -0
- nautobot/core/api/utils.py +1 -0
- nautobot/core/api/views.py +4 -0
- nautobot/core/apps/__init__.py +6 -3
- nautobot/core/constants.py +8 -0
- nautobot/core/factory.py +32 -1
- nautobot/core/filters.py +110 -14
- nautobot/core/forms/fields.py +10 -4
- nautobot/core/forms/forms.py +11 -3
- nautobot/core/forms/widgets.py +18 -1
- nautobot/core/graphql/generators.py +2 -2
- nautobot/core/graphql/schema.py +28 -7
- nautobot/core/jobs/__init__.py +20 -3
- nautobot/core/jobs/cleanup.py +100 -0
- nautobot/core/jobs/groups.py +38 -0
- nautobot/core/management/commands/generate_test_data.py +116 -3
- nautobot/core/models/__init__.py +34 -9
- nautobot/core/models/generics.py +19 -3
- nautobot/core/models/name_color_content_types.py +7 -28
- nautobot/core/models/querysets.py +4 -3
- nautobot/core/models/tree_queries.py +1 -1
- nautobot/core/models/utils.py +21 -5
- nautobot/core/settings.py +15 -19
- nautobot/core/settings.yaml +48 -13
- nautobot/core/settings_funcs.py +103 -0
- nautobot/core/tables.py +130 -56
- nautobot/core/templates/admin/search_form.html +1 -1
- nautobot/core/templates/buttons/add.html +11 -3
- nautobot/core/templates/buttons/consolidated_bulk_action_buttons.html +13 -0
- nautobot/core/templates/buttons/consolidated_detail_view_action_buttons.html +13 -0
- nautobot/core/templates/buttons/export.html +101 -53
- nautobot/core/templates/buttons/job_import.html +11 -3
- nautobot/core/templates/generic/object_bulk_destroy.html +3 -1
- nautobot/core/templates/generic/object_bulk_update.html +3 -1
- nautobot/core/templates/generic/object_changelog.html +0 -9
- nautobot/core/templates/generic/object_list.html +156 -17
- nautobot/core/templates/generic/object_retrieve.html +80 -16
- nautobot/core/templates/inc/extras_features_edit_form_fields.html +8 -0
- nautobot/core/templates/inc/javascript.html +2 -0
- nautobot/core/templates/inc/media.html +2 -2
- nautobot/core/templates/inc/nav_menu.html +1 -0
- nautobot/core/templates/inc/paginator.html +7 -7
- nautobot/core/templates/inc/search_panel.html +2 -2
- nautobot/core/templates/inc/table.html +2 -2
- nautobot/core/templates/nautobot_config.py.j2 +28 -8
- nautobot/core/templates/utilities/templatetags/dynamic_group_assignment_modal.html +37 -0
- nautobot/core/templates/utilities/templatetags/filter_form_modal.html +2 -2
- nautobot/core/templates/utilities/templatetags/saved_view_modal.html +38 -0
- nautobot/core/templates/utilities/theme_preview.html +25 -8
- nautobot/core/templates/utilities/worker_status.html +152 -0
- nautobot/core/templatetags/buttons.py +335 -38
- nautobot/core/templatetags/form_helpers.py +1 -1
- nautobot/core/templatetags/helpers.py +181 -11
- nautobot/core/testing/api.py +5 -4
- nautobot/core/testing/filters.py +63 -14
- nautobot/core/testing/mixins.py +46 -0
- nautobot/core/testing/models.py +22 -0
- nautobot/core/testing/schema.py +4 -8
- nautobot/core/testing/views.py +31 -14
- nautobot/core/tests/integration/test_general_functionality.py +1 -1
- nautobot/core/tests/integration/test_import_objects_ui.py +1 -0
- nautobot/core/tests/integration/test_swagger.py +1 -1
- nautobot/core/tests/nautobot_config.py +0 -1
- nautobot/core/tests/runner.py +2 -2
- nautobot/core/tests/test_api.py +1 -0
- nautobot/core/tests/test_authentication.py +7 -2
- nautobot/core/tests/test_filters.py +11 -9
- nautobot/core/tests/test_forms.py +9 -0
- nautobot/core/tests/test_graphql.py +27 -16
- nautobot/core/tests/test_jobs.py +204 -2
- nautobot/core/tests/test_tables.py +3 -1
- nautobot/core/tests/test_templatetags_helpers.py +12 -5
- nautobot/core/tests/test_templatetags_netutils.py +3 -3
- nautobot/core/tests/test_utils.py +31 -20
- nautobot/core/tests/test_views.py +6 -6
- nautobot/core/urls.py +8 -3
- nautobot/core/utils/deprecation.py +29 -0
- nautobot/core/utils/filtering.py +12 -9
- nautobot/core/utils/lookup.py +37 -2
- nautobot/core/utils/requests.py +4 -1
- nautobot/core/views/__init__.py +137 -24
- nautobot/core/views/generic.py +119 -67
- nautobot/core/views/mixins.py +105 -36
- nautobot/core/views/paginator.py +9 -3
- nautobot/core/views/renderers.py +121 -56
- nautobot/core/views/utils.py +81 -1
- nautobot/dcim/__init__.py +0 -1
- nautobot/dcim/api/serializers.py +180 -44
- nautobot/dcim/api/urls.py +7 -3
- nautobot/dcim/api/views.py +53 -7
- nautobot/dcim/apps.py +3 -0
- nautobot/dcim/choices.py +25 -0
- nautobot/dcim/constants.py +7 -0
- nautobot/dcim/factory.py +252 -18
- nautobot/dcim/filters/__init__.py +373 -193
- nautobot/dcim/filters/mixins.py +274 -1
- nautobot/dcim/forms.py +834 -121
- nautobot/dcim/graphql/types.py +2 -2
- nautobot/dcim/homepage.py +1 -1
- nautobot/dcim/migrations/0059_add_role_field_to_interface_models.py +27 -0
- nautobot/dcim/migrations/0060_alter_cable_status_alter_consoleport__path_and_more.py +303 -0
- nautobot/dcim/migrations/0061_module_models.py +862 -0
- nautobot/dcim/migrations/0062_module_data_migration.py +25 -0
- nautobot/dcim/models/__init__.py +8 -0
- nautobot/dcim/models/cables.py +15 -0
- nautobot/dcim/models/device_component_templates.py +207 -53
- nautobot/dcim/models/device_components.py +282 -99
- nautobot/dcim/models/devices.py +472 -13
- nautobot/dcim/models/racks.py +0 -1
- nautobot/dcim/navigation.py +47 -0
- nautobot/dcim/signals.py +3 -3
- nautobot/dcim/tables/__init__.py +35 -23
- nautobot/dcim/tables/devices.py +248 -47
- nautobot/dcim/tables/devicetypes.py +65 -9
- nautobot/dcim/tables/racks.py +5 -1
- nautobot/dcim/tables/template_code.py +46 -26
- nautobot/dcim/templates/dcim/cable_connect.html +76 -3
- nautobot/dcim/templates/dcim/console_port_connection_list.html +7 -5
- nautobot/dcim/templates/dcim/device/base.html +14 -6
- nautobot/dcim/templates/dcim/device/consoleports.html +2 -3
- nautobot/dcim/templates/dcim/device/consoleserverports.html +2 -3
- nautobot/dcim/templates/dcim/device/devicebays.html +6 -7
- nautobot/dcim/templates/dcim/device/frontports.html +2 -3
- nautobot/dcim/templates/dcim/device/interfaces.html +2 -3
- nautobot/dcim/templates/dcim/device/inventory.html +2 -3
- nautobot/dcim/templates/dcim/device/modulebays.html +49 -0
- nautobot/dcim/templates/dcim/device/poweroutlets.html +2 -3
- nautobot/dcim/templates/dcim/device/powerports.html +2 -3
- nautobot/dcim/templates/dcim/device/rearports.html +2 -3
- nautobot/dcim/templates/dcim/device.html +45 -1
- nautobot/dcim/templates/dcim/device_component.html +13 -5
- nautobot/dcim/templates/dcim/device_list.html +2 -1
- nautobot/dcim/templates/dcim/deviceredundancygroup_retrieve.html +6 -0
- nautobot/dcim/templates/dcim/devicetype.html +99 -98
- nautobot/dcim/templates/dcim/devicetype_list.html +8 -16
- nautobot/dcim/templates/dcim/inc/devicetype_component_table.html +1 -1
- nautobot/dcim/templates/dcim/inc/moduletype_component_table.html +39 -0
- nautobot/dcim/templates/dcim/interface.html +17 -2
- nautobot/dcim/templates/dcim/interface_connection_list.html +7 -5
- nautobot/dcim/templates/dcim/interface_edit.html +1 -0
- nautobot/dcim/templates/dcim/manufacturer.html +24 -0
- nautobot/dcim/templates/dcim/module/base.html +97 -0
- nautobot/dcim/templates/dcim/module_bulk_destroy.html +5 -0
- nautobot/dcim/templates/dcim/module_consoleports.html +53 -0
- nautobot/dcim/templates/dcim/module_consoleserverports.html +53 -0
- nautobot/dcim/templates/dcim/module_destroy.html +5 -0
- nautobot/dcim/templates/dcim/module_frontports.html +53 -0
- nautobot/dcim/templates/dcim/module_interfaces.html +57 -0
- nautobot/dcim/templates/dcim/module_list.html +20 -0
- nautobot/dcim/templates/dcim/module_modulebays.html +49 -0
- nautobot/dcim/templates/dcim/module_poweroutlets.html +53 -0
- nautobot/dcim/templates/dcim/module_powerports.html +53 -0
- nautobot/dcim/templates/dcim/module_rearports.html +53 -0
- nautobot/dcim/templates/dcim/module_retrieve.html +63 -0
- nautobot/dcim/templates/dcim/module_update.html +71 -0
- nautobot/dcim/templates/dcim/modulebay_bulk_destroy.html +5 -0
- nautobot/dcim/templates/dcim/modulebay_destroy.html +8 -0
- nautobot/dcim/templates/dcim/modulebay_retrieve.html +101 -0
- nautobot/dcim/templates/dcim/moduletype_list.html +11 -0
- nautobot/dcim/templates/dcim/moduletype_retrieve.html +159 -0
- nautobot/dcim/templates/dcim/power_port_connection_list.html +7 -5
- nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +65 -19
- nautobot/dcim/tests/integration/test_cable_connect_form.py +4 -4
- nautobot/dcim/tests/test_api.py +693 -208
- nautobot/dcim/tests/test_filters.py +843 -217
- nautobot/dcim/tests/test_models.py +1103 -8
- nautobot/dcim/tests/test_views.py +1525 -343
- nautobot/dcim/urls.py +17 -2
- nautobot/dcim/utils.py +2 -3
- nautobot/dcim/views.py +1109 -113
- nautobot/extras/__init__.py +0 -1
- nautobot/extras/api/serializers.py +115 -3
- nautobot/extras/api/urls.py +12 -0
- nautobot/extras/api/views.py +73 -59
- nautobot/extras/apps.py +2 -2
- nautobot/extras/choices.py +43 -0
- nautobot/extras/context_managers.py +13 -8
- nautobot/extras/datasources/git.py +2 -0
- nautobot/extras/factory.py +460 -9
- nautobot/extras/filters/__init__.py +174 -3
- nautobot/extras/filters/mixins.py +46 -43
- nautobot/extras/forms/base.py +24 -5
- nautobot/extras/forms/forms.py +227 -8
- nautobot/extras/forms/mixins.py +93 -0
- nautobot/extras/graphql/types.py +23 -10
- nautobot/extras/homepage.py +26 -3
- nautobot/extras/jobs.py +2 -2
- nautobot/extras/management/__init__.py +1 -0
- nautobot/extras/management/commands/refresh_dynamic_group_member_caches.py +1 -16
- nautobot/extras/migrations/0021_customfield_changelog_data.py +1 -0
- nautobot/extras/migrations/0109_dynamicgroup_group_type_dynamicgroup_tags_and_more.py +108 -0
- nautobot/extras/migrations/0110_alter_configcontext_cluster_groups_and_more.py +111 -0
- nautobot/extras/migrations/0111_metadata.py +162 -0
- nautobot/extras/migrations/0112_dynamic_group_group_type_data_migration.py +28 -0
- nautobot/extras/migrations/0113_saved_views.py +77 -0
- nautobot/extras/models/__init__.py +15 -1
- nautobot/extras/models/change_logging.py +3 -3
- nautobot/extras/models/contacts.py +4 -0
- nautobot/extras/models/customfields.py +18 -3
- nautobot/extras/models/groups.py +389 -225
- nautobot/extras/models/jobs.py +87 -3
- nautobot/extras/models/metadata.py +441 -0
- nautobot/extras/models/mixins.py +72 -62
- nautobot/extras/models/models.py +118 -9
- nautobot/extras/models/relationships.py +9 -2
- nautobot/extras/models/tags.py +13 -2
- nautobot/extras/navigation.py +57 -0
- nautobot/extras/plugins/__init__.py +3 -1
- nautobot/extras/querysets.py +30 -66
- nautobot/extras/signals.py +109 -101
- nautobot/extras/tables.py +201 -17
- nautobot/extras/templates/extras/dynamicgroup.html +44 -15
- nautobot/extras/templates/extras/dynamicgroup_edit.html +2 -0
- nautobot/extras/templates/extras/job.html +1 -1
- nautobot/extras/templates/extras/job_detail.html +11 -0
- nautobot/extras/templates/extras/jobresult.html +61 -74
- nautobot/extras/templates/extras/metadatatype_create.html +89 -0
- nautobot/extras/templates/extras/metadatatype_retrieve.html +67 -0
- nautobot/extras/templates/extras/object_dynamicgroups.html +7 -0
- nautobot/extras/templates/extras/objectchange_list.html +0 -12
- nautobot/extras/templates/extras/plugins_list.html +1 -3
- nautobot/extras/templates/extras/role_retrieve.html +48 -0
- nautobot/extras/templates/extras/staticgroupassociation_retrieve.html +20 -0
- nautobot/extras/tests/integration/test_customfields.py +1 -0
- nautobot/extras/tests/test_api.py +509 -23
- nautobot/extras/tests/test_changelog.py +20 -9
- nautobot/extras/tests/test_context_managers.py +22 -15
- nautobot/extras/tests/test_datasources.py +13 -1
- nautobot/extras/tests/test_dynamicgroups.py +201 -171
- nautobot/extras/tests/test_filters.py +211 -12
- nautobot/extras/tests/test_jobs.py +6 -6
- nautobot/extras/tests/test_models.py +501 -4
- nautobot/extras/tests/test_relationships.py +1 -0
- nautobot/extras/tests/test_views.py +586 -8
- nautobot/extras/tests/test_webhooks.py +1 -1
- nautobot/extras/urls.py +5 -0
- nautobot/extras/utils.py +85 -16
- nautobot/extras/views.py +562 -122
- nautobot/ipam/__init__.py +0 -1
- nautobot/ipam/apps.py +1 -0
- nautobot/ipam/factory.py +17 -19
- nautobot/ipam/filters.py +13 -0
- nautobot/ipam/forms.py +8 -4
- nautobot/ipam/graphql/types.py +2 -2
- nautobot/ipam/migrations/0047_alter_ipaddress_role_alter_ipaddress_status_and_more.py +59 -0
- nautobot/ipam/models.py +20 -20
- nautobot/ipam/querysets.py +1 -1
- nautobot/ipam/signals.py +4 -2
- nautobot/ipam/tables.py +5 -0
- nautobot/ipam/templates/ipam/ipaddress_interfaces.html +1 -1
- nautobot/ipam/templates/ipam/ipaddress_vm_interfaces.html +1 -1
- nautobot/ipam/templates/ipam/prefix.html +1 -0
- nautobot/ipam/tests/test_api.py +37 -18
- nautobot/ipam/tests/test_filters.py +26 -2
- nautobot/ipam/tests/test_models.py +9 -2
- nautobot/ipam/tests/test_querysets.py +1 -1
- nautobot/ipam/tests/test_views.py +3 -2
- nautobot/ipam/urls.py +2 -2
- nautobot/ipam/views.py +20 -34
- nautobot/project-static/css/base.css +21 -0
- nautobot/project-static/css/dark.css +11 -0
- nautobot/project-static/docs/404.html +894 -90
- nautobot/project-static/docs/apps/index.html +894 -90
- nautobot/project-static/docs/apps/nautobot-apps.html +894 -90
- nautobot/project-static/docs/assets/_mkdocstrings.css +5 -0
- nautobot/project-static/docs/assets/stylesheets/main.3cba04c6.min.css +1 -0
- nautobot/project-static/docs/assets/stylesheets/main.3cba04c6.min.css.map +1 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +921 -122
- nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +906 -103
- nautobot/project-static/docs/code-reference/nautobot/apps/api.html +1620 -905
- nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +937 -146
- nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +979 -190
- nautobot/project-static/docs/code-reference/nautobot/apps/config.html +903 -101
- nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +899 -95
- nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +993 -195
- nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +976 -133
- nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +1080 -274
- nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +1244 -336
- nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +1729 -877
- nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +1166 -383
- nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +2090 -1376
- nautobot/project-static/docs/code-reference/nautobot/apps/models.html +2248 -1424
- nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +914 -113
- nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +965 -165
- nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +1012 -225
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +1915 -1279
- nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +1848 -1104
- nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +906 -103
- nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +2335 -1701
- nautobot/project-static/docs/code-reference/nautobot/apps/views.html +1804 -1026
- nautobot/project-static/docs/development/apps/api/configuration-view.html +894 -90
- nautobot/project-static/docs/development/apps/api/database-backend-config.html +894 -90
- nautobot/project-static/docs/development/apps/api/models/django-admin.html +894 -90
- nautobot/project-static/docs/development/apps/api/models/global-search.html +894 -90
- nautobot/project-static/docs/development/apps/api/models/graphql.html +894 -90
- nautobot/project-static/docs/development/apps/api/models/index.html +944 -92
- nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +894 -90
- nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +894 -90
- nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +894 -90
- nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +894 -90
- nautobot/project-static/docs/development/apps/api/platform-features/index.html +894 -90
- nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +894 -90
- nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +894 -90
- nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +894 -90
- nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +894 -90
- nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +894 -90
- nautobot/project-static/docs/development/apps/api/prometheus.html +894 -90
- nautobot/project-static/docs/development/apps/api/setup.html +894 -90
- nautobot/project-static/docs/development/apps/api/testing.html +894 -90
- nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +894 -90
- nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +894 -90
- nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +894 -90
- nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +894 -90
- nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +894 -90
- nautobot/project-static/docs/development/apps/api/views/base-template.html +894 -90
- nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +894 -90
- nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +894 -90
- nautobot/project-static/docs/development/apps/api/views/help-documentation.html +894 -90
- nautobot/project-static/docs/development/apps/api/views/index.html +894 -90
- nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +894 -90
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +894 -90
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +894 -90
- nautobot/project-static/docs/development/apps/api/views/notes.html +894 -90
- nautobot/project-static/docs/development/apps/api/views/rest-api.html +894 -90
- nautobot/project-static/docs/development/apps/api/views/urls.html +894 -90
- nautobot/project-static/docs/development/apps/index.html +894 -90
- nautobot/project-static/docs/development/apps/migration/code-updates.html +894 -90
- nautobot/project-static/docs/development/apps/migration/dependency-updates.html +894 -90
- nautobot/project-static/docs/development/apps/migration/from-v1.html +894 -90
- nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +894 -90
- nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +894 -90
- nautobot/project-static/docs/development/apps/migration/model-updates/global.html +894 -90
- nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +894 -90
- nautobot/project-static/docs/development/apps/porting-from-netbox.html +894 -90
- nautobot/project-static/docs/development/core/application-registry.html +894 -90
- nautobot/project-static/docs/development/core/best-practices.html +895 -90
- nautobot/project-static/docs/development/core/bootstrap-ui.html +894 -90
- nautobot/project-static/docs/development/core/caching.html +894 -90
- nautobot/project-static/docs/development/core/controllers.html +894 -90
- nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +894 -90
- nautobot/project-static/docs/development/core/generic-views.html +894 -90
- nautobot/project-static/docs/development/core/getting-started.html +894 -90
- nautobot/project-static/docs/development/core/homepage.html +894 -90
- nautobot/project-static/docs/development/core/index.html +905 -90
- nautobot/project-static/docs/development/core/model-checklist.html +903 -91
- nautobot/project-static/docs/development/core/model-features.html +894 -90
- nautobot/project-static/docs/development/core/natural-keys.html +894 -90
- nautobot/project-static/docs/development/core/navigation-menu.html +894 -90
- nautobot/project-static/docs/development/core/release-checklist.html +897 -93
- nautobot/project-static/docs/development/core/role-internals.html +894 -90
- nautobot/project-static/docs/development/core/settings.html +894 -90
- nautobot/project-static/docs/development/core/style-guide.html +895 -91
- nautobot/project-static/docs/development/core/templates.html +906 -91
- nautobot/project-static/docs/development/core/testing.html +894 -90
- nautobot/project-static/docs/development/core/user-preferences.html +894 -90
- nautobot/project-static/docs/development/index.html +894 -90
- nautobot/project-static/docs/development/jobs/index.html +1271 -453
- nautobot/project-static/docs/development/jobs/migration/from-v1.html +894 -90
- nautobot/project-static/docs/index.html +9032 -13
- nautobot/project-static/docs/media/models/cloud_aws_direct_connect_dark.png +0 -0
- nautobot/project-static/docs/media/models/cloud_aws_direct_connect_light.png +0 -0
- nautobot/project-static/docs/models/cloud/cloudaccount.html +15 -0
- nautobot/project-static/docs/models/cloud/cloudnetwork.html +15 -0
- nautobot/project-static/docs/models/cloud/cloudnetworkprefixassignment.html +15 -0
- nautobot/project-static/docs/models/cloud/cloudresourcetype.html +15 -0
- nautobot/project-static/docs/models/cloud/cloudservice.html +15 -0
- nautobot/project-static/docs/models/cloud/cloudservicenetworkassignment.html +15 -0
- nautobot/project-static/docs/models/dcim/module.html +15 -0
- nautobot/project-static/docs/models/dcim/modulebay.html +15 -0
- nautobot/project-static/docs/models/dcim/modulebaytemplate.html +15 -0
- nautobot/project-static/docs/models/dcim/moduletype.html +15 -0
- nautobot/project-static/docs/models/extras/metadatachoice.html +15 -0
- nautobot/project-static/docs/models/extras/metadatatype.html +15 -0
- nautobot/project-static/docs/models/extras/objectmetadata.html +15 -0
- nautobot/project-static/docs/models/extras/role.html +15 -0
- nautobot/project-static/docs/models/extras/savedview.html +15 -0
- nautobot/project-static/docs/models/extras/staticgroupassociation.html +15 -0
- nautobot/project-static/docs/models/extras/status.html +15 -0
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/overview/application_stack.html +902 -91
- nautobot/project-static/docs/overview/design_philosophy.html +896 -92
- nautobot/project-static/docs/overview/index.html +13 -8228
- nautobot/project-static/docs/release-notes/index.html +1131 -94
- nautobot/project-static/docs/release-notes/version-1.0.html +894 -90
- nautobot/project-static/docs/release-notes/version-1.1.html +894 -90
- nautobot/project-static/docs/release-notes/version-1.2.html +894 -90
- nautobot/project-static/docs/release-notes/version-1.3.html +894 -90
- nautobot/project-static/docs/release-notes/version-1.4.html +894 -90
- nautobot/project-static/docs/release-notes/version-1.5.html +895 -91
- nautobot/project-static/docs/release-notes/version-1.6.html +895 -91
- nautobot/project-static/docs/release-notes/version-2.0.html +894 -90
- nautobot/project-static/docs/release-notes/version-2.1.html +894 -90
- nautobot/project-static/docs/release-notes/version-2.2.html +1137 -196
- nautobot/project-static/docs/release-notes/version-2.3.html +9954 -0
- nautobot/project-static/docs/requirements.txt +5 -5
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +335 -260
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +894 -90
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +894 -90
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +894 -90
- nautobot/project-static/docs/user-guide/administration/configuration/index.html +894 -90
- nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +1025 -175
- nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +894 -90
- nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +894 -90
- nautobot/project-static/docs/user-guide/administration/guides/caching.html +894 -90
- nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +902 -90
- nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +894 -90
- nautobot/project-static/docs/user-guide/administration/guides/permissions.html +894 -90
- nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +894 -90
- nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +894 -90
- nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +894 -90
- nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +894 -90
- nautobot/project-static/docs/user-guide/administration/installation/app-install.html +894 -90
- nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +894 -90
- nautobot/project-static/docs/user-guide/administration/installation/http-server.html +946 -155
- nautobot/project-static/docs/user-guide/administration/installation/index.html +903 -95
- nautobot/project-static/docs/user-guide/administration/installation/install_system.html +936 -124
- nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +956 -159
- nautobot/project-static/docs/user-guide/administration/installation/services.html +915 -114
- nautobot/project-static/docs/user-guide/administration/installation-extras/docker.html +910 -101
- nautobot/project-static/docs/user-guide/administration/installation-extras/health-checks.html +894 -90
- nautobot/project-static/docs/user-guide/administration/installation-extras/selinux-troubleshooting.html +894 -90
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +894 -90
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +894 -90
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +977 -121
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +894 -90
- nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +894 -90
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +894 -90
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +894 -90
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +894 -90
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +894 -90
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +894 -90
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +894 -90
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +894 -90
- nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +894 -90
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +894 -90
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +895 -91
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +894 -90
- nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +898 -90
- nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +897 -93
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +8984 -0
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +8828 -0
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +8829 -0
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +8828 -0
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +8829 -0
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +8833 -0
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +8828 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +908 -104
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +925 -107
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +925 -107
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +920 -102
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +925 -107
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +908 -104
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +908 -104
- nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +915 -107
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +922 -118
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +923 -119
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +920 -116
- nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +908 -104
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +916 -107
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +928 -110
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +938 -120
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +930 -108
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +908 -104
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +939 -121
- nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +930 -112
- nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +920 -116
- nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +923 -119
- nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +925 -117
- nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +8828 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +8846 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +8843 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +8823 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +918 -114
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +908 -104
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +942 -85
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +926 -108
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +908 -104
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +945 -88
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +923 -105
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +931 -127
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +920 -116
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +908 -104
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +924 -106
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +926 -108
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +908 -104
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +908 -104
- nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +908 -104
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +938 -90
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +894 -90
- nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +899 -91
- nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +899 -91
- nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +894 -90
- nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +894 -90
- nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +894 -90
- nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +894 -90
- nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +894 -90
- nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +894 -90
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +894 -90
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +894 -90
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +894 -90
- nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +894 -90
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +903 -98
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +894 -90
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +894 -90
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +894 -90
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +894 -90
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +894 -90
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +899 -91
- nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +894 -90
- nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +894 -90
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +894 -90
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +894 -90
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +894 -90
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +894 -90
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +894 -90
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +894 -90
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +894 -90
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +894 -90
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +894 -90
- nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +894 -90
- nautobot/project-static/docs/user-guide/feature-guides/graphql.html +894 -90
- nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/clear-view-button.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/cleared-view.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/config-table-columns-to-locations.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/configure-button.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/create-saved-view-success.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/current-saved-view-drop-down-menu.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/default-location-list-view.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/dropdown-button-after-new-saved-view.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/filter-application-to-locations.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/filter-button.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/global-default-location-list-view.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/location-list-view-with-saved-views.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/navigation-menu.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/save-as-new-view-drop-down.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/save-view-modal.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-buttons.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-success.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-view-unchecked.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-view.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-different-user.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-modal-unchecked.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/set-as-my-default-button.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/set-as-my-default-success.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/unsaved-saved-view.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/updated-saved-view.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +894 -90
- nautobot/project-static/docs/user-guide/feature-guides/relationships.html +894 -90
- nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +894 -90
- nautobot/project-static/docs/user-guide/index.html +894 -90
- nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +894 -90
- nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +894 -90
- nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +894 -90
- nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +894 -90
- nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +1260 -787
- nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +897 -93
- nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +894 -90
- nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +894 -90
- nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +894 -90
- nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +894 -90
- nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +894 -90
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +894 -90
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +894 -90
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +894 -90
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +894 -90
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +898 -90
- nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +894 -90
- nautobot/project-static/docs/user-guide/platform-functionality/note.html +897 -93
- nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +9061 -0
- nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +897 -93
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +894 -90
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +894 -90
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +894 -90
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +894 -90
- nautobot/project-static/docs/user-guide/platform-functionality/role.html +897 -93
- nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +9137 -0
- nautobot/project-static/docs/user-guide/platform-functionality/secret.html +897 -93
- nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +8933 -0
- nautobot/project-static/docs/user-guide/platform-functionality/status.html +894 -90
- nautobot/project-static/docs/user-guide/platform-functionality/tag.html +894 -90
- nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +952 -123
- nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +894 -90
- nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +894 -90
- nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +894 -90
- nautobot/project-static/js/forms.js +71 -0
- nautobot/project-static/js/table_sorting_indicator.js +46 -0
- nautobot/project-static/js/tableconfig.js +6 -1
- nautobot/project-static/materialdesignicons-7.4.47/css/materialdesignicons.min.css +3 -0
- nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/fonts/materialdesignicons-webfont.eot +0 -0
- nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/fonts/materialdesignicons-webfont.ttf +0 -0
- nautobot/project-static/materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff +0 -0
- nautobot/project-static/materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff2 +0 -0
- nautobot/tenancy/__init__.py +0 -1
- nautobot/tenancy/apps.py +1 -0
- nautobot/tenancy/factory.py +3 -2
- nautobot/tenancy/filters/__init__.py +1 -0
- nautobot/tenancy/forms.py +1 -1
- nautobot/tenancy/templates/tenancy/tenant.html +24 -20
- nautobot/tenancy/views.py +11 -10
- nautobot/users/__init__.py +0 -1
- nautobot/users/api/serializers.py +1 -1
- nautobot/users/api/views.py +4 -2
- nautobot/users/apps.py +3 -2
- nautobot/users/factory.py +3 -3
- nautobot/users/migrations/0010_user_default_saved_views.py +20 -0
- nautobot/users/models.py +12 -0
- nautobot/users/tests/test_filters.py +6 -3
- nautobot/users/urls.py +8 -0
- nautobot/virtualization/__init__.py +0 -1
- nautobot/virtualization/apps.py +1 -0
- nautobot/virtualization/filters.py +6 -1
- nautobot/virtualization/forms.py +11 -3
- nautobot/virtualization/graphql/types.py +2 -2
- nautobot/virtualization/migrations/0029_add_role_field_to_interface_models.py +27 -0
- nautobot/virtualization/migrations/0030_alter_virtualmachine_local_config_context_data_owner_content_type_and_more.py +67 -0
- nautobot/virtualization/models.py +0 -2
- nautobot/virtualization/tables.py +12 -8
- nautobot/virtualization/templates/virtualization/virtualmachine.html +1 -1
- nautobot/virtualization/templates/virtualization/vminterface.html +7 -1
- nautobot/virtualization/templates/virtualization/vminterface_edit.html +1 -0
- nautobot/virtualization/tests/test_api.py +9 -4
- nautobot/virtualization/tests/test_filters.py +22 -0
- nautobot/virtualization/tests/test_models.py +7 -3
- nautobot/virtualization/tests/test_views.py +19 -3
- nautobot/virtualization/urls.py +2 -2
- nautobot/virtualization/views.py +10 -32
- {nautobot-2.2.8.dist-info → nautobot-2.3.0.dist-info}/METADATA +21 -19
- {nautobot-2.2.8.dist-info → nautobot-2.3.0.dist-info}/RECORD +684 -564
- nautobot/project-static/docs/assets/stylesheets/main.76a95c52.min.css +0 -1
- nautobot/project-static/docs/assets/stylesheets/main.76a95c52.min.css.map +0 -1
- nautobot/project-static/materialdesignicons-6.5.95/.github/ISSUE_TEMPLATE.md +0 -3
- nautobot/project-static/materialdesignicons-6.5.95/README.md +0 -25
- nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.css +0 -26654
- nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.css.map +0 -16
- nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.min.css +0 -3
- nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.min.css.map +0 -16
- nautobot/project-static/materialdesignicons-6.5.95/fonts/materialdesignicons-webfont.woff +0 -0
- nautobot/project-static/materialdesignicons-6.5.95/fonts/materialdesignicons-webfont.woff2 +0 -0
- nautobot/project-static/materialdesignicons-6.5.95/package.json +0 -28
- nautobot/project-static/materialdesignicons-6.5.95/preview.html +0 -717
- nautobot/project-static/materialdesignicons-6.5.95/scss/_animated.scss +0 -27
- nautobot/project-static/materialdesignicons-6.5.95/scss/_core.scss +0 -10
- nautobot/project-static/materialdesignicons-6.5.95/scss/_extras.scss +0 -65
- nautobot/project-static/materialdesignicons-6.5.95/scss/_functions.scss +0 -20
- nautobot/project-static/materialdesignicons-6.5.95/scss/_icons.scss +0 -10
- nautobot/project-static/materialdesignicons-6.5.95/scss/_path.scss +0 -10
- nautobot/project-static/materialdesignicons-6.5.95/scss/_variables.scss +0 -6606
- nautobot/project-static/materialdesignicons-6.5.95/scss/materialdesignicons.scss +0 -8
- /nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/LICENSE +0 -0
- {nautobot-2.2.8.dist-info → nautobot-2.3.0.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.2.8.dist-info → nautobot-2.3.0.dist-info}/NOTICE +0 -0
- {nautobot-2.2.8.dist-info → nautobot-2.3.0.dist-info}/WHEEL +0 -0
- {nautobot-2.2.8.dist-info → nautobot-2.3.0.dist-info}/entry_points.txt +0 -0
nautobot/extras/models/groups.py
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
"""Dynamic Groups Models."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
import pickle
|
|
5
4
|
|
|
6
5
|
from django import forms
|
|
6
|
+
from django.contrib.contenttypes.fields import GenericForeignKey
|
|
7
7
|
from django.contrib.contenttypes.models import ContentType
|
|
8
|
-
from django.core.cache import cache
|
|
9
8
|
from django.core.exceptions import ValidationError
|
|
10
9
|
from django.core.serializers.json import DjangoJSONEncoder
|
|
11
10
|
from django.db import models
|
|
@@ -17,12 +16,14 @@ from nautobot.core.forms.constants import BOOLEAN_WITH_BLANK_CHOICES
|
|
|
17
16
|
from nautobot.core.forms.fields import DynamicModelChoiceField
|
|
18
17
|
from nautobot.core.forms.widgets import StaticSelect2
|
|
19
18
|
from nautobot.core.models import BaseManager, BaseModel
|
|
20
|
-
from nautobot.core.models.generics import OrganizationalModel
|
|
21
|
-
from nautobot.core.
|
|
19
|
+
from nautobot.core.models.generics import OrganizationalModel, PrimaryModel
|
|
20
|
+
from nautobot.core.models.querysets import RestrictedQuerySet
|
|
21
|
+
from nautobot.core.utils.data import is_uuid
|
|
22
|
+
from nautobot.core.utils.deprecation import method_deprecated, method_deprecated_in_favor_of
|
|
22
23
|
from nautobot.core.utils.lookup import get_filterset_for_model, get_form_for_model
|
|
23
|
-
from nautobot.extras.choices import DynamicGroupOperatorChoices
|
|
24
|
+
from nautobot.extras.choices import DynamicGroupOperatorChoices, DynamicGroupTypeChoices
|
|
24
25
|
from nautobot.extras.querysets import DynamicGroupMembershipQuerySet, DynamicGroupQuerySet
|
|
25
|
-
from nautobot.extras.utils import extras_features
|
|
26
|
+
from nautobot.extras.utils import extras_features, FeatureQuery
|
|
26
27
|
|
|
27
28
|
logger = logging.getLogger(__name__)
|
|
28
29
|
|
|
@@ -34,35 +35,47 @@ logger = logging.getLogger(__name__)
|
|
|
34
35
|
"graphql",
|
|
35
36
|
"webhooks",
|
|
36
37
|
)
|
|
37
|
-
class DynamicGroup(
|
|
38
|
-
"""
|
|
38
|
+
class DynamicGroup(PrimaryModel):
|
|
39
|
+
"""A group of related objects sharing a common content-type."""
|
|
39
40
|
|
|
40
|
-
name = models.CharField(max_length=CHARFIELD_MAX_LENGTH, unique=True
|
|
41
|
+
name = models.CharField(max_length=CHARFIELD_MAX_LENGTH, unique=True)
|
|
41
42
|
description = models.CharField(max_length=CHARFIELD_MAX_LENGTH, blank=True)
|
|
43
|
+
group_type = models.CharField(
|
|
44
|
+
choices=DynamicGroupTypeChoices.CHOICES, max_length=16, default=DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER
|
|
45
|
+
)
|
|
42
46
|
content_type = models.ForeignKey(
|
|
43
47
|
to=ContentType,
|
|
44
48
|
on_delete=models.CASCADE,
|
|
45
49
|
verbose_name="Object Type",
|
|
46
|
-
help_text="The type of object
|
|
50
|
+
help_text="The type of object contained in this group.",
|
|
47
51
|
related_name="dynamic_groups",
|
|
52
|
+
limit_choices_to=FeatureQuery("dynamic_groups"),
|
|
53
|
+
)
|
|
54
|
+
tenant = models.ForeignKey(
|
|
55
|
+
to="tenancy.Tenant",
|
|
56
|
+
on_delete=models.PROTECT,
|
|
57
|
+
related_name="managed_dynamic_groups", # "dynamic_groups" clash with Tenant.dynamic_groups property
|
|
58
|
+
blank=True,
|
|
59
|
+
null=True,
|
|
48
60
|
)
|
|
49
61
|
filter = models.JSONField(
|
|
50
62
|
encoder=DjangoJSONEncoder,
|
|
51
63
|
editable=False,
|
|
52
64
|
default=dict,
|
|
53
|
-
help_text="A JSON-encoded dictionary of filter parameters
|
|
65
|
+
help_text="A JSON-encoded dictionary of filter parameters defining membership of this group",
|
|
54
66
|
)
|
|
55
67
|
children = models.ManyToManyField(
|
|
56
68
|
"extras.DynamicGroup",
|
|
57
|
-
help_text="Child
|
|
69
|
+
help_text='"Child" groups that are combined together to define membership of this group',
|
|
58
70
|
through="extras.DynamicGroupMembership",
|
|
59
71
|
through_fields=("parent_group", "group"),
|
|
60
72
|
related_name="parents",
|
|
61
73
|
)
|
|
62
74
|
|
|
63
75
|
objects = BaseManager.from_queryset(DynamicGroupQuerySet)()
|
|
76
|
+
is_dynamic_group_associable_model = False
|
|
64
77
|
|
|
65
|
-
clone_fields = ["content_type", "filter"]
|
|
78
|
+
clone_fields = ["content_type", "group_type", "filter", "tenant"]
|
|
66
79
|
|
|
67
80
|
# This is used as a `startswith` check on field names, so these can be explicit fields or just
|
|
68
81
|
# substrings.
|
|
@@ -80,12 +93,6 @@ class DynamicGroup(OrganizationalModel):
|
|
|
80
93
|
class Meta:
|
|
81
94
|
ordering = ["content_type", "name"]
|
|
82
95
|
|
|
83
|
-
def __init__(self, *args, **kwargs):
|
|
84
|
-
super().__init__(*args, **kwargs)
|
|
85
|
-
|
|
86
|
-
# Accessing this sets the dynamic attributes. Is there a better way? Maybe?
|
|
87
|
-
getattr(self, "model")
|
|
88
|
-
|
|
89
96
|
def __str__(self):
|
|
90
97
|
return self.name
|
|
91
98
|
|
|
@@ -103,50 +110,43 @@ class DynamicGroup(OrganizationalModel):
|
|
|
103
110
|
except models.ObjectDoesNotExist:
|
|
104
111
|
model = None
|
|
105
112
|
|
|
106
|
-
if model is not None:
|
|
107
|
-
self._set_object_classes(model)
|
|
108
|
-
|
|
109
113
|
self._model = model
|
|
110
114
|
|
|
111
115
|
return self._model
|
|
112
116
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
# If object classes have already been mapped, return True.
|
|
123
|
-
if getattr(self, "_object_classes_mapped", False):
|
|
124
|
-
return True
|
|
117
|
+
@property
|
|
118
|
+
def filterset_class(self):
|
|
119
|
+
if getattr(self, "_filterset_class", None) is None:
|
|
120
|
+
try:
|
|
121
|
+
self._filterset_class = get_filterset_for_model(self.model)
|
|
122
|
+
except TypeError:
|
|
123
|
+
self._filterset_class = None
|
|
124
|
+
return self._filterset_class
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
logger.debug("Failed to map object classes for model %s", model)
|
|
135
|
-
self.filterset_class = None
|
|
136
|
-
self.filterform_class = None
|
|
137
|
-
self.form_class = None
|
|
138
|
-
self._object_classes_mapped = False
|
|
139
|
-
else:
|
|
140
|
-
self._object_classes_mapped = True
|
|
126
|
+
@property
|
|
127
|
+
def filterform_class(self):
|
|
128
|
+
if getattr(self, "_filterform_class", None) is None:
|
|
129
|
+
try:
|
|
130
|
+
self._filterform_class = get_form_for_model(self.model, form_prefix="Filter")
|
|
131
|
+
except TypeError:
|
|
132
|
+
self._filterform_class = None
|
|
133
|
+
return self._filterform_class
|
|
141
134
|
|
|
142
|
-
|
|
135
|
+
@property
|
|
136
|
+
def form_class(self):
|
|
137
|
+
if getattr(self, "_form_class", None) is None:
|
|
138
|
+
try:
|
|
139
|
+
self._form_class = get_form_for_model(self.model)
|
|
140
|
+
except TypeError:
|
|
141
|
+
self._form_class = None
|
|
142
|
+
return self._form_class
|
|
143
143
|
|
|
144
144
|
@cached_property
|
|
145
145
|
def _map_filter_fields(self):
|
|
146
146
|
"""Return all FilterForm fields in a dictionary."""
|
|
147
147
|
|
|
148
148
|
# Fail gracefully with an empty dict if nothing is working yet.
|
|
149
|
-
if not self.
|
|
149
|
+
if not self.form_class:
|
|
150
150
|
return {}
|
|
151
151
|
|
|
152
152
|
# Get model form and fields
|
|
@@ -289,74 +289,148 @@ class DynamicGroup(OrganizationalModel):
|
|
|
289
289
|
|
|
290
290
|
return self._map_filter_fields
|
|
291
291
|
|
|
292
|
-
|
|
292
|
+
@property
|
|
293
|
+
def members(self):
|
|
293
294
|
"""
|
|
294
|
-
Return
|
|
295
|
+
Return the (cached) member objects for this group.
|
|
295
296
|
|
|
296
|
-
|
|
297
|
+
If up-to-the-minute accuracy is needed, call `update_cached_members()` instead.
|
|
297
298
|
"""
|
|
299
|
+
# Since associated_object is a GenericForeignKey, we can't just do:
|
|
300
|
+
# return self.static_group_associations.values_list("associated_object", flat=True)
|
|
301
|
+
return self.model.objects.filter(
|
|
302
|
+
pk__in=self.static_group_associations(manager="all_objects").values_list("associated_object_id", flat=True)
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
@members.setter
|
|
306
|
+
def members(self, value):
|
|
307
|
+
"""Set the member objects (QuerySet or list of records) for this staticly defined group."""
|
|
308
|
+
if self.group_type != DynamicGroupTypeChoices.TYPE_STATIC:
|
|
309
|
+
raise ValidationError(
|
|
310
|
+
f"Group {self} is not staticly defined, setting its members directly is not permitted."
|
|
311
|
+
)
|
|
312
|
+
return self._set_members(value)
|
|
313
|
+
|
|
314
|
+
def _set_members(self, value):
|
|
315
|
+
"""Internal API for updating the static/cached members of this group."""
|
|
316
|
+
if isinstance(value, models.QuerySet):
|
|
317
|
+
if value.model != self.model:
|
|
318
|
+
raise TypeError(f"QuerySet does not contain {self.model._meta.label_lower} objects")
|
|
319
|
+
to_remove = self.members.exclude(pk__in=value.values_list("pk", flat=True))
|
|
320
|
+
self._remove_members(to_remove)
|
|
321
|
+
to_add = value.exclude(pk__in=self.members.values_list("pk", flat=True))
|
|
322
|
+
self._add_members(to_add)
|
|
323
|
+
else:
|
|
324
|
+
for obj in value:
|
|
325
|
+
if not isinstance(obj, self.model):
|
|
326
|
+
raise TypeError(f"{obj} is not a {self.model._meta.label_lower}")
|
|
327
|
+
to_remove = []
|
|
328
|
+
for member in self.members:
|
|
329
|
+
if member not in value:
|
|
330
|
+
to_remove.append(member)
|
|
331
|
+
self._remove_members(to_remove)
|
|
332
|
+
to_add = []
|
|
333
|
+
members = self.members
|
|
334
|
+
for candidate in value:
|
|
335
|
+
if candidate not in members:
|
|
336
|
+
to_add.append(candidate)
|
|
337
|
+
self._add_members(to_add)
|
|
338
|
+
|
|
339
|
+
return self.members
|
|
340
|
+
|
|
341
|
+
def add_members(self, objects_to_add):
|
|
342
|
+
"""Add the given list or QuerySet of objects to this staticly defined group."""
|
|
343
|
+
if self.group_type != DynamicGroupTypeChoices.TYPE_STATIC:
|
|
344
|
+
raise ValidationError(f"Group {self} is not staticly defined, adding members directly is not permitted.")
|
|
345
|
+
return self._add_members(objects_to_add)
|
|
346
|
+
|
|
347
|
+
def _add_members(self, objects_to_add):
|
|
348
|
+
"""Internal API for adding the given list or QuerySet of objects to the cached/static members of this group."""
|
|
349
|
+
if isinstance(objects_to_add, models.QuerySet):
|
|
350
|
+
if objects_to_add.model != self.model:
|
|
351
|
+
raise TypeError(f"QuerySet does not contain {self.model._meta.label_lower} objects")
|
|
352
|
+
else:
|
|
353
|
+
for obj in objects_to_add:
|
|
354
|
+
if not isinstance(obj, self.model):
|
|
355
|
+
raise TypeError(f"{obj} is not a {self.model._meta.label_lower}")
|
|
356
|
+
|
|
357
|
+
if self.group_type == DynamicGroupTypeChoices.TYPE_STATIC:
|
|
358
|
+
for obj in objects_to_add:
|
|
359
|
+
# We don't use `.bulk_create()` currently because we want change logging for these creates.
|
|
360
|
+
# Might be a good future performance improvement though.
|
|
361
|
+
StaticGroupAssociation.all_objects.get_or_create(
|
|
362
|
+
dynamic_group=self, associated_object_type=self.content_type, associated_object_id=obj.pk
|
|
363
|
+
)
|
|
364
|
+
else:
|
|
365
|
+
# Cached/hidden static group associations, so we can use bulk-create to bypass change logging.
|
|
366
|
+
existing_members = self.members
|
|
367
|
+
sgas = [
|
|
368
|
+
StaticGroupAssociation(
|
|
369
|
+
dynamic_group=self, associated_object_type=self.content_type, associated_object_id=obj.pk
|
|
370
|
+
)
|
|
371
|
+
for obj in objects_to_add
|
|
372
|
+
if obj not in existing_members
|
|
373
|
+
]
|
|
374
|
+
StaticGroupAssociation.all_objects.bulk_create(sgas)
|
|
375
|
+
|
|
376
|
+
def remove_members(self, objects_to_remove):
|
|
377
|
+
"""Remove the given list or QuerySet of objects from this staticly defined group."""
|
|
378
|
+
if self.group_type != DynamicGroupTypeChoices.TYPE_STATIC:
|
|
379
|
+
raise ValidationError(f"Group {self} is not staticly defined, removing members directly is not permitted.")
|
|
380
|
+
return self._remove_members(objects_to_remove)
|
|
381
|
+
|
|
382
|
+
def _remove_members(self, objects_to_remove):
|
|
383
|
+
"""Internal API for removing the given list or QuerySet from the cached/static members of this Group."""
|
|
384
|
+
if isinstance(objects_to_remove, models.QuerySet):
|
|
385
|
+
if objects_to_remove.model != self.model:
|
|
386
|
+
raise TypeError(f"QuerySet does not contain {self.model._meta.label_lower} objects")
|
|
387
|
+
StaticGroupAssociation.all_objects.filter(
|
|
388
|
+
dynamic_group=self,
|
|
389
|
+
associated_object_type=self.content_type,
|
|
390
|
+
associated_object_id__in=objects_to_remove.values_list("pk", flat=True),
|
|
391
|
+
).delete()
|
|
392
|
+
else:
|
|
393
|
+
pks_to_remove = set()
|
|
394
|
+
for obj in objects_to_remove:
|
|
395
|
+
if not isinstance(obj, self.model):
|
|
396
|
+
raise TypeError(f"{obj} is not a {self.model._meta.label_lower}")
|
|
397
|
+
pks_to_remove.add(obj.pk)
|
|
298
398
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
raise RuntimeError(f"Could not determine queryset for model '{model}'")
|
|
303
|
-
|
|
304
|
-
filterset = self.filterset_class(self.filter, model.objects.all())
|
|
305
|
-
if not filterset.is_valid():
|
|
306
|
-
logger.warning('Filter for DynamicGroup "%s" is not valid', self)
|
|
307
|
-
return model.objects.none()
|
|
308
|
-
|
|
309
|
-
qs = filterset.qs
|
|
310
|
-
|
|
311
|
-
# Make sure that this instance can't be a member of its own group.
|
|
312
|
-
if self.present_in_database and model == self.__class__:
|
|
313
|
-
qs = qs.exclude(pk=self.pk)
|
|
314
|
-
|
|
315
|
-
return qs
|
|
316
|
-
|
|
317
|
-
@property
|
|
318
|
-
def members(self):
|
|
319
|
-
"""Return the member objects for this group, never cached."""
|
|
320
|
-
# If there are child groups, return the generated group queryset, otherwise use this group's
|
|
321
|
-
# `filter` directly.
|
|
322
|
-
if self.children.exists():
|
|
323
|
-
return self.get_group_queryset()
|
|
324
|
-
return self.get_queryset()
|
|
399
|
+
StaticGroupAssociation.all_objects.filter(
|
|
400
|
+
dynamic_group=self, associated_object_type=self.content_type, associated_object_id__in=pks_to_remove
|
|
401
|
+
).delete()
|
|
325
402
|
|
|
326
403
|
@property
|
|
404
|
+
@method_deprecated("Members are now cached in the database via StaticGroupAssociations rather than in Redis.")
|
|
327
405
|
def members_cache_key(self):
|
|
328
|
-
"""
|
|
406
|
+
"""Obsolete cache key for this group's members."""
|
|
329
407
|
return f"nautobot.extras.dynamicgroup.{self.id}.members_cached"
|
|
330
408
|
|
|
331
409
|
@property
|
|
410
|
+
@method_deprecated_in_favor_of(members.fget)
|
|
332
411
|
def members_cached(self):
|
|
333
|
-
"""
|
|
412
|
+
"""Deprecated - use `members()` instead."""
|
|
413
|
+
return self.members
|
|
334
414
|
|
|
335
|
-
|
|
336
|
-
try:
|
|
337
|
-
cached_query = cache.get(self.members_cache_key)
|
|
338
|
-
if cached_query is not None:
|
|
339
|
-
unpickled_query = pickle.loads(cached_query) # noqa: S301 # suspicious-pickle-usage -- we know, but we control what's in the DB
|
|
340
|
-
except pickle.UnpicklingError:
|
|
341
|
-
logger.warning("Failed to unpickle cached members for %s", self)
|
|
342
|
-
finally:
|
|
343
|
-
if unpickled_query is None:
|
|
344
|
-
unpickled_query = self.members.all()
|
|
345
|
-
cached_query = pickle.dumps(unpickled_query) # Explicitly pickle the query to evaluate it.
|
|
346
|
-
cache.set(
|
|
347
|
-
self.members_cache_key, cached_query, get_settings_or_config("DYNAMIC_GROUPS_MEMBER_CACHE_TIMEOUT")
|
|
348
|
-
)
|
|
349
|
-
|
|
350
|
-
return unpickled_query
|
|
351
|
-
|
|
352
|
-
def update_cached_members(self):
|
|
415
|
+
def update_cached_members(self, members=None):
|
|
353
416
|
"""
|
|
354
|
-
Update the cached members of
|
|
417
|
+
Update the cached members of this group and return the resulting members.
|
|
355
418
|
"""
|
|
419
|
+
if members is None:
|
|
420
|
+
if self.group_type in (
|
|
421
|
+
DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER,
|
|
422
|
+
DynamicGroupTypeChoices.TYPE_DYNAMIC_SET,
|
|
423
|
+
):
|
|
424
|
+
members = self._get_group_queryset()
|
|
425
|
+
elif self.group_type == DynamicGroupTypeChoices.TYPE_STATIC:
|
|
426
|
+
return self.members # nothing to do
|
|
427
|
+
else:
|
|
428
|
+
raise RuntimeError(f"Unknown/invalid group_type {self.group_type}")
|
|
356
429
|
|
|
357
|
-
|
|
430
|
+
self._set_members(members)
|
|
431
|
+
logger.debug("Refreshed cache for %s, now with %d members", self, self.count)
|
|
358
432
|
|
|
359
|
-
return
|
|
433
|
+
return members
|
|
360
434
|
|
|
361
435
|
def has_member(self, obj, use_cache=False):
|
|
362
436
|
"""
|
|
@@ -366,7 +440,7 @@ class DynamicGroup(OrganizationalModel):
|
|
|
366
440
|
|
|
367
441
|
Args:
|
|
368
442
|
obj (django.db.models.Model): The object to check for membership.
|
|
369
|
-
use_cache (bool, optional):
|
|
443
|
+
use_cache (bool, optional): Obsolete; cache is now always used.
|
|
370
444
|
|
|
371
445
|
Returns:
|
|
372
446
|
bool: True if the object is a member of this group, otherwise False.
|
|
@@ -374,23 +448,18 @@ class DynamicGroup(OrganizationalModel):
|
|
|
374
448
|
|
|
375
449
|
# Object's class may have content type cached, so check that first.
|
|
376
450
|
try:
|
|
377
|
-
if
|
|
451
|
+
if type(obj)._content_type.id != self.content_type_id:
|
|
378
452
|
return False
|
|
379
453
|
except AttributeError:
|
|
380
454
|
# Object did not have `_content_type` even though we wanted to use it.
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if not use_cache and ContentType.objects.get_for_model(obj).id != self.content_type_id:
|
|
384
|
-
return False
|
|
455
|
+
if ContentType.objects.get_for_model(obj).id != self.content_type_id:
|
|
456
|
+
return False
|
|
385
457
|
|
|
386
|
-
|
|
387
|
-
return self.members.filter(pk=obj.pk).exists()
|
|
388
|
-
else:
|
|
389
|
-
return obj in list(self.members_cached)
|
|
458
|
+
return self.members.filter(pk=obj.pk).exists()
|
|
390
459
|
|
|
391
460
|
@property
|
|
392
461
|
def count(self):
|
|
393
|
-
"""Return the number of member objects in this group."""
|
|
462
|
+
"""Return the (cached) number of member objects in this group."""
|
|
394
463
|
return self.members.count()
|
|
395
464
|
|
|
396
465
|
def get_group_members_url(self):
|
|
@@ -404,9 +473,12 @@ class DynamicGroup(OrganizationalModel):
|
|
|
404
473
|
"""
|
|
405
474
|
Set all desired fields from `form_data` into `filter` dict.
|
|
406
475
|
|
|
407
|
-
:
|
|
408
|
-
Dict of filter parameters, generally from a filter form's `cleaned_data`
|
|
476
|
+
Args:
|
|
477
|
+
form_data (dict): Dict of filter parameters, generally from a filter form's `cleaned_data`
|
|
409
478
|
"""
|
|
479
|
+
if self.group_type != DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER:
|
|
480
|
+
raise ValidationError(f"Group {self} is not a filter-defined group (instead, group_type {self.group_type})")
|
|
481
|
+
|
|
410
482
|
# Get the authoritative source of filter fields we want to keep.
|
|
411
483
|
filter_fields = self.get_filter_fields()
|
|
412
484
|
|
|
@@ -471,7 +543,7 @@ class DynamicGroup(OrganizationalModel):
|
|
|
471
543
|
|
|
472
544
|
def get_initial(self):
|
|
473
545
|
"""
|
|
474
|
-
Return
|
|
546
|
+
Return a form-friendly version of `self.filter` for initial form data.
|
|
475
547
|
|
|
476
548
|
This is intended for use to populate the dynamically-generated filter form created by
|
|
477
549
|
`generate_filter_form()`.
|
|
@@ -514,11 +586,17 @@ class DynamicGroup(OrganizationalModel):
|
|
|
514
586
|
# Accessing `self.model` will determine if the `content_type` is not correctly set, blocking validation.
|
|
515
587
|
if self.model is None:
|
|
516
588
|
raise ValidationError({"filter": "Filter requires a `content_type` to be set"})
|
|
589
|
+
if self.filterset_class is None:
|
|
590
|
+
raise ValidationError({"filter": "Unable to locate the FilterSet class for this model."})
|
|
517
591
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
592
|
+
if self.group_type != DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER:
|
|
593
|
+
if self.filter:
|
|
594
|
+
raise ValidationError({"filter": "Filter can only be set for groups of type `dynamic-filter`."})
|
|
595
|
+
else:
|
|
596
|
+
# Validate against the filterset's internal form validation.
|
|
597
|
+
filterset = self.filterset_class(self.filter)
|
|
598
|
+
if not filterset.is_valid():
|
|
599
|
+
raise ValidationError(filterset.errors)
|
|
522
600
|
|
|
523
601
|
def delete(self, *args, **kwargs):
|
|
524
602
|
"""Check if we're a child and attempt to block delete if we are."""
|
|
@@ -548,14 +626,17 @@ class DynamicGroup(OrganizationalModel):
|
|
|
548
626
|
if self.content_type != database_object.content_type:
|
|
549
627
|
raise ValidationError({"content_type": "ContentType cannot be changed once created"})
|
|
550
628
|
|
|
551
|
-
|
|
629
|
+
# TODO limit most changes to self.group_type as well.
|
|
630
|
+
|
|
631
|
+
def _generate_query_for_filter(self, filter_field, value):
|
|
552
632
|
"""
|
|
553
633
|
Return a `Q` object generated from a `filter_field` and `value`.
|
|
554
634
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
:
|
|
558
|
-
|
|
635
|
+
Helper to `_generate_filter_based_query()`.
|
|
636
|
+
|
|
637
|
+
Args:
|
|
638
|
+
filter_field (Filter): filterset filter field instance
|
|
639
|
+
value (Any): value passed to the filter
|
|
559
640
|
"""
|
|
560
641
|
query = models.Q()
|
|
561
642
|
if filter_field is None:
|
|
@@ -587,7 +668,7 @@ class DynamicGroup(OrganizationalModel):
|
|
|
587
668
|
# pass it to `generate_query` to get a correct Q object back out. When values are being
|
|
588
669
|
# reconstructed from saved filters, lists of names are common e.g. (`{"location": ["ams01",
|
|
589
670
|
# "ams02"]}`, the value being a list of location names (`["ams01", "ams02"]`).
|
|
590
|
-
if value and isinstance(value, list) and isinstance(value[0], str):
|
|
671
|
+
if value and isinstance(value, list) and isinstance(value[0], str) and not is_uuid(value[0]):
|
|
591
672
|
model_field = django_filters.utils.get_model_field(self._model, filter_field.field_name)
|
|
592
673
|
related_model = model_field.related_model
|
|
593
674
|
lookup_kwargs = {f"{to_field_name}__in": value}
|
|
@@ -618,35 +699,39 @@ class DynamicGroup(OrganizationalModel):
|
|
|
618
699
|
|
|
619
700
|
return query
|
|
620
701
|
|
|
621
|
-
def
|
|
702
|
+
def _generate_filter_based_query(self):
|
|
622
703
|
"""
|
|
623
|
-
Return a `Q` object generated from
|
|
704
|
+
Return a `Q` object generated from this group's filters.
|
|
624
705
|
|
|
625
|
-
|
|
626
|
-
DynamicGroup instance
|
|
706
|
+
Helper to `generate_query()`.
|
|
627
707
|
"""
|
|
628
|
-
|
|
708
|
+
if self.group_type != DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER:
|
|
709
|
+
raise RuntimeError(f"{self} is not a dynamic-filter group")
|
|
710
|
+
|
|
711
|
+
filterset = self.filterset_class(self.filter, self.model.objects.all())
|
|
629
712
|
query = models.Q()
|
|
630
713
|
|
|
631
714
|
# In this case we want all filters for a group's filter dict in a set intersection (boolean
|
|
632
715
|
# AND) because ALL filter conditions must match for the filter parameters to be valid.
|
|
633
|
-
for field_name, value in
|
|
634
|
-
filter_field =
|
|
635
|
-
query &= self.
|
|
716
|
+
for field_name, value in filterset.data.items():
|
|
717
|
+
filter_field = filterset.filters.get(field_name)
|
|
718
|
+
query &= self._generate_query_for_filter(filter_field, value)
|
|
636
719
|
|
|
637
720
|
return query
|
|
638
721
|
|
|
639
|
-
def
|
|
722
|
+
def _perform_membership_set_operation(self, operator, query, next_set):
|
|
640
723
|
"""
|
|
641
|
-
Perform set operation for a group membership.
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
Q
|
|
648
|
-
|
|
649
|
-
|
|
724
|
+
Perform set operation for a group membership.
|
|
725
|
+
|
|
726
|
+
The `operator` and `next_set` are used to decide the appropriate action to take on the `query`.
|
|
727
|
+
|
|
728
|
+
Args:
|
|
729
|
+
operator (str): DynamicGroupOperatorChoices choice
|
|
730
|
+
query (Q): Query so far
|
|
731
|
+
next_set (Q): Additional query to apply based on the operator.
|
|
732
|
+
|
|
733
|
+
Returns:
|
|
734
|
+
Q: updated query object
|
|
650
735
|
"""
|
|
651
736
|
if operator == "union":
|
|
652
737
|
query |= next_set
|
|
@@ -657,74 +742,71 @@ class DynamicGroup(OrganizationalModel):
|
|
|
657
742
|
|
|
658
743
|
return query
|
|
659
744
|
|
|
660
|
-
def generate_members_query(self):
|
|
661
|
-
"""
|
|
662
|
-
Return a `Q` object generated from all direct members or from self if `filter` is set.
|
|
663
|
-
"""
|
|
664
|
-
if self.filter:
|
|
665
|
-
return self.generate_query_for_group(self)
|
|
666
|
-
|
|
667
|
-
query = models.Q()
|
|
668
|
-
for membership in self.dynamic_group_memberships.all():
|
|
669
|
-
group = membership.group
|
|
670
|
-
operator = membership.operator
|
|
671
|
-
next_set = self.generate_query_for_group(group)
|
|
672
|
-
query = self.perform_membership_set_operation(operator, query, next_set)
|
|
673
|
-
|
|
674
|
-
return query
|
|
675
|
-
|
|
676
745
|
def generate_query(self):
|
|
677
746
|
"""
|
|
678
|
-
Return a `Q` object generated recursively from
|
|
747
|
+
Return a `Q` object generated recursively from this dynamic group.
|
|
679
748
|
"""
|
|
680
|
-
|
|
681
|
-
|
|
749
|
+
if self.group_type == DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER:
|
|
750
|
+
return self._generate_filter_based_query()
|
|
682
751
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
752
|
+
if self.group_type == DynamicGroupTypeChoices.TYPE_DYNAMIC_SET:
|
|
753
|
+
query = models.Q()
|
|
754
|
+
memberships = self.dynamic_group_memberships.all()
|
|
755
|
+
# Enumerate the filters for each child group, trusting that they handle their own children.
|
|
756
|
+
for membership in memberships:
|
|
757
|
+
group = membership.group
|
|
758
|
+
operator = membership.operator
|
|
759
|
+
logger.debug("Processing group %s...", group)
|
|
686
760
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
group = membership.group
|
|
690
|
-
operator = membership.operator
|
|
691
|
-
logger.debug("Processing group %s...", group)
|
|
761
|
+
if group.group_type == DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER:
|
|
762
|
+
logger.debug("Query: %s -> %s -> %s", group, group.filter, operator)
|
|
692
763
|
|
|
693
|
-
|
|
694
|
-
|
|
764
|
+
next_set = group.generate_query()
|
|
765
|
+
query = self._perform_membership_set_operation(operator, query, next_set)
|
|
695
766
|
|
|
696
|
-
|
|
697
|
-
query = self.perform_membership_set_operation(operator, query, next_set)
|
|
767
|
+
return query
|
|
698
768
|
|
|
699
|
-
|
|
769
|
+
# TODO? if self.group_type == DynamicGroupTypeChoices.TYPE_STATIC:
|
|
770
|
+
|
|
771
|
+
raise RuntimeError(f"generate_query not implemented for group_type {self.group_type}")
|
|
700
772
|
|
|
701
|
-
def
|
|
702
|
-
"""
|
|
773
|
+
def _get_group_queryset(self):
|
|
774
|
+
"""Construct the queryset representing dynamic membership of this group."""
|
|
703
775
|
query = self.generate_query()
|
|
704
|
-
|
|
705
|
-
return qs.filter(query)
|
|
776
|
+
return self.model.objects.filter(query)
|
|
706
777
|
|
|
778
|
+
# TODO: unused in core
|
|
707
779
|
def add_child(self, child, operator, weight):
|
|
708
780
|
"""
|
|
709
781
|
Add a child group including `operator` and `weight`.
|
|
710
782
|
|
|
711
|
-
:
|
|
712
|
-
DynamicGroup
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
:param weight:
|
|
716
|
-
Integer weight used to order filtering
|
|
783
|
+
Args:
|
|
784
|
+
child (DynamicGroup): child group to add
|
|
785
|
+
operator (str): DynamicGroupOperatorChoices choice value used to dictate filtering behavior
|
|
786
|
+
weight (int): Integer weight used to order filtering
|
|
717
787
|
"""
|
|
788
|
+
if self.group_type != DynamicGroupTypeChoices.TYPE_DYNAMIC_SET:
|
|
789
|
+
if self.filter or self.group_type != DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER:
|
|
790
|
+
raise ValidationError(f"{self} is not a dynamic-set group.")
|
|
791
|
+
else:
|
|
792
|
+
# For backwards compatibility
|
|
793
|
+
self.group_type = DynamicGroupTypeChoices.TYPE_DYNAMIC_SET
|
|
794
|
+
self.validated_save()
|
|
795
|
+
|
|
718
796
|
instance = self.children.through(parent_group=self, group=child, operator=operator, weight=weight)
|
|
719
797
|
return instance.validated_save()
|
|
720
798
|
|
|
799
|
+
# TODO: unused in core
|
|
721
800
|
def remove_child(self, child):
|
|
722
801
|
"""
|
|
723
802
|
Remove a child group.
|
|
724
803
|
|
|
725
|
-
:
|
|
726
|
-
DynamicGroup
|
|
804
|
+
Args:
|
|
805
|
+
child (DynamicGroup): child group to remove
|
|
727
806
|
"""
|
|
807
|
+
if self.group_type != DynamicGroupTypeChoices.TYPE_DYNAMIC_SET:
|
|
808
|
+
raise ValidationError(f"{self} is not a dynamic-set group.")
|
|
809
|
+
|
|
728
810
|
instance = self.children.through.objects.get(parent_group=self, group=child)
|
|
729
811
|
return instance.delete()
|
|
730
812
|
|
|
@@ -732,8 +814,8 @@ class DynamicGroup(OrganizationalModel):
|
|
|
732
814
|
"""
|
|
733
815
|
Recursively return a list of the children of all child groups.
|
|
734
816
|
|
|
735
|
-
:
|
|
736
|
-
DynamicGroup
|
|
817
|
+
Args:
|
|
818
|
+
group (DynamicGroup): parent group to traverse from. If not set, this group (self) is used.
|
|
737
819
|
"""
|
|
738
820
|
if group is None:
|
|
739
821
|
group = self
|
|
@@ -743,7 +825,7 @@ class DynamicGroup(OrganizationalModel):
|
|
|
743
825
|
logger.debug("Processing group %s...", child_group)
|
|
744
826
|
descendants.append(child_group)
|
|
745
827
|
if child_group.children.exists():
|
|
746
|
-
descendants.extend(
|
|
828
|
+
descendants.extend(child_group.get_descendants())
|
|
747
829
|
|
|
748
830
|
return descendants
|
|
749
831
|
|
|
@@ -751,8 +833,8 @@ class DynamicGroup(OrganizationalModel):
|
|
|
751
833
|
"""
|
|
752
834
|
Recursively return a list of the parents of all parent groups.
|
|
753
835
|
|
|
754
|
-
:
|
|
755
|
-
DynamicGroup
|
|
836
|
+
Args:
|
|
837
|
+
group (DynamicGroup): child group to traverse from. If not set, this group (self) is used.
|
|
756
838
|
"""
|
|
757
839
|
if group is None:
|
|
758
840
|
group = self
|
|
@@ -762,10 +844,11 @@ class DynamicGroup(OrganizationalModel):
|
|
|
762
844
|
logger.debug("Processing group %s...", parent_group)
|
|
763
845
|
ancestors.append(parent_group)
|
|
764
846
|
if parent_group.parents.exists():
|
|
765
|
-
ancestors.extend(
|
|
847
|
+
ancestors.extend(parent_group.get_ancestors())
|
|
766
848
|
|
|
767
849
|
return ancestors
|
|
768
850
|
|
|
851
|
+
# TODO: unused in core
|
|
769
852
|
def get_siblings(self, include_self=False):
|
|
770
853
|
"""Return groups that share the same parents."""
|
|
771
854
|
siblings = DynamicGroup.objects.filter(parents__in=self.parents.all())
|
|
@@ -774,19 +857,25 @@ class DynamicGroup(OrganizationalModel):
|
|
|
774
857
|
|
|
775
858
|
return siblings.exclude(pk=self.pk)
|
|
776
859
|
|
|
860
|
+
# TODO: this is an interesting definition of "root node", as a node with no children has is_root() = False??
|
|
861
|
+
# TODO: unused in core
|
|
777
862
|
def is_root(self):
|
|
778
863
|
"""Return whether this is a root node (has children, but no parents)."""
|
|
779
864
|
return self.children.exists() and not self.parents.exists()
|
|
780
865
|
|
|
866
|
+
# TODO: this is an interesting definition of "leaf node", as a node with no parents has is_leaf() = False??
|
|
867
|
+
# TODO: unused in core
|
|
781
868
|
def is_leaf(self):
|
|
782
869
|
"""Return whether this is a leaf node (has parents, but no children)."""
|
|
783
870
|
return self.parents.exists() and not self.children.exists()
|
|
784
871
|
|
|
872
|
+
# TODO: unused in core
|
|
785
873
|
def get_ancestors_queryset(self):
|
|
786
874
|
"""Return a queryset of all ancestors."""
|
|
787
875
|
pks = [obj.pk for obj in self.get_ancestors()]
|
|
788
876
|
return self.ordered_queryset_from_pks(pks)
|
|
789
877
|
|
|
878
|
+
# TODO: unused in core
|
|
790
879
|
def get_descendants_queryset(self):
|
|
791
880
|
"""Return a queryset of all descendants."""
|
|
792
881
|
pks = [obj.pk for obj in self.get_descendants()]
|
|
@@ -819,14 +908,15 @@ class DynamicGroup(OrganizationalModel):
|
|
|
819
908
|
|
|
820
909
|
def flatten_ancestors_tree(self, tree):
|
|
821
910
|
"""
|
|
822
|
-
Recursively flatten a tree mapping of ancestors to a list, adding a `depth attribute to each
|
|
911
|
+
Recursively flatten a tree mapping of ancestors to a list, adding a `depth` attribute to each
|
|
823
912
|
instance in the list that can be used for visualizing tree depth.
|
|
824
913
|
|
|
825
|
-
:
|
|
826
|
-
Output from `ancestors_tree()`
|
|
914
|
+
Args:
|
|
915
|
+
tree (dict): Output from `ancestors_tree()`
|
|
827
916
|
"""
|
|
828
|
-
return self._flatten_tree(tree
|
|
917
|
+
return self._flatten_tree(tree)
|
|
829
918
|
|
|
919
|
+
# TODO: unused in core
|
|
830
920
|
def descendants_tree(self):
|
|
831
921
|
"""
|
|
832
922
|
Return a nested mapping of descendants with the following structure:
|
|
@@ -851,44 +941,35 @@ class DynamicGroup(OrganizationalModel):
|
|
|
851
941
|
|
|
852
942
|
return tree
|
|
853
943
|
|
|
944
|
+
# TODO: unused in core
|
|
854
945
|
def flatten_descendants_tree(self, tree):
|
|
855
946
|
"""
|
|
856
947
|
Recursively flatten a tree mapping of descendants to a list, adding a `depth` attribute to each
|
|
857
948
|
instance in the list that can be used for visualizing tree depth.
|
|
858
949
|
|
|
859
|
-
:
|
|
860
|
-
Output from `
|
|
950
|
+
Args:
|
|
951
|
+
tree (dict): Output from `descendants_tree()`
|
|
861
952
|
"""
|
|
862
|
-
return self._flatten_tree(tree
|
|
953
|
+
return self._flatten_tree(tree)
|
|
863
954
|
|
|
864
|
-
def _flatten_tree(self, tree,
|
|
955
|
+
def _flatten_tree(self, tree, nodes=None, depth=1):
|
|
865
956
|
"""
|
|
866
957
|
Recursively flatten a tree mapping to a list, adding a `depth` attribute to each instance in
|
|
867
958
|
the list that can be used for visualizing tree depth.
|
|
868
959
|
|
|
869
|
-
:
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
:param nodes:
|
|
874
|
-
An ordered list used to hold the flattened nodes
|
|
875
|
-
:param depth:
|
|
876
|
-
The tree traversal depth
|
|
960
|
+
Args:
|
|
961
|
+
tree (dict): Output from `ancestors_tree()` or `descendants_tree()`
|
|
962
|
+
nodes (list): Used in recursion, will contain the flattened nodes.
|
|
963
|
+
depth (int): Used in recursion, the tree traversal depth.
|
|
877
964
|
"""
|
|
878
965
|
|
|
879
966
|
if nodes is None:
|
|
880
967
|
nodes = []
|
|
881
968
|
|
|
882
|
-
if descending:
|
|
883
|
-
method = "get_descendants"
|
|
884
|
-
else:
|
|
885
|
-
method = "get_ancestors"
|
|
886
|
-
|
|
887
969
|
for item in tree:
|
|
888
970
|
item.depth = depth
|
|
889
971
|
nodes.append(item)
|
|
890
|
-
|
|
891
|
-
self._flatten_tree(branches, nodes=nodes, descending=descending, depth=depth + 1)
|
|
972
|
+
self._flatten_tree(tree[item], nodes=nodes, depth=depth + 1)
|
|
892
973
|
|
|
893
974
|
return nodes
|
|
894
975
|
|
|
@@ -907,6 +988,7 @@ class DynamicGroup(OrganizationalModel):
|
|
|
907
988
|
|
|
908
989
|
return tree
|
|
909
990
|
|
|
991
|
+
# TODO: unused in core
|
|
910
992
|
def _ordered_filter(self, queryset, field_names, values):
|
|
911
993
|
"""
|
|
912
994
|
Filters the provided `queryset` using `{field_name}__in` expressions for each field_name in the
|
|
@@ -943,6 +1025,7 @@ class DynamicGroup(OrganizationalModel):
|
|
|
943
1025
|
|
|
944
1026
|
return queryset.filter(**filter_condition).order_by(order_by)
|
|
945
1027
|
|
|
1028
|
+
# TODO: unused in core
|
|
946
1029
|
def ordered_queryset_from_pks(self, pk_list):
|
|
947
1030
|
"""
|
|
948
1031
|
Generates a queryset ordered by the provided list of primary keys.
|
|
@@ -966,6 +1049,7 @@ class DynamicGroupMembership(BaseModel):
|
|
|
966
1049
|
objects = BaseManager.from_queryset(DynamicGroupMembershipQuerySet)()
|
|
967
1050
|
|
|
968
1051
|
documentation_static_path = "docs/user-guide/platform-functionality/dynamicgroup.html"
|
|
1052
|
+
is_metadata_associable_model = False
|
|
969
1053
|
|
|
970
1054
|
class Meta:
|
|
971
1055
|
unique_together = ["group", "parent_group", "operator", "weight"]
|
|
@@ -984,11 +1068,13 @@ class DynamicGroupMembership(BaseModel):
|
|
|
984
1068
|
"""Return the group filter."""
|
|
985
1069
|
return self.group.filter
|
|
986
1070
|
|
|
1071
|
+
# TODO: unused in core
|
|
987
1072
|
@property
|
|
988
1073
|
def members(self):
|
|
989
1074
|
"""Return the group members."""
|
|
990
1075
|
return self.group.members
|
|
991
1076
|
|
|
1077
|
+
# TODO: unused in core
|
|
992
1078
|
@property
|
|
993
1079
|
def count(self):
|
|
994
1080
|
"""Return the group count."""
|
|
@@ -1001,10 +1087,12 @@ class DynamicGroupMembership(BaseModel):
|
|
|
1001
1087
|
return self.group.get_absolute_url(api=api)
|
|
1002
1088
|
return super().get_absolute_url(api=api)
|
|
1003
1089
|
|
|
1090
|
+
# TODO: unused in core
|
|
1004
1091
|
def get_group_members_url(self):
|
|
1005
1092
|
"""Return the group members URL."""
|
|
1006
1093
|
return self.group.get_group_members_url()
|
|
1007
1094
|
|
|
1095
|
+
# TODO: unused in core
|
|
1008
1096
|
def get_siblings(self, include_self=False):
|
|
1009
1097
|
"""Return group memberships that share the same parent group."""
|
|
1010
1098
|
siblings = DynamicGroupMembership.objects.filter(parent_group=self.parent_group)
|
|
@@ -1013,19 +1101,19 @@ class DynamicGroupMembership(BaseModel):
|
|
|
1013
1101
|
|
|
1014
1102
|
return siblings.exclude(pk=self.pk)
|
|
1015
1103
|
|
|
1104
|
+
# TODO: unused in core
|
|
1016
1105
|
def generate_query(self):
|
|
1017
1106
|
return self.group.generate_query()
|
|
1018
1107
|
|
|
1019
1108
|
def clean(self):
|
|
1020
1109
|
super().clean()
|
|
1021
1110
|
|
|
1022
|
-
# Enforce
|
|
1023
|
-
if self.parent_group.filter:
|
|
1024
|
-
raise ValidationError(
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
)
|
|
1111
|
+
# Enforce group types
|
|
1112
|
+
if self.parent_group.group_type != DynamicGroupTypeChoices.TYPE_DYNAMIC_SET and self.parent_group.filter:
|
|
1113
|
+
raise ValidationError({"parent_group": 'A parent group must be of `group_type` `"dynamic-set"`.'})
|
|
1114
|
+
|
|
1115
|
+
if self.group.group_type == DynamicGroupTypeChoices.TYPE_STATIC:
|
|
1116
|
+
raise ValidationError({"group": 'Groups of `group_type` `"static"` may not be child groups at this time.'})
|
|
1029
1117
|
|
|
1030
1118
|
# Enforce matching content_type
|
|
1031
1119
|
if self.parent_group.content_type != self.group.content_type:
|
|
@@ -1037,3 +1125,79 @@ class DynamicGroupMembership(BaseModel):
|
|
|
1037
1125
|
|
|
1038
1126
|
if self.group in self.parent_group.get_ancestors():
|
|
1039
1127
|
raise ValidationError({"group": "Cannot add ancestor as a child"})
|
|
1128
|
+
|
|
1129
|
+
def save(self, *args, **kwargs):
|
|
1130
|
+
# For backwards compatibility
|
|
1131
|
+
if self.parent_group.group_type == DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER and not self.parent_group.filter:
|
|
1132
|
+
self.parent_group.group_type = DynamicGroupTypeChoices.TYPE_DYNAMIC_SET
|
|
1133
|
+
self.parent_group.save()
|
|
1134
|
+
return super().save(*args, **kwargs)
|
|
1135
|
+
|
|
1136
|
+
|
|
1137
|
+
class StaticGroupAssociationManager(BaseManager.from_queryset(RestrictedQuerySet)):
|
|
1138
|
+
use_in_migrations = True
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
class StaticGroupAssociationDefaultManager(StaticGroupAssociationManager):
|
|
1142
|
+
"""Subclass of StaticGroupAssociationManager that automatically filters out cached/hidden associations."""
|
|
1143
|
+
|
|
1144
|
+
def get_queryset(self):
|
|
1145
|
+
return super().get_queryset().filter(dynamic_group__group_type=DynamicGroupTypeChoices.TYPE_STATIC)
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
@extras_features(
|
|
1149
|
+
"custom_validators",
|
|
1150
|
+
"export_templates",
|
|
1151
|
+
"graphql",
|
|
1152
|
+
"webhooks",
|
|
1153
|
+
)
|
|
1154
|
+
class StaticGroupAssociation(OrganizationalModel):
|
|
1155
|
+
"""Intermediary model for associating an object statically to a DynamicGroup of group_type `static`."""
|
|
1156
|
+
|
|
1157
|
+
dynamic_group = models.ForeignKey(
|
|
1158
|
+
to=DynamicGroup, on_delete=models.CASCADE, related_name="static_group_associations"
|
|
1159
|
+
)
|
|
1160
|
+
associated_object_type = models.ForeignKey(
|
|
1161
|
+
to=ContentType,
|
|
1162
|
+
on_delete=models.CASCADE,
|
|
1163
|
+
related_name="static_group_associations",
|
|
1164
|
+
limit_choices_to=FeatureQuery("dynamic_groups"),
|
|
1165
|
+
)
|
|
1166
|
+
associated_object_id = models.UUIDField(db_index=True)
|
|
1167
|
+
associated_object = GenericForeignKey(ct_field="associated_object_type", fk_field="associated_object_id")
|
|
1168
|
+
|
|
1169
|
+
objects = StaticGroupAssociationDefaultManager()
|
|
1170
|
+
all_objects = StaticGroupAssociationManager()
|
|
1171
|
+
|
|
1172
|
+
is_contact_associable_model = False
|
|
1173
|
+
is_dynamic_group_associable_model = False
|
|
1174
|
+
is_saved_view_model = False
|
|
1175
|
+
|
|
1176
|
+
class Meta:
|
|
1177
|
+
unique_together = [["dynamic_group", "associated_object_type", "associated_object_id"]]
|
|
1178
|
+
ordering = ["dynamic_group", "associated_object_type", "associated_object_id"]
|
|
1179
|
+
indexes = [
|
|
1180
|
+
models.Index(
|
|
1181
|
+
name="extras_sga_double",
|
|
1182
|
+
fields=["dynamic_group", "associated_object_id"],
|
|
1183
|
+
),
|
|
1184
|
+
models.Index(
|
|
1185
|
+
name="extras_sga_associated_object",
|
|
1186
|
+
fields=["associated_object_type_id", "associated_object_id"],
|
|
1187
|
+
),
|
|
1188
|
+
]
|
|
1189
|
+
|
|
1190
|
+
def __str__(self):
|
|
1191
|
+
return f"{self.associated_object} as a member of {self.dynamic_group}"
|
|
1192
|
+
|
|
1193
|
+
def clean(self):
|
|
1194
|
+
super().clean()
|
|
1195
|
+
|
|
1196
|
+
if self.associated_object_type != self.dynamic_group.content_type:
|
|
1197
|
+
raise ValidationError({"associated_object_type": "Must match the dynamic_group.content_type"})
|
|
1198
|
+
|
|
1199
|
+
def to_objectchange(self, *args, **kwargs):
|
|
1200
|
+
"""Change log StaticGroupAssociations belonging to a "static" group; all others are an implementation detail."""
|
|
1201
|
+
if self.dynamic_group.group_type != DynamicGroupTypeChoices.TYPE_STATIC:
|
|
1202
|
+
return None
|
|
1203
|
+
return super().to_objectchange(*args, **kwargs)
|