nautobot 2.3.1__py3-none-any.whl → 2.3.3__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.
- nautobot/core/celery/schedulers.py +18 -0
- nautobot/core/settings.yaml +3 -3
- nautobot/core/tables.py +1 -1
- nautobot/core/templates/home.html +4 -3
- nautobot/core/templatetags/buttons.py +1 -1
- nautobot/core/tests/runner.py +27 -9
- nautobot/core/tests/test_utils.py +13 -0
- nautobot/core/utils/lookup.py +7 -1
- nautobot/core/views/utils.py +3 -3
- nautobot/dcim/factory.py +3 -3
- nautobot/dcim/tables/devices.py +7 -7
- nautobot/dcim/templates/dcim/device.html +12 -0
- nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +12 -0
- nautobot/dcim/utils.py +9 -6
- nautobot/extras/api/serializers.py +2 -0
- nautobot/extras/context_managers.py +11 -4
- nautobot/extras/filters/__init__.py +14 -2
- nautobot/extras/forms/forms.py +6 -0
- nautobot/extras/forms/mixins.py +2 -2
- nautobot/extras/jobs.py +0 -1
- nautobot/extras/management/__init__.py +3 -0
- nautobot/extras/migrations/0115_scheduledjob_time_zone.py +23 -0
- nautobot/extras/models/groups.py +4 -1
- nautobot/extras/models/jobs.py +24 -11
- nautobot/extras/tables.py +34 -4
- nautobot/extras/templates/extras/scheduledjob.html +13 -2
- nautobot/extras/tests/test_api.py +17 -18
- nautobot/extras/tests/test_context_managers.py +33 -14
- nautobot/extras/tests/test_dynamicgroups.py +11 -0
- nautobot/extras/tests/test_filters.py +57 -1
- nautobot/extras/tests/test_models.py +304 -1
- nautobot/extras/tests/test_views.py +4 -2
- nautobot/extras/views.py +7 -0
- nautobot/ipam/api/views.py +9 -2
- nautobot/ipam/choices.py +17 -0
- nautobot/ipam/factory.py +6 -0
- nautobot/ipam/filters.py +1 -1
- nautobot/ipam/forms.py +5 -3
- nautobot/ipam/migrations/0048_vrf_status.py +23 -0
- nautobot/ipam/migrations/0049_vrf_data_migration.py +25 -0
- nautobot/ipam/models.py +6 -0
- nautobot/ipam/tables.py +3 -2
- nautobot/ipam/templates/ipam/vrf.html +4 -0
- nautobot/ipam/templates/ipam/vrf_edit.html +1 -0
- nautobot/ipam/tests/test_api.py +44 -3
- nautobot/ipam/tests/test_views.py +3 -0
- nautobot/project-static/css/base.css +6 -0
- nautobot/project-static/docs/404.html +23 -23
- nautobot/project-static/docs/apps/index.html +25 -25
- nautobot/project-static/docs/apps/nautobot-apps.html +24 -24
- nautobot/project-static/docs/assets/javascripts/bundle.56dfad97.min.js +16 -0
- nautobot/project-static/docs/assets/javascripts/bundle.56dfad97.min.js.map +7 -0
- nautobot/project-static/docs/assets/javascripts/workers/{search.b8dbb3d2.min.js → search.07f07601.min.js} +1 -1
- nautobot/project-static/docs/assets/javascripts/workers/{search.b8dbb3d2.min.js.map → search.07f07601.min.js.map} +1 -1
- nautobot/project-static/docs/assets/stylesheets/main.35f28582.min.css +1 -0
- nautobot/project-static/docs/assets/stylesheets/main.35f28582.min.css.map +1 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +26 -26
- nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +26 -26
- nautobot/project-static/docs/code-reference/nautobot/apps/api.html +58 -58
- nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +32 -31
- nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +31 -31
- nautobot/project-static/docs/code-reference/nautobot/apps/config.html +25 -25
- nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +25 -25
- nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +30 -30
- nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +34 -34
- nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +41 -41
- nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +48 -48
- nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +92 -92
- nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +41 -41
- nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +85 -85
- nautobot/project-static/docs/code-reference/nautobot/apps/models.html +84 -84
- nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +26 -26
- nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +28 -28
- nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +40 -40
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +78 -78
- nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +77 -77
- nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +26 -26
- nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +80 -80
- nautobot/project-static/docs/code-reference/nautobot/apps/views.html +67 -67
- nautobot/project-static/docs/development/apps/api/configuration-view.html +25 -25
- nautobot/project-static/docs/development/apps/api/database-backend-config.html +25 -25
- nautobot/project-static/docs/development/apps/api/models/django-admin.html +25 -25
- nautobot/project-static/docs/development/apps/api/models/global-search.html +25 -25
- nautobot/project-static/docs/development/apps/api/models/graphql.html +25 -25
- nautobot/project-static/docs/development/apps/api/models/index.html +25 -25
- nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +25 -25
- nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +25 -25
- nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +25 -25
- nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +25 -25
- nautobot/project-static/docs/development/apps/api/platform-features/index.html +25 -25
- nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +25 -25
- nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +25 -25
- nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +25 -25
- nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +25 -25
- nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +25 -25
- nautobot/project-static/docs/development/apps/api/prometheus.html +25 -25
- nautobot/project-static/docs/development/apps/api/setup.html +25 -25
- nautobot/project-static/docs/development/apps/api/testing.html +25 -25
- nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +25 -25
- nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +25 -25
- nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +25 -25
- nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +25 -25
- nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +25 -25
- nautobot/project-static/docs/development/apps/api/views/base-template.html +25 -25
- nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +25 -25
- nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +25 -25
- nautobot/project-static/docs/development/apps/api/views/help-documentation.html +25 -25
- nautobot/project-static/docs/development/apps/api/views/index.html +25 -25
- nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +25 -25
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +25 -25
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +25 -25
- nautobot/project-static/docs/development/apps/api/views/notes.html +25 -25
- nautobot/project-static/docs/development/apps/api/views/rest-api.html +25 -25
- nautobot/project-static/docs/development/apps/api/views/urls.html +25 -25
- nautobot/project-static/docs/development/apps/index.html +25 -25
- nautobot/project-static/docs/development/apps/migration/code-updates.html +25 -25
- nautobot/project-static/docs/development/apps/migration/dependency-updates.html +25 -25
- nautobot/project-static/docs/development/apps/migration/from-v1.html +25 -25
- nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +25 -25
- nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +25 -25
- nautobot/project-static/docs/development/apps/migration/model-updates/global.html +25 -25
- nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +25 -25
- nautobot/project-static/docs/development/apps/porting-from-netbox.html +25 -25
- nautobot/project-static/docs/development/core/application-registry.html +25 -25
- nautobot/project-static/docs/development/core/best-practices.html +25 -25
- nautobot/project-static/docs/development/core/bootstrap-ui.html +25 -25
- nautobot/project-static/docs/development/core/caching.html +25 -25
- nautobot/project-static/docs/development/core/controllers.html +25 -25
- nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +25 -25
- nautobot/project-static/docs/development/core/generic-views.html +25 -25
- nautobot/project-static/docs/development/core/getting-started.html +25 -25
- nautobot/project-static/docs/development/core/homepage.html +25 -25
- nautobot/project-static/docs/development/core/index.html +25 -25
- nautobot/project-static/docs/development/core/model-checklist.html +25 -25
- nautobot/project-static/docs/development/core/model-features.html +25 -25
- nautobot/project-static/docs/development/core/natural-keys.html +25 -25
- nautobot/project-static/docs/development/core/navigation-menu.html +25 -25
- nautobot/project-static/docs/development/core/release-checklist.html +25 -25
- nautobot/project-static/docs/development/core/role-internals.html +25 -25
- nautobot/project-static/docs/development/core/settings.html +25 -25
- nautobot/project-static/docs/development/core/style-guide.html +25 -25
- nautobot/project-static/docs/development/core/templates.html +25 -25
- nautobot/project-static/docs/development/core/testing.html +25 -25
- nautobot/project-static/docs/development/core/user-preferences.html +25 -25
- nautobot/project-static/docs/development/index.html +25 -25
- nautobot/project-static/docs/development/jobs/index.html +25 -25
- nautobot/project-static/docs/development/jobs/migration/from-v1.html +25 -25
- nautobot/project-static/docs/index.html +30 -30
- nautobot/project-static/docs/overview/application_stack.html +31 -31
- nautobot/project-static/docs/overview/design_philosophy.html +25 -25
- nautobot/project-static/docs/release-notes/index.html +25 -25
- nautobot/project-static/docs/release-notes/version-1.0.html +25 -25
- nautobot/project-static/docs/release-notes/version-1.1.html +25 -25
- nautobot/project-static/docs/release-notes/version-1.2.html +25 -25
- nautobot/project-static/docs/release-notes/version-1.3.html +25 -25
- nautobot/project-static/docs/release-notes/version-1.4.html +25 -25
- nautobot/project-static/docs/release-notes/version-1.5.html +25 -25
- nautobot/project-static/docs/release-notes/version-1.6.html +25 -25
- nautobot/project-static/docs/release-notes/version-2.0.html +25 -25
- nautobot/project-static/docs/release-notes/version-2.1.html +25 -25
- nautobot/project-static/docs/release-notes/version-2.2.html +25 -25
- nautobot/project-static/docs/release-notes/version-2.3.html +318 -61
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +271 -542
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +25 -25
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +25 -25
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +25 -25
- nautobot/project-static/docs/user-guide/administration/configuration/index.html +25 -25
- nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +28 -28
- nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +25 -25
- nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +25 -25
- nautobot/project-static/docs/user-guide/administration/guides/caching.html +25 -25
- nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +25 -25
- nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +25 -25
- nautobot/project-static/docs/user-guide/administration/guides/permissions.html +25 -25
- nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +25 -25
- nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +25 -25
- nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +25 -25
- nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +25 -25
- nautobot/project-static/docs/user-guide/administration/installation/app-install.html +25 -25
- nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +25 -25
- nautobot/project-static/docs/user-guide/administration/installation/http-server.html +25 -25
- nautobot/project-static/docs/user-guide/administration/installation/index.html +25 -25
- nautobot/project-static/docs/user-guide/administration/installation/install_system.html +25 -25
- nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +25 -25
- nautobot/project-static/docs/user-guide/administration/installation/services.html +25 -25
- nautobot/project-static/docs/user-guide/administration/installation-extras/docker.html +25 -25
- nautobot/project-static/docs/user-guide/administration/installation-extras/health-checks.html +25 -25
- nautobot/project-static/docs/user-guide/administration/installation-extras/selinux-troubleshooting.html +25 -25
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +25 -25
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +25 -25
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +25 -25
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +25 -25
- nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +25 -25
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +25 -25
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +25 -25
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +25 -25
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +25 -25
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +25 -25
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +25 -25
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +25 -25
- nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +25 -25
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +25 -25
- nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +25 -25
- nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +25 -25
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +25 -25
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +25 -25
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +25 -25
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +25 -25
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +25 -25
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +25 -25
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +25 -25
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +25 -25
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +25 -25
- nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +25 -25
- nautobot/project-static/docs/user-guide/feature-guides/graphql.html +25 -25
- nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +25 -25
- nautobot/project-static/docs/user-guide/feature-guides/relationships.html +25 -25
- nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +25 -25
- nautobot/project-static/docs/user-guide/index.html +25 -25
- nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +25 -25
- nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +25 -25
- nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +25 -25
- nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +25 -25
- nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +25 -25
- nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +25 -25
- nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +25 -25
- nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +25 -25
- nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +25 -25
- nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +25 -25
- nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +25 -25
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +25 -25
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +25 -25
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +25 -25
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +25 -25
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +25 -25
- nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +25 -25
- nautobot/project-static/docs/user-guide/platform-functionality/note.html +25 -25
- nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +25 -25
- nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +25 -25
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +25 -25
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +25 -25
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +25 -25
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +25 -25
- nautobot/project-static/docs/user-guide/platform-functionality/role.html +25 -25
- nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +25 -25
- nautobot/project-static/docs/user-guide/platform-functionality/secret.html +25 -25
- nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +25 -25
- nautobot/project-static/docs/user-guide/platform-functionality/status.html +25 -25
- nautobot/project-static/docs/user-guide/platform-functionality/tag.html +25 -25
- nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +25 -25
- nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +25 -25
- nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +25 -25
- nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +25 -25
- nautobot/project-static/js/homepage_layout.js +3 -0
- {nautobot-2.3.1.dist-info → nautobot-2.3.3.dist-info}/METADATA +4 -4
- {nautobot-2.3.1.dist-info → nautobot-2.3.3.dist-info}/RECORD +335 -332
- nautobot/project-static/docs/assets/javascripts/bundle.fe8b6f2b.min.js +0 -29
- nautobot/project-static/docs/assets/javascripts/bundle.fe8b6f2b.min.js.map +0 -7
- nautobot/project-static/docs/assets/stylesheets/main.3cba04c6.min.css +0 -1
- nautobot/project-static/docs/assets/stylesheets/main.3cba04c6.min.css.map +0 -1
- {nautobot-2.3.1.dist-info → nautobot-2.3.3.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.3.1.dist-info → nautobot-2.3.3.dist-info}/NOTICE +0 -0
- {nautobot-2.3.1.dist-info → nautobot-2.3.3.dist-info}/WHEEL +0 -0
- {nautobot-2.3.1.dist-info → nautobot-2.3.3.dist-info}/entry_points.txt +0 -0
nautobot/extras/models/jobs.py
CHANGED
|
@@ -4,7 +4,6 @@ import contextlib
|
|
|
4
4
|
from datetime import timedelta
|
|
5
5
|
import logging
|
|
6
6
|
|
|
7
|
-
from celery import schedules
|
|
8
7
|
from celery.exceptions import NotRegistered
|
|
9
8
|
from celery.utils.log import get_logger, LoggingProxy
|
|
10
9
|
from django.conf import settings
|
|
@@ -16,7 +15,9 @@ from django.db.models import signals
|
|
|
16
15
|
from django.utils import timezone
|
|
17
16
|
from django.utils.functional import cached_property
|
|
18
17
|
from django_celery_beat.clockedschedule import clocked
|
|
18
|
+
from django_celery_beat.tzcrontab import TzAwareCrontab
|
|
19
19
|
from prometheus_client import Histogram
|
|
20
|
+
from timezone_field import TimeZoneField
|
|
20
21
|
|
|
21
22
|
from nautobot.core.celery import (
|
|
22
23
|
app,
|
|
@@ -935,6 +936,9 @@ class ScheduledJob(BaseModel):
|
|
|
935
936
|
verbose_name="Start Datetime",
|
|
936
937
|
help_text="Datetime when the schedule should begin triggering the task to run",
|
|
937
938
|
)
|
|
939
|
+
# Django always stores DateTimeField as UTC internally, but we want scheduled jobs to respect DST and similar,
|
|
940
|
+
# so we need to store the time zone the job was scheduled under as well.
|
|
941
|
+
time_zone = TimeZoneField(default=timezone.get_default_timezone_name)
|
|
938
942
|
# todoindex:
|
|
939
943
|
enabled = models.BooleanField(
|
|
940
944
|
default=True,
|
|
@@ -1005,12 +1009,15 @@ class ScheduledJob(BaseModel):
|
|
|
1005
1009
|
def __str__(self):
|
|
1006
1010
|
return f"{self.name}: {self.interval}"
|
|
1007
1011
|
|
|
1012
|
+
class Meta:
|
|
1013
|
+
ordering = ["name"]
|
|
1014
|
+
|
|
1008
1015
|
def save(self, *args, **kwargs):
|
|
1009
1016
|
self.queue = self.queue or ""
|
|
1010
1017
|
# make sure non-valid crontab doesn't get saved
|
|
1011
1018
|
if self.interval == JobExecutionType.TYPE_CUSTOM:
|
|
1012
1019
|
try:
|
|
1013
|
-
self.get_crontab(self.crontab)
|
|
1020
|
+
self.get_crontab(self.crontab, tz=self.time_zone)
|
|
1014
1021
|
except Exception as e:
|
|
1015
1022
|
raise ValidationError({"crontab": e})
|
|
1016
1023
|
if not self.enabled:
|
|
@@ -1055,7 +1062,7 @@ class ScheduledJob(BaseModel):
|
|
|
1055
1062
|
return timezone.now() + timedelta(seconds=15)
|
|
1056
1063
|
|
|
1057
1064
|
@classmethod
|
|
1058
|
-
def get_crontab(cls, crontab):
|
|
1065
|
+
def get_crontab(cls, crontab, tz=None):
|
|
1059
1066
|
"""
|
|
1060
1067
|
Wrapper method translates crontab syntax to Celery crontab.
|
|
1061
1068
|
|
|
@@ -1068,13 +1075,17 @@ class ScheduledJob(BaseModel):
|
|
|
1068
1075
|
|
|
1069
1076
|
No support for Last (L), Weekday (W), Number symbol (#), Question mark (?), and special @ strings.
|
|
1070
1077
|
"""
|
|
1078
|
+
if not tz:
|
|
1079
|
+
tz = timezone.get_default_timezone()
|
|
1071
1080
|
minute, hour, day_of_month, month_of_year, day_of_week = crontab.split(" ")
|
|
1072
|
-
|
|
1081
|
+
|
|
1082
|
+
return TzAwareCrontab(
|
|
1073
1083
|
minute=minute,
|
|
1074
1084
|
hour=hour,
|
|
1075
1085
|
day_of_month=day_of_month,
|
|
1076
1086
|
month_of_year=month_of_year,
|
|
1077
1087
|
day_of_week=day_of_week,
|
|
1088
|
+
tz=tz,
|
|
1078
1089
|
)
|
|
1079
1090
|
|
|
1080
1091
|
@classmethod
|
|
@@ -1116,13 +1127,13 @@ class ScheduledJob(BaseModel):
|
|
|
1116
1127
|
"""
|
|
1117
1128
|
|
|
1118
1129
|
if interval == JobExecutionType.TYPE_IMMEDIATELY:
|
|
1119
|
-
start_time = timezone.
|
|
1130
|
+
start_time = timezone.localtime()
|
|
1120
1131
|
name = name or f"{job_model.name} - {start_time}"
|
|
1121
1132
|
elif interval == JobExecutionType.TYPE_CUSTOM:
|
|
1122
1133
|
if start_time is None:
|
|
1123
1134
|
# "start_time" is checked against models.ScheduledJob.earliest_possible_time()
|
|
1124
1135
|
# which returns timezone.now() + timedelta(seconds=15)
|
|
1125
|
-
start_time = timezone.
|
|
1136
|
+
start_time = timezone.localtime() + timedelta(seconds=20)
|
|
1126
1137
|
|
|
1127
1138
|
celery_kwargs = {
|
|
1128
1139
|
"nautobot_job_profile": profile,
|
|
@@ -1145,6 +1156,7 @@ class ScheduledJob(BaseModel):
|
|
|
1145
1156
|
task=job_model.class_path,
|
|
1146
1157
|
job_model=job_model,
|
|
1147
1158
|
start_time=start_time,
|
|
1159
|
+
time_zone=start_time.tzinfo,
|
|
1148
1160
|
description=f"Nautobot job {name} scheduled by {user} for {start_time}",
|
|
1149
1161
|
kwargs=job_kwargs,
|
|
1150
1162
|
celery_kwargs=celery_kwargs,
|
|
@@ -1159,15 +1171,16 @@ class ScheduledJob(BaseModel):
|
|
|
1159
1171
|
return scheduled_job
|
|
1160
1172
|
|
|
1161
1173
|
def to_cron(self):
|
|
1162
|
-
|
|
1174
|
+
tz = self.time_zone
|
|
1175
|
+
t = self.start_time.astimezone(tz)
|
|
1163
1176
|
if self.interval == JobExecutionType.TYPE_HOURLY:
|
|
1164
|
-
return
|
|
1177
|
+
return TzAwareCrontab(minute=t.minute, tz=tz)
|
|
1165
1178
|
elif self.interval == JobExecutionType.TYPE_DAILY:
|
|
1166
|
-
return
|
|
1179
|
+
return TzAwareCrontab(minute=t.minute, hour=t.hour, tz=tz)
|
|
1167
1180
|
elif self.interval == JobExecutionType.TYPE_WEEKLY:
|
|
1168
|
-
return
|
|
1181
|
+
return TzAwareCrontab(minute=t.minute, hour=t.hour, day_of_week=t.strftime("%w"), tz=tz)
|
|
1169
1182
|
elif self.interval == JobExecutionType.TYPE_CUSTOM:
|
|
1170
|
-
return self.get_crontab(self.crontab)
|
|
1183
|
+
return self.get_crontab(self.crontab, tz=tz)
|
|
1171
1184
|
raise ValueError(f"I do not know to convert {self.interval} to a Cronjob!")
|
|
1172
1185
|
|
|
1173
1186
|
|
nautobot/extras/tables.py
CHANGED
|
@@ -110,6 +110,10 @@ JOB_BUTTONS = """
|
|
|
110
110
|
<a href="{% url 'extras:jobresult_list' %}?job_model={{ record.name | urlencode }}" class="btn btn-default btn-xs" title="Job Results"><i class="mdi mdi-format-list-bulleted" aria-hidden="true"></i></a>
|
|
111
111
|
"""
|
|
112
112
|
|
|
113
|
+
SCHEDULED_JOB_BUTTONS = """
|
|
114
|
+
<a href="{% url 'extras:jobresult_list' %}?scheduled_job={{ record.name | urlencode }}" class="btn btn-default btn-xs" title="Job Results"><i class="mdi mdi-format-list-bulleted" aria-hidden="true"></i></a>
|
|
115
|
+
"""
|
|
116
|
+
|
|
113
117
|
OBJECTCHANGE_OBJECT = """
|
|
114
118
|
{% if record.changed_object and record.changed_object.get_absolute_url %}
|
|
115
119
|
<a href="{{ record.changed_object.get_absolute_url }}">{{ record.object_repr }}</a>
|
|
@@ -835,6 +839,10 @@ class JobResultTable(BaseTable):
|
|
|
835
839
|
orderable=False,
|
|
836
840
|
attrs={"td": {"class": "text-nowrap report-stats"}},
|
|
837
841
|
)
|
|
842
|
+
scheduled_job = tables.Column(
|
|
843
|
+
linkify=True,
|
|
844
|
+
verbose_name="Scheduled Job",
|
|
845
|
+
)
|
|
838
846
|
actions = tables.TemplateColumn(
|
|
839
847
|
template_code="""
|
|
840
848
|
{% load helpers %}
|
|
@@ -884,6 +892,7 @@ class JobResultTable(BaseTable):
|
|
|
884
892
|
"date_created",
|
|
885
893
|
"name",
|
|
886
894
|
"job_model",
|
|
895
|
+
"scheduled_job",
|
|
887
896
|
"duration",
|
|
888
897
|
"date_done",
|
|
889
898
|
"user",
|
|
@@ -1038,16 +1047,37 @@ class NoteTable(BaseTable):
|
|
|
1038
1047
|
|
|
1039
1048
|
class ScheduledJobTable(BaseTable):
|
|
1040
1049
|
pk = ToggleColumn()
|
|
1041
|
-
name = tables.
|
|
1050
|
+
name = tables.Column(linkify=True)
|
|
1042
1051
|
job_model = tables.Column(verbose_name="Job", linkify=True)
|
|
1043
1052
|
interval = tables.Column(verbose_name="Execution Type")
|
|
1044
|
-
start_time = tables.
|
|
1045
|
-
last_run_at = tables.
|
|
1053
|
+
start_time = tables.DateTimeColumn(verbose_name="First Run", format=settings.SHORT_DATETIME_FORMAT)
|
|
1054
|
+
last_run_at = tables.DateTimeColumn(verbose_name="Most Recent Run", format=settings.SHORT_DATETIME_FORMAT)
|
|
1055
|
+
crontab = tables.Column()
|
|
1046
1056
|
total_run_count = tables.Column(verbose_name="Total Run Count")
|
|
1057
|
+
actions = ButtonsColumn(ScheduledJob, buttons=("delete"), prepend_template=SCHEDULED_JOB_BUTTONS)
|
|
1047
1058
|
|
|
1048
1059
|
class Meta(BaseTable.Meta):
|
|
1049
1060
|
model = ScheduledJob
|
|
1050
|
-
fields = (
|
|
1061
|
+
fields = (
|
|
1062
|
+
"pk",
|
|
1063
|
+
"name",
|
|
1064
|
+
"total_run_count",
|
|
1065
|
+
"job_model",
|
|
1066
|
+
"interval",
|
|
1067
|
+
"start_time",
|
|
1068
|
+
"last_run_at",
|
|
1069
|
+
"crontab",
|
|
1070
|
+
"time_zone",
|
|
1071
|
+
"actions",
|
|
1072
|
+
)
|
|
1073
|
+
default_columns = (
|
|
1074
|
+
"pk",
|
|
1075
|
+
"name",
|
|
1076
|
+
"job_model",
|
|
1077
|
+
"interval",
|
|
1078
|
+
"last_run_at",
|
|
1079
|
+
"actions",
|
|
1080
|
+
)
|
|
1051
1081
|
|
|
1052
1082
|
|
|
1053
1083
|
class ScheduledJobApprovalQueueTable(BaseTable):
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
{% load helpers %}
|
|
5
5
|
{% load perms %}
|
|
6
6
|
{% load plugins %}
|
|
7
|
+
{% load tz %}
|
|
7
8
|
|
|
8
9
|
{% block buttons %}
|
|
9
10
|
{% plugin_buttons object %}
|
|
@@ -94,13 +95,23 @@
|
|
|
94
95
|
<tr>
|
|
95
96
|
<td>Start Time</td>
|
|
96
97
|
<td>
|
|
97
|
-
{{ object.start_time }}
|
|
98
|
+
{{ object.start_time|timezone:object.time_zone|date:'Y-m-d H:i:s T' }}
|
|
99
|
+
{% if default_time_zone != object.time_zone %}
|
|
100
|
+
<br>{{ object.start_time|timezone:default_time_zone|date:'Y-m-d H:i:s T' }}
|
|
101
|
+
{% endif %}
|
|
98
102
|
</td>
|
|
99
103
|
</tr>
|
|
100
104
|
<tr>
|
|
101
105
|
<td>Last Run At</td>
|
|
102
106
|
<td>
|
|
103
|
-
{
|
|
107
|
+
{% if object.last_run_at %}
|
|
108
|
+
{{ object.last_run_at|timezone:object.time_zone|date:'Y-m-d H:i:s T' }}
|
|
109
|
+
{% if default_time_zone != object.time_zone %}
|
|
110
|
+
<br>{{ object.last_run_at|timezone:default_time_zone|date:'Y-m-d H:i:s T' }}
|
|
111
|
+
{% endif %}
|
|
112
|
+
{% else %}
|
|
113
|
+
{{ object.last_run_at|placeholder }}
|
|
114
|
+
{% endif %}
|
|
104
115
|
</td>
|
|
105
116
|
</tr>
|
|
106
117
|
<tr>
|
|
@@ -12,6 +12,11 @@ from django.urls import reverse
|
|
|
12
12
|
from django.utils.timezone import make_aware, now
|
|
13
13
|
from rest_framework import status
|
|
14
14
|
|
|
15
|
+
try:
|
|
16
|
+
from zoneinfo import ZoneInfo
|
|
17
|
+
except ImportError: # Python 3.8
|
|
18
|
+
from backports.zoneinfo import ZoneInfo
|
|
19
|
+
|
|
15
20
|
from nautobot.core.choices import ColorChoices
|
|
16
21
|
from nautobot.core.models.fields import slugify_dashes_to_underscores
|
|
17
22
|
from nautobot.core.testing import APITestCase, APIViewTestCases
|
|
@@ -1583,7 +1588,7 @@ class JobTest(
|
|
|
1583
1588
|
"schedule": {
|
|
1584
1589
|
"name": "test",
|
|
1585
1590
|
"interval": "future",
|
|
1586
|
-
"start_time": str(
|
|
1591
|
+
"start_time": str(now() + timedelta(minutes=1)),
|
|
1587
1592
|
},
|
|
1588
1593
|
}
|
|
1589
1594
|
|
|
@@ -1780,7 +1785,7 @@ class JobTest(
|
|
|
1780
1785
|
"var2": "Ground control to Major Tom",
|
|
1781
1786
|
"var23": "Commencing countdown, engines on",
|
|
1782
1787
|
"var1": test_file,
|
|
1783
|
-
"_schedule_start_time": str(
|
|
1788
|
+
"_schedule_start_time": str(now() + timedelta(minutes=1)),
|
|
1784
1789
|
"_schedule_interval": "future",
|
|
1785
1790
|
"_schedule_name": "test",
|
|
1786
1791
|
}
|
|
@@ -1799,7 +1804,7 @@ class JobTest(
|
|
|
1799
1804
|
data = {
|
|
1800
1805
|
"data": {"var1": "x", "var2": 1, "var3": False, "var4": d.pk},
|
|
1801
1806
|
"schedule": {
|
|
1802
|
-
"start_time": str(
|
|
1807
|
+
"start_time": str(now() + timedelta(minutes=1)),
|
|
1803
1808
|
"interval": "future",
|
|
1804
1809
|
"name": "test",
|
|
1805
1810
|
},
|
|
@@ -1838,7 +1843,7 @@ class JobTest(
|
|
|
1838
1843
|
data = {
|
|
1839
1844
|
"data": {},
|
|
1840
1845
|
"schedule": {
|
|
1841
|
-
"start_time": str(
|
|
1846
|
+
"start_time": str(now() + timedelta(minutes=1)),
|
|
1842
1847
|
"interval": "future",
|
|
1843
1848
|
"name": "test",
|
|
1844
1849
|
},
|
|
@@ -1914,7 +1919,7 @@ class JobTest(
|
|
|
1914
1919
|
data = {
|
|
1915
1920
|
"data": {"var1": "x", "var2": 1, "var3": False, "var4": d.pk},
|
|
1916
1921
|
"schedule": {
|
|
1917
|
-
"start_time": str(
|
|
1922
|
+
"start_time": str(now() - timedelta(minutes=1)),
|
|
1918
1923
|
"interval": "future",
|
|
1919
1924
|
"name": "test",
|
|
1920
1925
|
},
|
|
@@ -1933,7 +1938,7 @@ class JobTest(
|
|
|
1933
1938
|
data = {
|
|
1934
1939
|
"data": {"var1": "x", "var2": 1, "var3": False, "var4": d.pk},
|
|
1935
1940
|
"schedule": {
|
|
1936
|
-
"start_time": str(
|
|
1941
|
+
"start_time": str(now() + timedelta(minutes=1)),
|
|
1937
1942
|
"interval": "hourly",
|
|
1938
1943
|
"name": "test",
|
|
1939
1944
|
},
|
|
@@ -2438,30 +2443,24 @@ class ScheduledJobTest(
|
|
|
2438
2443
|
name="test2",
|
|
2439
2444
|
task="pass.TestPass",
|
|
2440
2445
|
job_model=job_model,
|
|
2441
|
-
interval=JobExecutionType.
|
|
2446
|
+
interval=JobExecutionType.TYPE_DAILY,
|
|
2442
2447
|
user=user,
|
|
2443
2448
|
approval_required=True,
|
|
2444
|
-
start_time=
|
|
2449
|
+
start_time=datetime(2020, 1, 23, 12, 34, 56, tzinfo=ZoneInfo("America/New_York")),
|
|
2450
|
+
time_zone=ZoneInfo("America/New_York"),
|
|
2445
2451
|
)
|
|
2446
2452
|
ScheduledJob.objects.create(
|
|
2447
2453
|
name="test3",
|
|
2448
2454
|
task="pass.TestPass",
|
|
2449
2455
|
job_model=job_model,
|
|
2450
|
-
interval=JobExecutionType.
|
|
2456
|
+
interval=JobExecutionType.TYPE_CUSTOM,
|
|
2457
|
+
crontab="34 12 * * *",
|
|
2458
|
+
enabled=False,
|
|
2451
2459
|
user=user,
|
|
2452
2460
|
approval_required=True,
|
|
2453
2461
|
start_time=now(),
|
|
2454
2462
|
)
|
|
2455
2463
|
|
|
2456
|
-
# TODO: Unskip after resolving #2908, #2909
|
|
2457
|
-
@skip("DRF's built-in OrderingFilter triggering natural key attribute error in our base")
|
|
2458
|
-
def test_list_objects_ascending_ordered(self):
|
|
2459
|
-
pass
|
|
2460
|
-
|
|
2461
|
-
@skip("DRF's built-in OrderingFilter triggering natural key attribute error in our base")
|
|
2462
|
-
def test_list_objects_descending_ordered(self):
|
|
2463
|
-
pass
|
|
2464
|
-
|
|
2465
2464
|
|
|
2466
2465
|
class JobApprovalTest(APITestCase):
|
|
2467
2466
|
@classmethod
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from unittest import mock
|
|
2
|
+
|
|
1
3
|
from django.contrib.auth import get_user_model
|
|
2
4
|
from django.contrib.contenttypes.models import ContentType
|
|
3
5
|
from django.test import TestCase
|
|
@@ -72,7 +74,9 @@ class WebRequestContextTestCase(TestCase):
|
|
|
72
74
|
self.assertEqual(oc_list[0].changed_object, location)
|
|
73
75
|
self.assertEqual(oc_list[0].action, ObjectChangeActionChoices.ACTION_CREATE)
|
|
74
76
|
|
|
75
|
-
|
|
77
|
+
@mock.patch("nautobot.extras.jobs.enqueue_job_hooks")
|
|
78
|
+
@mock.patch("nautobot.extras.context_managers.enqueue_webhooks")
|
|
79
|
+
def test_create_then_delete(self, mock_enqueue_webhooks, mock_enqueue_job_hooks):
|
|
76
80
|
"""Test that a create followed by a delete is logged as two changes"""
|
|
77
81
|
location_type = LocationType.objects.get(name="Campus")
|
|
78
82
|
location_status = Status.objects.get_for_model(Location).first()
|
|
@@ -88,6 +92,8 @@ class WebRequestContextTestCase(TestCase):
|
|
|
88
92
|
self.assertEqual(len(oc_list), 2)
|
|
89
93
|
self.assertEqual(oc_list[0].action, ObjectChangeActionChoices.ACTION_DELETE)
|
|
90
94
|
self.assertEqual(oc_list[1].action, ObjectChangeActionChoices.ACTION_CREATE)
|
|
95
|
+
mock_enqueue_job_hooks.assert_has_calls([mock.call(oc_list[0]), mock.call(oc_list[1])])
|
|
96
|
+
mock_enqueue_webhooks.assert_has_calls([mock.call(oc_list[0]), mock.call(oc_list[1])])
|
|
91
97
|
|
|
92
98
|
def test_update_then_delete(self):
|
|
93
99
|
"""Test that an update followed by a delete is logged as a single delete"""
|
|
@@ -179,21 +185,30 @@ class WebRequestContextTestCase(TestCase):
|
|
|
179
185
|
with self.subTest():
|
|
180
186
|
self.assertEqual(oc_list[0].change_context_detail, "test_change_log_context")
|
|
181
187
|
|
|
182
|
-
|
|
188
|
+
@mock.patch("nautobot.extras.webhooks.process_webhook.apply_async")
|
|
189
|
+
def test_change_webhook_enqueued(self, mock_apply_async):
|
|
183
190
|
"""Test that the webhook resides on the queue"""
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
191
|
+
with web_request_context(self.user):
|
|
192
|
+
location = Location(
|
|
193
|
+
name="Test Location 2",
|
|
194
|
+
location_type=LocationType.objects.get(name="Campus"),
|
|
195
|
+
status=Status.objects.get_for_model(Location).first(),
|
|
196
|
+
)
|
|
197
|
+
location.save()
|
|
190
198
|
|
|
191
199
|
# Verify that a job was queued for the object creation webhook
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
200
|
+
oc_list = get_changes_for_model(location)
|
|
201
|
+
mock_apply_async.assert_called_once()
|
|
202
|
+
call_args = mock_apply_async.call_args.kwargs["args"]
|
|
203
|
+
self.assertEqual(8, len(call_args), call_args)
|
|
204
|
+
self.assertEqual(call_args[0], Webhook.objects.get(type_create=True).pk)
|
|
205
|
+
self.assertEqual(call_args[1], oc_list[0].object_data_v2)
|
|
206
|
+
self.assertEqual(call_args[2], "location")
|
|
207
|
+
self.assertEqual(call_args[3], "create")
|
|
208
|
+
self.assertIsInstance(call_args[4], str) # str(timezone.now())
|
|
209
|
+
self.assertEqual(call_args[5], self.user.username)
|
|
210
|
+
self.assertEqual(call_args[6], oc_list[0].request_id)
|
|
211
|
+
self.assertEqual(call_args[7], oc_list[0].get_snapshots())
|
|
197
212
|
|
|
198
213
|
|
|
199
214
|
class WebRequestContextTransactionTestCase(TransactionTestCase):
|
|
@@ -282,7 +297,8 @@ class BulkEditDeleteChangeLogging(TestCase):
|
|
|
282
297
|
self.assertIsNone(snapshots["differences"]["removed"])
|
|
283
298
|
self.assertEqual(snapshots["differences"]["added"]["description"], "changed")
|
|
284
299
|
|
|
285
|
-
|
|
300
|
+
@mock.patch("nautobot.extras.jobs.import_jobs")
|
|
301
|
+
def test_bulk_edit(self, mock_import_jobs):
|
|
286
302
|
"""Test that edits to multiple objects are correctly logged"""
|
|
287
303
|
location_type = LocationType.objects.get(name="Campus")
|
|
288
304
|
location_status = Status.objects.get_for_model(Location).first()
|
|
@@ -309,6 +325,9 @@ class BulkEditDeleteChangeLogging(TestCase):
|
|
|
309
325
|
self.assertIsNone(snapshots["differences"]["removed"])
|
|
310
326
|
self.assertEqual(snapshots["differences"]["added"]["description"], "changed")
|
|
311
327
|
|
|
328
|
+
# Check for regression of https://github.com/nautobot/nautobot/issues/6203
|
|
329
|
+
mock_import_jobs.assert_called_once()
|
|
330
|
+
|
|
312
331
|
def test_bulk_edit_device_type_software_image_file(self):
|
|
313
332
|
"""Test that bulk edits to null does not cause integrity error"""
|
|
314
333
|
manufacturer = Manufacturer.objects.create(name="Test")
|
|
@@ -494,6 +494,17 @@ class DynamicGroupModelTest(DynamicGroupTestBase): # TODO: BaseModelTestCase mi
|
|
|
494
494
|
self.assertIsInstance(fields["cluster"], MultiMatchModelMultipleChoiceField)
|
|
495
495
|
self.assertIsInstance(fields["cluster"].widget, APISelectMultiple)
|
|
496
496
|
|
|
497
|
+
def test_map_filter_fields_with_custom_filter_method(self):
|
|
498
|
+
"""
|
|
499
|
+
Test that extension_filters with custom methods can be concatenated into `generate_query_{filter_method}`
|
|
500
|
+
and _map_filter_fields method doesn't brake on string concatenation.
|
|
501
|
+
This is a regression test for issue #6184.
|
|
502
|
+
"""
|
|
503
|
+
tenant_content_type = ContentType.objects.get_for_model(Tenant)
|
|
504
|
+
# using Tenant as example app has a custom filter with a custom method.
|
|
505
|
+
group = DynamicGroup(name="tenant", content_type=tenant_content_type)
|
|
506
|
+
self.assertIsInstance(group._map_filter_fields, dict)
|
|
507
|
+
|
|
497
508
|
def test_map_filter_fields_skip_missing(self):
|
|
498
509
|
"""
|
|
499
510
|
Test that missing fields are skipped in `DynamicGroup._map_filter_fields`
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from datetime import datetime
|
|
1
2
|
import uuid
|
|
2
3
|
|
|
3
4
|
from django.contrib.auth import get_user_model
|
|
@@ -5,6 +6,12 @@ from django.contrib.contenttypes.models import ContentType
|
|
|
5
6
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
|
6
7
|
from django.db.models import Q
|
|
7
8
|
from django.test import override_settings, RequestFactory
|
|
9
|
+
from django.utils.timezone import now
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from zoneinfo import ZoneInfo
|
|
13
|
+
except ImportError: # Python 3.8
|
|
14
|
+
from backports.zoneinfo import ZoneInfo
|
|
8
15
|
|
|
9
16
|
from nautobot.core.choices import ColorChoices
|
|
10
17
|
from nautobot.core.testing import FilterTestCases
|
|
@@ -22,6 +29,7 @@ from nautobot.dcim.models import (
|
|
|
22
29
|
from nautobot.extras.choices import (
|
|
23
30
|
CustomFieldTypeChoices,
|
|
24
31
|
DynamicGroupTypeChoices,
|
|
32
|
+
JobExecutionType,
|
|
25
33
|
JobResultStatusChoices,
|
|
26
34
|
MetadataTypeDataTypeChoices,
|
|
27
35
|
ObjectChangeActionChoices,
|
|
@@ -93,6 +101,7 @@ from nautobot.extras.models import (
|
|
|
93
101
|
RelationshipAssociation,
|
|
94
102
|
Role,
|
|
95
103
|
SavedView,
|
|
104
|
+
ScheduledJob,
|
|
96
105
|
Secret,
|
|
97
106
|
SecretsGroup,
|
|
98
107
|
SecretsGroupAssociation,
|
|
@@ -1124,13 +1133,49 @@ class JobResultFilterSetTestCase(FilterTestCases.FilterTestCase):
|
|
|
1124
1133
|
def setUpTestData(cls):
|
|
1125
1134
|
jobs = Job.objects.all()[:3]
|
|
1126
1135
|
cls.jobs = jobs
|
|
1136
|
+
user = User.objects.create(username="user1", is_active=True)
|
|
1137
|
+
job_model = Job.objects.get_for_class_path("pass.TestPass")
|
|
1138
|
+
scheduled_jobs = [
|
|
1139
|
+
ScheduledJob.objects.create(
|
|
1140
|
+
name="test1",
|
|
1141
|
+
task="pass.TestPass",
|
|
1142
|
+
job_model=job_model,
|
|
1143
|
+
interval=JobExecutionType.TYPE_IMMEDIATELY,
|
|
1144
|
+
user=user,
|
|
1145
|
+
approval_required=True,
|
|
1146
|
+
start_time=now(),
|
|
1147
|
+
),
|
|
1148
|
+
ScheduledJob.objects.create(
|
|
1149
|
+
name="test2",
|
|
1150
|
+
task="pass.TestPass",
|
|
1151
|
+
job_model=job_model,
|
|
1152
|
+
interval=JobExecutionType.TYPE_DAILY,
|
|
1153
|
+
user=user,
|
|
1154
|
+
approval_required=True,
|
|
1155
|
+
start_time=datetime(2020, 1, 23, 12, 34, 56, tzinfo=ZoneInfo("America/New_York")),
|
|
1156
|
+
time_zone=ZoneInfo("America/New_York"),
|
|
1157
|
+
),
|
|
1158
|
+
ScheduledJob.objects.create(
|
|
1159
|
+
name="test3",
|
|
1160
|
+
task="pass.TestPass",
|
|
1161
|
+
job_model=job_model,
|
|
1162
|
+
interval=JobExecutionType.TYPE_CUSTOM,
|
|
1163
|
+
crontab="34 12 * * *",
|
|
1164
|
+
enabled=False,
|
|
1165
|
+
user=user,
|
|
1166
|
+
approval_required=True,
|
|
1167
|
+
start_time=now(),
|
|
1168
|
+
),
|
|
1169
|
+
]
|
|
1170
|
+
cls.scheduled_jobs = scheduled_jobs
|
|
1127
1171
|
user = UserFactory.create()
|
|
1128
|
-
for job in jobs:
|
|
1172
|
+
for idx, job in enumerate(jobs):
|
|
1129
1173
|
JobResult.objects.create(
|
|
1130
1174
|
job_model=job,
|
|
1131
1175
|
name=job.class_path,
|
|
1132
1176
|
user=user,
|
|
1133
1177
|
status=JobResultStatusChoices.STATUS_STARTED,
|
|
1178
|
+
scheduled_job=scheduled_jobs[idx],
|
|
1134
1179
|
)
|
|
1135
1180
|
|
|
1136
1181
|
def test_job_model(self):
|
|
@@ -1144,6 +1189,17 @@ class JobResultFilterSetTestCase(FilterTestCases.FilterTestCase):
|
|
|
1144
1189
|
self.filterset(params, self.queryset).qs, self.queryset.filter(job_model__in=jobs).distinct()
|
|
1145
1190
|
)
|
|
1146
1191
|
|
|
1192
|
+
def test_scheduled_job(self):
|
|
1193
|
+
scheduled_jobs = list(self.scheduled_jobs[:2])
|
|
1194
|
+
filter_params = [
|
|
1195
|
+
{"scheduled_job": [scheduled_jobs[0].pk, scheduled_jobs[1].name]},
|
|
1196
|
+
]
|
|
1197
|
+
for params in filter_params:
|
|
1198
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
1199
|
+
self.filterset(params, self.queryset).qs,
|
|
1200
|
+
self.queryset.filter(scheduled_job__in=scheduled_jobs).distinct(),
|
|
1201
|
+
)
|
|
1202
|
+
|
|
1147
1203
|
|
|
1148
1204
|
class JobHookFilterSetTestCase(FilterTestCases.NameOnlyFilterTestCase):
|
|
1149
1205
|
queryset = JobHook.objects.all()
|