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
|
@@ -14,8 +14,15 @@ from django.db.models import ProtectedError
|
|
|
14
14
|
from django.db.utils import IntegrityError
|
|
15
15
|
from django.test import override_settings
|
|
16
16
|
from django.test.utils import isolate_apps
|
|
17
|
-
from django.utils.timezone import now
|
|
17
|
+
from django.utils.timezone import get_default_timezone, now
|
|
18
|
+
from django_celery_beat.tzcrontab import TzAwareCrontab
|
|
18
19
|
from jinja2.exceptions import TemplateAssertionError, TemplateSyntaxError
|
|
20
|
+
import time_machine
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from zoneinfo import ZoneInfo
|
|
24
|
+
except ImportError: # python 3.8
|
|
25
|
+
from backports.zoneinfo import ZoneInfo
|
|
19
26
|
|
|
20
27
|
from nautobot.circuits.models import CircuitType
|
|
21
28
|
from nautobot.core.choices import ColorChoices
|
|
@@ -30,6 +37,7 @@ from nautobot.dcim.models import (
|
|
|
30
37
|
Platform,
|
|
31
38
|
)
|
|
32
39
|
from nautobot.extras.choices import (
|
|
40
|
+
JobExecutionType,
|
|
33
41
|
JobResultStatusChoices,
|
|
34
42
|
LogLevelChoices,
|
|
35
43
|
MetadataTypeDataTypeChoices,
|
|
@@ -65,6 +73,7 @@ from nautobot.extras.models import (
|
|
|
65
73
|
ObjectMetadata,
|
|
66
74
|
Role,
|
|
67
75
|
SavedView,
|
|
76
|
+
ScheduledJob,
|
|
68
77
|
Secret,
|
|
69
78
|
SecretsGroup,
|
|
70
79
|
SecretsGroupAssociation,
|
|
@@ -1082,12 +1091,15 @@ class JobModelTest(ModelTestCases.BaseModelTestCase):
|
|
|
1082
1091
|
cls.app_job = JobModel.objects.get(job_class_name="ExampleJob")
|
|
1083
1092
|
|
|
1084
1093
|
def test_job_class(self):
|
|
1094
|
+
self.assertIsNotNone(self.local_job.job_class)
|
|
1085
1095
|
self.assertEqual(self.local_job.job_class.description, "Validate job import")
|
|
1086
1096
|
|
|
1097
|
+
self.assertIsNotNone(self.app_job.job_class)
|
|
1087
1098
|
self.assertEqual(self.app_job.job_class, ExampleJob)
|
|
1088
1099
|
|
|
1089
1100
|
def test_class_path(self):
|
|
1090
1101
|
self.assertEqual(self.local_job.class_path, "pass.TestPass")
|
|
1102
|
+
self.assertIsNotNone(self.local_job.job_class)
|
|
1091
1103
|
self.assertEqual(self.local_job.class_path, self.local_job.job_class.class_path)
|
|
1092
1104
|
|
|
1093
1105
|
self.assertEqual(self.app_job.class_path, "example_app.jobs.ExampleJob")
|
|
@@ -1109,6 +1121,7 @@ class JobModelTest(ModelTestCases.BaseModelTestCase):
|
|
|
1109
1121
|
self.assertTrue(job_model.enabled)
|
|
1110
1122
|
else:
|
|
1111
1123
|
self.assertFalse(job_model.enabled)
|
|
1124
|
+
self.assertIsNotNone(job_model.job_class)
|
|
1112
1125
|
for field_name in JOB_OVERRIDABLE_FIELDS:
|
|
1113
1126
|
if field_name == "name" and "duplicate_name" in job_model.job_class.__module__:
|
|
1114
1127
|
pass # name field for test_duplicate_name jobs tested in test_duplicate_job_name below
|
|
@@ -1164,6 +1177,7 @@ class JobModelTest(ModelTestCases.BaseModelTestCase):
|
|
|
1164
1177
|
setattr(self.job_containing_sensitive_variables, f"{field_name}_override", False)
|
|
1165
1178
|
self.job_containing_sensitive_variables.validated_save()
|
|
1166
1179
|
self.job_containing_sensitive_variables.refresh_from_db()
|
|
1180
|
+
self.assertIsNotNone(self.job_containing_sensitive_variables.job_class)
|
|
1167
1181
|
for field_name in overridden_attrs:
|
|
1168
1182
|
self.assertEqual(
|
|
1169
1183
|
getattr(self.job_containing_sensitive_variables, field_name),
|
|
@@ -1790,6 +1804,295 @@ class SavedViewTest(ModelTestCases.BaseModelTestCase):
|
|
|
1790
1804
|
self.assertEqual(self.ipaddress_global_sv.is_shared, True)
|
|
1791
1805
|
|
|
1792
1806
|
|
|
1807
|
+
@override_settings(TIME_ZONE="UTC")
|
|
1808
|
+
class ScheduledJobTest(ModelTestCases.BaseModelTestCase):
|
|
1809
|
+
"""Tests for the `ScheduledJob` model class."""
|
|
1810
|
+
|
|
1811
|
+
model = ScheduledJob
|
|
1812
|
+
|
|
1813
|
+
def setUp(self):
|
|
1814
|
+
self.user = User.objects.create_user(username="scheduledjobuser")
|
|
1815
|
+
self.job_model = JobModel.objects.get(name="TestPass")
|
|
1816
|
+
|
|
1817
|
+
self.daily_utc_job = ScheduledJob.objects.create(
|
|
1818
|
+
name="Daily UTC Job",
|
|
1819
|
+
task="pass.TestPass",
|
|
1820
|
+
job_model=self.job_model,
|
|
1821
|
+
interval=JobExecutionType.TYPE_DAILY,
|
|
1822
|
+
start_time=datetime(year=2050, month=1, day=22, hour=17, minute=0, tzinfo=get_default_timezone()),
|
|
1823
|
+
time_zone=get_default_timezone(),
|
|
1824
|
+
)
|
|
1825
|
+
self.daily_est_job = ScheduledJob.objects.create(
|
|
1826
|
+
name="Daily EST Job",
|
|
1827
|
+
task="pass.TestPass",
|
|
1828
|
+
job_model=self.job_model,
|
|
1829
|
+
interval=JobExecutionType.TYPE_DAILY,
|
|
1830
|
+
start_time=datetime(year=2050, month=1, day=22, hour=17, minute=0, tzinfo=ZoneInfo("America/New_York")),
|
|
1831
|
+
time_zone=ZoneInfo("America/New_York"),
|
|
1832
|
+
)
|
|
1833
|
+
self.crontab_utc_job = ScheduledJob.create_schedule(
|
|
1834
|
+
job_model=self.job_model,
|
|
1835
|
+
user=self.user,
|
|
1836
|
+
name="Crontab UTC Job",
|
|
1837
|
+
interval=JobExecutionType.TYPE_CUSTOM,
|
|
1838
|
+
crontab="0 17 * * *",
|
|
1839
|
+
)
|
|
1840
|
+
self.crontab_est_job = ScheduledJob.objects.create(
|
|
1841
|
+
name="Crontab EST Job",
|
|
1842
|
+
task="pass.TestPass",
|
|
1843
|
+
job_model=self.job_model,
|
|
1844
|
+
interval=JobExecutionType.TYPE_CUSTOM,
|
|
1845
|
+
start_time=datetime(year=2050, month=1, day=22, hour=17, minute=0, tzinfo=ZoneInfo("America/New_York")),
|
|
1846
|
+
time_zone=ZoneInfo("America/New_York"),
|
|
1847
|
+
crontab="0 17 * * *",
|
|
1848
|
+
)
|
|
1849
|
+
self.one_off_utc_job = ScheduledJob.objects.create(
|
|
1850
|
+
name="One-off UTC Job",
|
|
1851
|
+
task="pass.TestPass",
|
|
1852
|
+
job_model=self.job_model,
|
|
1853
|
+
interval=JobExecutionType.TYPE_FUTURE,
|
|
1854
|
+
start_time=datetime(year=2050, month=1, day=22, hour=0, minute=0, tzinfo=ZoneInfo("UTC")),
|
|
1855
|
+
time_zone=ZoneInfo("UTC"),
|
|
1856
|
+
)
|
|
1857
|
+
self.one_off_est_job = ScheduledJob.create_schedule(
|
|
1858
|
+
job_model=self.job_model,
|
|
1859
|
+
user=self.user,
|
|
1860
|
+
name="One-off EST Job",
|
|
1861
|
+
interval=JobExecutionType.TYPE_FUTURE,
|
|
1862
|
+
start_time=datetime(year=2050, month=1, day=22, hour=0, minute=0, tzinfo=ZoneInfo("America/New_York")),
|
|
1863
|
+
)
|
|
1864
|
+
|
|
1865
|
+
def test_schedule(self):
|
|
1866
|
+
"""Test the schedule property."""
|
|
1867
|
+
with self.subTest("Test TYPE_DAILY schedules"):
|
|
1868
|
+
daily_utc_schedule = self.daily_utc_job.schedule
|
|
1869
|
+
daily_est_schedule = self.daily_est_job.schedule
|
|
1870
|
+
self.assertIsInstance(daily_utc_schedule, TzAwareCrontab)
|
|
1871
|
+
self.assertIsInstance(daily_est_schedule, TzAwareCrontab)
|
|
1872
|
+
self.assertNotEqual(daily_utc_schedule, daily_est_schedule)
|
|
1873
|
+
# Crontabs are validated in test_to_cron()
|
|
1874
|
+
|
|
1875
|
+
with self.subTest("Test TYPE_CUSTOM schedules"):
|
|
1876
|
+
crontab_utc_schedule = self.crontab_utc_job.schedule
|
|
1877
|
+
crontab_est_schedule = self.crontab_est_job.schedule
|
|
1878
|
+
self.assertIsInstance(crontab_utc_schedule, TzAwareCrontab)
|
|
1879
|
+
self.assertIsInstance(crontab_est_schedule, TzAwareCrontab)
|
|
1880
|
+
self.assertNotEqual(crontab_utc_schedule, crontab_est_schedule)
|
|
1881
|
+
# Crontabs are validated in test_to_cron()
|
|
1882
|
+
|
|
1883
|
+
with self.subTest("Test TYPE_FUTURE schedules"):
|
|
1884
|
+
# TYPE_FUTURE schedules are one off, not cron tabs:
|
|
1885
|
+
self.assertEqual(self.one_off_utc_job.schedule.clocked_time, self.one_off_utc_job.start_time)
|
|
1886
|
+
self.assertEqual(self.one_off_est_job.schedule.clocked_time, self.one_off_est_job.start_time)
|
|
1887
|
+
self.assertEqual(
|
|
1888
|
+
self.one_off_est_job.schedule.clocked_time - self.one_off_utc_job.schedule.clocked_time,
|
|
1889
|
+
timedelta(hours=5),
|
|
1890
|
+
)
|
|
1891
|
+
|
|
1892
|
+
def test_to_cron(self):
|
|
1893
|
+
"""Test the to_cron() method and its interaction with time zone variants."""
|
|
1894
|
+
|
|
1895
|
+
with self.subTest("Test TYPE_DAILY schedule with UTC time zone and UTC schedule time zone"):
|
|
1896
|
+
self.daily_utc_job.refresh_from_db()
|
|
1897
|
+
daily_utc_schedule = self.daily_utc_job.to_cron()
|
|
1898
|
+
self.assertEqual(daily_utc_schedule.tz, ZoneInfo("UTC"))
|
|
1899
|
+
self.assertEqual(daily_utc_schedule.hour, {17})
|
|
1900
|
+
self.assertEqual(daily_utc_schedule.minute, {0})
|
|
1901
|
+
last_run = datetime(2050, 1, 21, 17, 0, tzinfo=ZoneInfo("UTC"))
|
|
1902
|
+
with time_machine.travel("2050-01-22 16:59 +0000"):
|
|
1903
|
+
is_due, _ = daily_utc_schedule.is_due(last_run_at=last_run)
|
|
1904
|
+
self.assertFalse(is_due)
|
|
1905
|
+
with time_machine.travel("2050-01-22 17:00 +0000"):
|
|
1906
|
+
is_due, _ = daily_utc_schedule.is_due(last_run_at=last_run)
|
|
1907
|
+
self.assertTrue(is_due)
|
|
1908
|
+
|
|
1909
|
+
with self.subTest("Test TYPE_DAILY schedule with UTC time zone and EST schedule time zone"):
|
|
1910
|
+
self.daily_est_job.refresh_from_db()
|
|
1911
|
+
daily_est_schedule = self.daily_est_job.to_cron()
|
|
1912
|
+
self.assertEqual(daily_est_schedule.tz, ZoneInfo("America/New_York"))
|
|
1913
|
+
self.assertEqual(daily_est_schedule.hour, {17})
|
|
1914
|
+
self.assertEqual(daily_est_schedule.minute, {0})
|
|
1915
|
+
last_run = datetime(2050, 1, 21, 22, 0, tzinfo=ZoneInfo("UTC"))
|
|
1916
|
+
with time_machine.travel("2050-01-22 21:59 +0000"):
|
|
1917
|
+
is_due, _ = daily_est_schedule.is_due(last_run_at=last_run)
|
|
1918
|
+
self.assertFalse(is_due)
|
|
1919
|
+
with time_machine.travel("2050-01-22 22:00 +0000"):
|
|
1920
|
+
is_due, _ = daily_est_schedule.is_due(last_run_at=last_run)
|
|
1921
|
+
self.assertTrue(is_due)
|
|
1922
|
+
|
|
1923
|
+
with self.subTest("Test TYPE_CUSTOM schedule with UTC time zone and UTC schedule time zone"):
|
|
1924
|
+
self.crontab_utc_job.refresh_from_db()
|
|
1925
|
+
crontab_utc_schedule = self.crontab_utc_job.to_cron()
|
|
1926
|
+
self.assertEqual(crontab_utc_schedule.tz, ZoneInfo("UTC"))
|
|
1927
|
+
self.assertEqual(crontab_utc_schedule.hour, {17})
|
|
1928
|
+
self.assertEqual(crontab_utc_schedule.minute, {0})
|
|
1929
|
+
|
|
1930
|
+
with self.subTest("Test TYPE_CUSTOM schedule with UTC time zone and EST schedule time zone"):
|
|
1931
|
+
self.crontab_est_job.refresh_from_db()
|
|
1932
|
+
crontab_est_schedule = self.crontab_est_job.to_cron()
|
|
1933
|
+
self.assertEqual(crontab_est_schedule.tz, ZoneInfo("America/New_York"))
|
|
1934
|
+
self.assertEqual(crontab_est_schedule.hour, {17})
|
|
1935
|
+
self.assertEqual(crontab_est_schedule.minute, {0})
|
|
1936
|
+
|
|
1937
|
+
with self.subTest("Test TYPE_FUTURE schedules do not map to cron"):
|
|
1938
|
+
with self.assertRaises(ValueError):
|
|
1939
|
+
self.one_off_utc_job.to_cron()
|
|
1940
|
+
with self.assertRaises(ValueError):
|
|
1941
|
+
self.one_off_est_job.to_cron()
|
|
1942
|
+
|
|
1943
|
+
with override_settings(TIME_ZONE="America/New_York"):
|
|
1944
|
+
with self.subTest("Test TYPE_DAILY schedule with EST time zone and UTC schedule time zone"):
|
|
1945
|
+
self.daily_utc_job.refresh_from_db()
|
|
1946
|
+
daily_utc_schedule = self.daily_utc_job.to_cron()
|
|
1947
|
+
self.assertEqual(daily_utc_schedule.tz, ZoneInfo("UTC"))
|
|
1948
|
+
self.assertEqual(daily_utc_schedule.hour, {17})
|
|
1949
|
+
self.assertEqual(daily_utc_schedule.minute, {0})
|
|
1950
|
+
last_run = datetime(2050, 1, 21, 12, 0, tzinfo=ZoneInfo("America/New_York"))
|
|
1951
|
+
with time_machine.travel("2050-01-22 11:59 -0500"):
|
|
1952
|
+
is_due, _ = daily_utc_schedule.is_due(last_run_at=last_run)
|
|
1953
|
+
self.assertFalse(is_due)
|
|
1954
|
+
with time_machine.travel("2050-01-22 12:00 -0500"):
|
|
1955
|
+
is_due, _ = daily_utc_schedule.is_due(last_run_at=last_run)
|
|
1956
|
+
self.assertTrue(is_due)
|
|
1957
|
+
|
|
1958
|
+
with self.subTest("Test TYPE_DAILY schedule with EST time zone and EST schedule time zone"):
|
|
1959
|
+
self.daily_est_job.refresh_from_db()
|
|
1960
|
+
daily_est_schedule = self.daily_est_job.to_cron()
|
|
1961
|
+
self.assertEqual(daily_est_schedule.tz, ZoneInfo("America/New_York"))
|
|
1962
|
+
self.assertEqual(daily_est_schedule.hour, {17})
|
|
1963
|
+
self.assertEqual(daily_est_schedule.minute, {0})
|
|
1964
|
+
last_run = datetime(2050, 1, 21, 22, 0, tzinfo=ZoneInfo("America/New_York"))
|
|
1965
|
+
with time_machine.travel("2050-01-22 16:59 -0500"):
|
|
1966
|
+
is_due, _ = daily_est_schedule.is_due(last_run_at=last_run)
|
|
1967
|
+
self.assertFalse(is_due)
|
|
1968
|
+
with time_machine.travel("2050-01-22 17:00 -0500"):
|
|
1969
|
+
is_due, _ = daily_est_schedule.is_due(last_run_at=last_run)
|
|
1970
|
+
self.assertTrue(is_due)
|
|
1971
|
+
|
|
1972
|
+
with self.subTest("Test TYPE_CUSTOM schedule with EST time zone and UTC schedule time zone"):
|
|
1973
|
+
self.crontab_utc_job.refresh_from_db()
|
|
1974
|
+
crontab_utc_schedule = self.crontab_utc_job.to_cron()
|
|
1975
|
+
self.assertEqual(crontab_utc_schedule.tz, ZoneInfo("UTC"))
|
|
1976
|
+
self.assertEqual(crontab_utc_schedule.hour, {17})
|
|
1977
|
+
self.assertEqual(crontab_utc_schedule.minute, {0})
|
|
1978
|
+
|
|
1979
|
+
with self.subTest("Test TYPE_CUSTOM schedule with EST time zone and EST schedule time zone"):
|
|
1980
|
+
self.crontab_est_job.refresh_from_db()
|
|
1981
|
+
crontab_est_schedule = self.crontab_est_job.to_cron()
|
|
1982
|
+
self.assertEqual(crontab_est_schedule.tz, ZoneInfo("America/New_York"))
|
|
1983
|
+
self.assertEqual(crontab_est_schedule.hour, {17})
|
|
1984
|
+
self.assertEqual(crontab_est_schedule.minute, {0})
|
|
1985
|
+
|
|
1986
|
+
def test_crontab_dst(self):
|
|
1987
|
+
"""Test that TYPE_CUSTOM behavior around DST is as expected."""
|
|
1988
|
+
cronjob = ScheduledJob.objects.create(
|
|
1989
|
+
name="DST Aware Cronjob",
|
|
1990
|
+
task="pass.TestPass",
|
|
1991
|
+
job_model=self.job_model,
|
|
1992
|
+
enabled=False,
|
|
1993
|
+
interval=JobExecutionType.TYPE_CUSTOM,
|
|
1994
|
+
start_time=datetime(year=2024, month=1, day=1, hour=17, minute=0, tzinfo=ZoneInfo("America/New_York")),
|
|
1995
|
+
crontab="0 17 * * *", # 5 PM local time
|
|
1996
|
+
time_zone=ZoneInfo("America/New_York"),
|
|
1997
|
+
)
|
|
1998
|
+
|
|
1999
|
+
# Before DST takes effect
|
|
2000
|
+
with self.subTest("Test UTC time zone with EST job"):
|
|
2001
|
+
cronjob.refresh_from_db()
|
|
2002
|
+
crontab = cronjob.to_cron()
|
|
2003
|
+
with time_machine.travel("2024-03-09 21:59 +0000"):
|
|
2004
|
+
is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 8, 17, 0, tzinfo=ZoneInfo("America/New_York")))
|
|
2005
|
+
self.assertFalse(is_due)
|
|
2006
|
+
with time_machine.travel("2024-03-09 22:00 +0000"):
|
|
2007
|
+
is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 8, 17, 0, tzinfo=ZoneInfo("America/New_York")))
|
|
2008
|
+
self.assertTrue(is_due)
|
|
2009
|
+
|
|
2010
|
+
with self.subTest("Test EST time zone with EST job"), override_settings(TIME_ZONE="America/New_York"):
|
|
2011
|
+
cronjob.refresh_from_db()
|
|
2012
|
+
crontab = cronjob.to_cron()
|
|
2013
|
+
with time_machine.travel("2024-03-09 16:59 -0500"):
|
|
2014
|
+
is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 8, 17, 0, tzinfo=ZoneInfo("America/New_York")))
|
|
2015
|
+
self.assertFalse(is_due)
|
|
2016
|
+
with time_machine.travel("2024-03-09 17:00 -0500"):
|
|
2017
|
+
is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 8, 17, 0, tzinfo=ZoneInfo("America/New_York")))
|
|
2018
|
+
self.assertTrue(is_due)
|
|
2019
|
+
|
|
2020
|
+
# Day that DST takes effect
|
|
2021
|
+
with self.subTest("Test UTC time zone with EDT job"):
|
|
2022
|
+
cronjob.refresh_from_db()
|
|
2023
|
+
crontab = cronjob.to_cron()
|
|
2024
|
+
with time_machine.travel("2024-03-10 20:59 +0000"):
|
|
2025
|
+
is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 9, 17, 0, tzinfo=ZoneInfo("America/New_York")))
|
|
2026
|
+
self.assertFalse(is_due)
|
|
2027
|
+
with time_machine.travel("2024-03-10 21:00 +0000"):
|
|
2028
|
+
is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 9, 17, 0, tzinfo=ZoneInfo("America/New_York")))
|
|
2029
|
+
self.assertTrue(is_due)
|
|
2030
|
+
|
|
2031
|
+
with self.subTest("Test EDT time zone with EDT job"), override_settings(TIME_ZONE="America/New_York"):
|
|
2032
|
+
cronjob.refresh_from_db()
|
|
2033
|
+
crontab = cronjob.to_cron()
|
|
2034
|
+
with time_machine.travel("2024-03-10 16:59 -0400"):
|
|
2035
|
+
is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 9, 17, 0, tzinfo=ZoneInfo("America/New_York")))
|
|
2036
|
+
self.assertFalse(is_due)
|
|
2037
|
+
with time_machine.travel("2024-03-10 17:00 -0400"):
|
|
2038
|
+
is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 9, 17, 0, tzinfo=ZoneInfo("America/New_York")))
|
|
2039
|
+
self.assertTrue(is_due)
|
|
2040
|
+
|
|
2041
|
+
def test_daily_dst(self):
|
|
2042
|
+
"""Test the interaction of TYPE_DAILY around DST."""
|
|
2043
|
+
daily = ScheduledJob.objects.create(
|
|
2044
|
+
name="Daily Job",
|
|
2045
|
+
task="pass.TestPass",
|
|
2046
|
+
job_model=self.job_model,
|
|
2047
|
+
enabled=False,
|
|
2048
|
+
interval=JobExecutionType.TYPE_DAILY,
|
|
2049
|
+
start_time=datetime(year=2024, month=1, day=1, hour=17, minute=0, tzinfo=ZoneInfo("America/New_York")),
|
|
2050
|
+
time_zone=ZoneInfo("America/New_York"),
|
|
2051
|
+
)
|
|
2052
|
+
|
|
2053
|
+
# Before DST takes effect
|
|
2054
|
+
with self.subTest("Test UTC time zone with EST job"):
|
|
2055
|
+
daily.refresh_from_db()
|
|
2056
|
+
crontab = daily.to_cron()
|
|
2057
|
+
with time_machine.travel("2024-03-09 21:59 +0000"):
|
|
2058
|
+
is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 8, 17, 0, tzinfo=ZoneInfo("America/New_York")))
|
|
2059
|
+
self.assertFalse(is_due)
|
|
2060
|
+
with time_machine.travel("2024-03-09 22:00 +0000"):
|
|
2061
|
+
is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 8, 17, 0, tzinfo=ZoneInfo("America/New_York")))
|
|
2062
|
+
self.assertTrue(is_due)
|
|
2063
|
+
|
|
2064
|
+
with self.subTest("Test EST time zone with EST job"), override_settings(TIME_ZONE="America/New_York"):
|
|
2065
|
+
daily.refresh_from_db()
|
|
2066
|
+
crontab = daily.to_cron()
|
|
2067
|
+
with time_machine.travel("2024-03-09 16:59 -0500"):
|
|
2068
|
+
is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 8, 17, 0, tzinfo=ZoneInfo("America/New_York")))
|
|
2069
|
+
self.assertFalse(is_due)
|
|
2070
|
+
with time_machine.travel("2024-03-09 17:00 -0500"):
|
|
2071
|
+
is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 8, 17, 0, tzinfo=ZoneInfo("America/New_York")))
|
|
2072
|
+
self.assertTrue(is_due)
|
|
2073
|
+
|
|
2074
|
+
# Day that DST takes effect
|
|
2075
|
+
with self.subTest("Test UTC time zone with EDT job"):
|
|
2076
|
+
daily.refresh_from_db()
|
|
2077
|
+
crontab = daily.to_cron()
|
|
2078
|
+
with time_machine.travel("2024-03-10 20:59 +0000"):
|
|
2079
|
+
is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 9, 17, 0, tzinfo=ZoneInfo("America/New_York")))
|
|
2080
|
+
self.assertFalse(is_due)
|
|
2081
|
+
with time_machine.travel("2024-03-10 21:00 +0000"):
|
|
2082
|
+
is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 9, 17, 0, tzinfo=ZoneInfo("America/New_York")))
|
|
2083
|
+
self.assertTrue(is_due)
|
|
2084
|
+
|
|
2085
|
+
with self.subTest("Test EDT time zone with EDT job"), override_settings(TIME_ZONE="America/New_York"):
|
|
2086
|
+
daily.refresh_from_db()
|
|
2087
|
+
crontab = daily.to_cron()
|
|
2088
|
+
with time_machine.travel("2024-03-10 16:59 -0400"):
|
|
2089
|
+
is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 9, 17, 0, tzinfo=ZoneInfo("America/New_York")))
|
|
2090
|
+
self.assertFalse(is_due)
|
|
2091
|
+
with time_machine.travel("2024-03-10 17:00 -0400"):
|
|
2092
|
+
is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 9, 17, 0, tzinfo=ZoneInfo("America/New_York")))
|
|
2093
|
+
self.assertTrue(is_due)
|
|
2094
|
+
|
|
2095
|
+
|
|
1793
2096
|
class SecretTest(ModelTestCases.BaseModelTestCase):
|
|
1794
2097
|
"""
|
|
1795
2098
|
Tests for the `Secret` model class.
|
|
@@ -1744,16 +1744,17 @@ class ScheduledJobTestCase(
|
|
|
1744
1744
|
ScheduledJob.objects.create(
|
|
1745
1745
|
name="test2",
|
|
1746
1746
|
task="pass.TestPass",
|
|
1747
|
-
interval=JobExecutionType.
|
|
1747
|
+
interval=JobExecutionType.TYPE_DAILY,
|
|
1748
1748
|
user=user,
|
|
1749
1749
|
start_time=timezone.now(),
|
|
1750
1750
|
)
|
|
1751
1751
|
ScheduledJob.objects.create(
|
|
1752
1752
|
name="test3",
|
|
1753
1753
|
task="pass.TestPass",
|
|
1754
|
-
interval=JobExecutionType.
|
|
1754
|
+
interval=JobExecutionType.TYPE_CUSTOM,
|
|
1755
1755
|
user=user,
|
|
1756
1756
|
start_time=timezone.now(),
|
|
1757
|
+
crontab="15 10 * * *",
|
|
1757
1758
|
)
|
|
1758
1759
|
|
|
1759
1760
|
def test_only_enabled_is_listed(self):
|
|
@@ -3007,6 +3008,7 @@ class JobCustomTemplateTestCase(TestCase):
|
|
|
3007
3008
|
cls.run_url = reverse("extras:job_run", kwargs={"pk": cls.example_job.pk})
|
|
3008
3009
|
|
|
3009
3010
|
def test_rendering_custom_template(self):
|
|
3011
|
+
self.assertIsNotNone(self.example_job.job_class)
|
|
3010
3012
|
obj_perm = ObjectPermission(name="Test permission", actions=["view", "run"])
|
|
3011
3013
|
obj_perm.save()
|
|
3012
3014
|
obj_perm.users.add(self.user)
|
nautobot/extras/views.py
CHANGED
|
@@ -2,6 +2,7 @@ import logging
|
|
|
2
2
|
from urllib.parse import parse_qs
|
|
3
3
|
|
|
4
4
|
from celery import chain
|
|
5
|
+
from django.conf import settings
|
|
5
6
|
from django.contrib import messages
|
|
6
7
|
from django.contrib.auth.models import AnonymousUser
|
|
7
8
|
from django.contrib.contenttypes.models import ContentType
|
|
@@ -23,6 +24,11 @@ from django_tables2 import RequestConfig
|
|
|
23
24
|
from jsonschema.validators import Draft7Validator
|
|
24
25
|
from rest_framework.decorators import action
|
|
25
26
|
|
|
27
|
+
try:
|
|
28
|
+
from zoneinfo import ZoneInfo
|
|
29
|
+
except ImportError: # python 3.8
|
|
30
|
+
from backports.zoneinfo import ZoneInfo
|
|
31
|
+
|
|
26
32
|
from nautobot.core.forms import restrict_form_fields
|
|
27
33
|
from nautobot.core.models.querysets import count_related
|
|
28
34
|
from nautobot.core.models.utils import pretty_print_query
|
|
@@ -1916,6 +1922,7 @@ class ScheduledJobView(generic.ObjectView):
|
|
|
1916
1922
|
return {
|
|
1917
1923
|
"labels": labels,
|
|
1918
1924
|
"job_class_found": (job_class is not None),
|
|
1925
|
+
"default_time_zone": ZoneInfo(settings.TIME_ZONE),
|
|
1919
1926
|
**super().get_extra_context(request, instance),
|
|
1920
1927
|
}
|
|
1921
1928
|
|
nautobot/ipam/api/views.py
CHANGED
|
@@ -202,7 +202,7 @@ class PrefixViewSet(NautobotModelViewSet):
|
|
|
202
202
|
if requested_prefix["prefix_length"] >= available_prefix.prefixlen:
|
|
203
203
|
allocated_prefix = f"{available_prefix.network}/{requested_prefix['prefix_length']}"
|
|
204
204
|
requested_prefix["prefix"] = allocated_prefix
|
|
205
|
-
requested_prefix["namespace"] = prefix.namespace
|
|
205
|
+
requested_prefix["namespace"] = prefix.namespace
|
|
206
206
|
break
|
|
207
207
|
else:
|
|
208
208
|
return Response(
|
|
@@ -210,6 +210,10 @@ class PrefixViewSet(NautobotModelViewSet):
|
|
|
210
210
|
status=status.HTTP_204_NO_CONTENT,
|
|
211
211
|
)
|
|
212
212
|
|
|
213
|
+
# The serializer usage above has mapped "custom_fields" dict to "_custom_field_data".
|
|
214
|
+
# We need to convert it back to "custom_fields" as we're going to deserialize it a second time below
|
|
215
|
+
requested_prefix["custom_fields"] = requested_prefix.pop("_custom_field_data", {})
|
|
216
|
+
|
|
213
217
|
# Remove the allocated prefix from the list of available prefixes
|
|
214
218
|
available_prefixes.remove(allocated_prefix)
|
|
215
219
|
|
|
@@ -299,7 +303,10 @@ class PrefixViewSet(NautobotModelViewSet):
|
|
|
299
303
|
prefix_length = prefix.prefix.prefixlen
|
|
300
304
|
for requested_ip in requested_ips:
|
|
301
305
|
requested_ip["address"] = f"{next(available_ips)}/{prefix_length}"
|
|
302
|
-
requested_ip["namespace"] = prefix.namespace
|
|
306
|
+
requested_ip["namespace"] = prefix.namespace
|
|
307
|
+
# The serializer usage above has mapped "custom_fields" dict to "_custom_field_data".
|
|
308
|
+
# We need to convert it back to "custom_fields" as we're going to deserialize it a second time below
|
|
309
|
+
requested_ip["custom_fields"] = requested_ip.pop("_custom_field_data", {})
|
|
303
310
|
|
|
304
311
|
# Initialize the serializer with a list or a single object depending on what was requested
|
|
305
312
|
context = {"request": request, "depth": 0}
|
nautobot/ipam/choices.py
CHANGED
|
@@ -102,6 +102,23 @@ class IPAddressTypeChoices(ChoiceSet):
|
|
|
102
102
|
)
|
|
103
103
|
|
|
104
104
|
|
|
105
|
+
#
|
|
106
|
+
# VRFs
|
|
107
|
+
#
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class VRFStatusChoices(ChoiceSet):
|
|
111
|
+
STATUS_ACTIVE = "active"
|
|
112
|
+
STATUS_DOWN = "down"
|
|
113
|
+
STATUS_DEPRECATED = "deprecated"
|
|
114
|
+
|
|
115
|
+
CHOICES = (
|
|
116
|
+
(STATUS_ACTIVE, "Active"),
|
|
117
|
+
(STATUS_DOWN, "Down"),
|
|
118
|
+
(STATUS_DEPRECATED, "Deprecated"),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
105
122
|
#
|
|
106
123
|
# VLANs
|
|
107
124
|
#
|
nautobot/ipam/factory.py
CHANGED
|
@@ -84,6 +84,7 @@ class VRFFactory(PrimaryModelFactory):
|
|
|
84
84
|
model = VRF
|
|
85
85
|
exclude = (
|
|
86
86
|
"has_description",
|
|
87
|
+
"has_status",
|
|
87
88
|
"has_tenant",
|
|
88
89
|
)
|
|
89
90
|
|
|
@@ -103,6 +104,11 @@ class VRFFactory(PrimaryModelFactory):
|
|
|
103
104
|
has_description = NautobotBoolIterator()
|
|
104
105
|
description = factory.Maybe("has_description", factory.Faker("text", max_nb_chars=CHARFIELD_MAX_LENGTH), "")
|
|
105
106
|
|
|
107
|
+
has_status = NautobotBoolIterator()
|
|
108
|
+
status = factory.Maybe(
|
|
109
|
+
"has_status", random_instance(lambda: Status.objects.get_for_model(VRF), allow_null=False), None
|
|
110
|
+
)
|
|
111
|
+
|
|
106
112
|
namespace = random_instance(Namespace, allow_null=False)
|
|
107
113
|
|
|
108
114
|
@factory.post_generation
|
nautobot/ipam/filters.py
CHANGED
|
@@ -66,7 +66,7 @@ class NamespaceFilterSet(NautobotFilterSet):
|
|
|
66
66
|
fields = "__all__"
|
|
67
67
|
|
|
68
68
|
|
|
69
|
-
class VRFFilterSet(NautobotFilterSet, TenancyModelFilterSetMixin):
|
|
69
|
+
class VRFFilterSet(NautobotFilterSet, StatusModelFilterSetMixin, TenancyModelFilterSetMixin):
|
|
70
70
|
q = SearchFilter(
|
|
71
71
|
filter_predicates={
|
|
72
72
|
"name": "icontains",
|
nautobot/ipam/forms.py
CHANGED
|
@@ -125,6 +125,7 @@ class VRFForm(NautobotModelForm, TenancyForm):
|
|
|
125
125
|
"name",
|
|
126
126
|
"rd",
|
|
127
127
|
"namespace",
|
|
128
|
+
"status",
|
|
128
129
|
"description",
|
|
129
130
|
"import_targets",
|
|
130
131
|
"export_targets",
|
|
@@ -140,10 +141,11 @@ class VRFForm(NautobotModelForm, TenancyForm):
|
|
|
140
141
|
}
|
|
141
142
|
help_texts = {
|
|
142
143
|
"rd": "Route distinguisher unique to this Namespace (as defined in RFC 4364)",
|
|
144
|
+
"status": "Operational status of this VRF",
|
|
143
145
|
}
|
|
144
146
|
|
|
145
147
|
|
|
146
|
-
class VRFBulkEditForm(TagsBulkEditFormMixin, NautobotBulkEditForm):
|
|
148
|
+
class VRFBulkEditForm(TagsBulkEditFormMixin, StatusModelBulkEditFormMixin, NautobotBulkEditForm):
|
|
147
149
|
pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput())
|
|
148
150
|
namespace = DynamicModelChoiceField(queryset=Namespace.objects.all(), required=False)
|
|
149
151
|
tenant = DynamicModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
|
@@ -162,9 +164,9 @@ class VRFBulkEditForm(TagsBulkEditFormMixin, NautobotBulkEditForm):
|
|
|
162
164
|
]
|
|
163
165
|
|
|
164
166
|
|
|
165
|
-
class VRFFilterForm(NautobotFilterForm, TenancyFilterForm):
|
|
167
|
+
class VRFFilterForm(NautobotFilterForm, StatusModelFilterFormMixin, TenancyFilterForm):
|
|
166
168
|
model = VRF
|
|
167
|
-
field_order = ["q", "import_targets", "export_targets", "tenant_group", "tenant"]
|
|
169
|
+
field_order = ["q", "import_targets", "export_targets", "status", "tenant_group", "tenant"]
|
|
168
170
|
q = forms.CharField(required=False, label="Search")
|
|
169
171
|
import_targets = DynamicModelMultipleChoiceField(
|
|
170
172
|
queryset=RouteTarget.objects.all(), to_field_name="name", required=False
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Generated by Django 4.2.15 on 2024-08-26 17:20
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
import django.db.models.deletion
|
|
5
|
+
|
|
6
|
+
import nautobot.extras.models.statuses
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Migration(migrations.Migration):
|
|
10
|
+
dependencies = [
|
|
11
|
+
("extras", "0114_computedfield_grouping"),
|
|
12
|
+
("ipam", "0047_alter_ipaddress_role_alter_ipaddress_status_and_more"),
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
operations = [
|
|
16
|
+
migrations.AddField(
|
|
17
|
+
model_name="vrf",
|
|
18
|
+
name="status",
|
|
19
|
+
field=nautobot.extras.models.statuses.StatusField(
|
|
20
|
+
blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to="extras.status"
|
|
21
|
+
),
|
|
22
|
+
),
|
|
23
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Generated by Django 4.2.15 on 2024-08-26 18:05
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
|
|
5
|
+
from nautobot.extras.management import clear_status_choices, populate_status_choices
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def populate_vrf_status_choices(apps, schema_editor):
|
|
9
|
+
"""Create default Status records for the VRF model."""
|
|
10
|
+
populate_status_choices(apps, schema_editor, models=["ipam.VRF"])
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def clear_vrf_status_choices(apps, schema_editor):
|
|
14
|
+
"""Remove default Status records for the VRF model."""
|
|
15
|
+
clear_status_choices(apps, schema_editor, models=["ipam.VRF"])
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Migration(migrations.Migration):
|
|
19
|
+
dependencies = [
|
|
20
|
+
("ipam", "0048_vrf_status"),
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
operations = [
|
|
24
|
+
migrations.RunPython(populate_vrf_status_choices, clear_vrf_status_choices),
|
|
25
|
+
]
|
nautobot/ipam/models.py
CHANGED
|
@@ -97,6 +97,7 @@ def get_default_namespace_pk():
|
|
|
97
97
|
"custom_validators",
|
|
98
98
|
"export_templates",
|
|
99
99
|
"graphql",
|
|
100
|
+
"statuses",
|
|
100
101
|
"webhooks",
|
|
101
102
|
)
|
|
102
103
|
class VRF(PrimaryModel):
|
|
@@ -114,6 +115,7 @@ class VRF(PrimaryModel):
|
|
|
114
115
|
verbose_name="Route distinguisher",
|
|
115
116
|
help_text="Unique route distinguisher (as defined in RFC 4364)",
|
|
116
117
|
)
|
|
118
|
+
status = StatusField(blank=True, null=True)
|
|
117
119
|
namespace = models.ForeignKey(
|
|
118
120
|
"ipam.Namespace",
|
|
119
121
|
on_delete=models.PROTECT,
|
|
@@ -533,6 +535,10 @@ class Prefix(PrimaryModel):
|
|
|
533
535
|
def __str__(self):
|
|
534
536
|
return str(self.prefix)
|
|
535
537
|
|
|
538
|
+
@property
|
|
539
|
+
def display(self):
|
|
540
|
+
return f"{self.prefix}: {self.namespace}"
|
|
541
|
+
|
|
536
542
|
def _deconstruct_prefix(self, prefix):
|
|
537
543
|
if prefix:
|
|
538
544
|
if isinstance(prefix, str):
|
nautobot/ipam/tables.py
CHANGED
|
@@ -217,7 +217,7 @@ class NamespaceTable(BaseTable):
|
|
|
217
217
|
#
|
|
218
218
|
|
|
219
219
|
|
|
220
|
-
class VRFTable(BaseTable):
|
|
220
|
+
class VRFTable(StatusTableMixin, BaseTable):
|
|
221
221
|
pk = ToggleColumn()
|
|
222
222
|
name = tables.LinkColumn()
|
|
223
223
|
# rd = tables.Column(verbose_name="RD")
|
|
@@ -232,6 +232,7 @@ class VRFTable(BaseTable):
|
|
|
232
232
|
"pk",
|
|
233
233
|
"name",
|
|
234
234
|
# "rd",
|
|
235
|
+
"status",
|
|
235
236
|
"namespace",
|
|
236
237
|
"tenant",
|
|
237
238
|
"description",
|
|
@@ -240,7 +241,7 @@ class VRFTable(BaseTable):
|
|
|
240
241
|
"tags",
|
|
241
242
|
)
|
|
242
243
|
# default_columns = ("pk", "name", "rd", "namespace", "tenant", "description")
|
|
243
|
-
default_columns = ("pk", "name", "namespace", "tenant", "description")
|
|
244
|
+
default_columns = ("pk", "name", "status", "namespace", "tenant", "description")
|
|
244
245
|
|
|
245
246
|
|
|
246
247
|
class VRFDeviceAssignmentTable(BaseTable):
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
<td>Tenant</td>
|
|
20
20
|
<td>{{ object.tenant|hyperlinked_object }}</td>
|
|
21
21
|
</tr>
|
|
22
|
+
<tr>
|
|
23
|
+
<td>Status</td>
|
|
24
|
+
<td>{{ object.status|hyperlinked_object_with_color }}</td>
|
|
25
|
+
</tr>
|
|
22
26
|
<tr>
|
|
23
27
|
<td>Description</td>
|
|
24
28
|
<td>{{ object.description|placeholder }}</td>
|