From 18d5abb2f641db7f364f7566187e57bebdae9fe9 Mon Sep 17 00:00:00 2001 From: Tarun Tak Date: Wed, 29 Oct 2025 01:53:22 +0530 Subject: [PATCH] chore: Replace pytz with zoneinfo for UTC handling - Part 1 (#37523) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First PR to replace pytz with zoneinfo for UTC handling across codebase. This PR migrates all UTC timezone handling from pytz to Python’s standard library zoneinfo. The pytz library is now deprecated, and its documentation recommends using zoneinfo for all new code. This update modernizes our codebase, removes legacy pytz usage, and ensures compatibility with current best practices for timezone management in Python 3.9+. No functional changes to timezone logic - just a direct replacement for UTC handling. https://github.com/openedx/edx-platform/issues/33980 --- .../djangoapps/bookmarks/tests/test_models.py | 4 +- openedx/core/djangoapps/catalog/utils.py | 4 +- .../core/djangoapps/ccxcon/tests/test_api.py | 6 +-- .../content/course_overviews/models.py | 8 +-- .../tests/test_course_overviews.py | 5 +- .../course_overviews/tests/test_signals.py | 4 +- .../management/commands/notify_credentials.py | 5 +- .../core/djangoapps/credit/api/provider.py | 4 +- openedx/core/djangoapps/credit/models.py | 8 +-- openedx/core/djangoapps/credit/serializers.py | 4 +- .../core/djangoapps/credit/tests/factories.py | 4 +- .../core/djangoapps/credit/tests/test_api.py | 6 +-- .../djangoapps/credit/tests/test_signals.py | 6 +-- .../djangoapps/credit/tests/test_views.py | 8 +-- openedx/core/djangoapps/credit/views.py | 4 +- .../djangoapps/enrollments/tests/test_data.py | 4 +- .../enrollments/tests/test_views.py | 20 +++---- .../models/tests/test_course_details.py | 10 ++-- .../core/djangoapps/notifications/tasks.py | 4 +- .../tests/test_notification_grouping.py | 8 +-- .../notifications/tests/test_views.py | 4 +- .../core/djangoapps/notifications/views.py | 6 +-- .../dot_overrides/validators.py | 6 +-- .../core/djangoapps/oauth_dispatch/models.py | 4 +- .../oauth_dispatch/tests/factories.py | 4 +- .../djangoapps/password_policy/compliance.py | 4 +- .../password_policy/tests/test_compliance.py | 6 +-- .../profile_images/tests/test_views.py | 6 +-- .../core/djangoapps/profile_images/views.py | 4 +- .../djangoapps/programs/tests/test_tasks.py | 10 ++-- .../djangoapps/programs/tests/test_utils.py | 24 ++++----- openedx/core/djangoapps/programs/utils.py | 16 +++--- .../schedules/management/commands/__init__.py | 4 +- .../send_course_next_section_update.py | 4 +- .../setup_models_to_send_test_emails.py | 12 ++--- .../commands/tests/send_email_base.py | 12 ++--- .../tests/test_send_email_base_command.py | 6 +-- .../djangoapps/schedules/tests/factories.py | 6 +-- .../schedules/tests/test_resolvers.py | 6 +-- .../schedules/tests/test_signals.py | 4 +- .../djangoapps/schedules/tests/test_utils.py | 4 +- openedx/core/djangoapps/schedules/utils.py | 4 +- .../core/djangoapps/user_api/accounts/api.py | 4 +- .../accounts/tests/retirement_helpers.py | 4 +- .../user_api/accounts/tests/test_api.py | 4 +- .../accounts/tests/test_image_helpers.py | 4 +- .../accounts/tests/test_retirement_views.py | 10 ++-- .../user_api/accounts/tests/test_views.py | 12 ++--- .../djangoapps/user_api/accounts/views.py | 17 +++--- .../commands/create_user_gdpr_testing.py | 4 +- .../core/djangoapps/user_authn/tests/utils.py | 4 +- .../djangoapps/user_authn/views/register.py | 8 +-- .../user_authn/views/tests/test_password.py | 4 +- .../user_authn/views/tests/test_register.py | 4 +- .../views/tests/test_reset_password.py | 4 +- openedx/core/djangoapps/util/testing.py | 4 +- .../tests/test_partition_scheme.py | 8 +-- openedx/core/lib/xblock_utils/__init__.py | 4 +- openedx/features/calendar_sync/ics.py | 4 +- .../features/calendar_sync/tests/test_ics.py | 6 +-- .../content_type_gating/partitions.py | 4 +- .../content_type_gating/tests/test_models.py | 8 +-- .../tests/test_access.py | 11 ++-- .../tests/test_models.py | 53 ++++++++++++------- .../tests/views/test_course_updates.py | 4 +- openedx/features/discounts/applicability.py | 4 +- .../discounts/tests/test_applicability.py | 6 +-- openedx/features/discounts/utils.py | 4 +- .../completion_integration/test_handlers.py | 8 +-- .../xblock_integration/xblock_testcase.py | 4 +- 70 files changed, 263 insertions(+), 234 deletions(-) diff --git a/openedx/core/djangoapps/bookmarks/tests/test_models.py b/openedx/core/djangoapps/bookmarks/tests/test_models.py index 2c6877218a..57a7234bcc 100644 --- a/openedx/core/djangoapps/bookmarks/tests/test_models.py +++ b/openedx/core/djangoapps/bookmarks/tests/test_models.py @@ -6,9 +6,9 @@ Tests for Bookmarks models. import datetime from contextlib import contextmanager from unittest import mock +from zoneinfo import ZoneInfo import ddt -import pytz from freezegun import freeze_time from opaque_keys.edx.keys import UsageKey from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator @@ -352,7 +352,7 @@ class BookmarkModelTests(BookmarksTestsBase): bookmark, __ = Bookmark.create(bookmark_data) assert bookmark.xblock_cache is not None - modification_datetime = datetime.datetime.now(pytz.utc) + datetime.timedelta(seconds=seconds_delta) + modification_datetime = datetime.datetime.now(ZoneInfo("UTC")) + datetime.timedelta(seconds=seconds_delta) with freeze_time(modification_datetime): bookmark.xblock_cache.paths = paths bookmark.xblock_cache.save() diff --git a/openedx/core/djangoapps/catalog/utils.py b/openedx/core/djangoapps/catalog/utils.py index 3a241a5c51..10abd33647 100644 --- a/openedx/core/djangoapps/catalog/utils.py +++ b/openedx/core/djangoapps/catalog/utils.py @@ -5,6 +5,7 @@ import datetime import logging import uuid from typing import TYPE_CHECKING, Any, List, Union +from zoneinfo import ZoneInfo import pycountry import requests @@ -13,7 +14,6 @@ from django.core.exceptions import ObjectDoesNotExist from edx_rest_api_client.auth import SuppliedJwtAuth from edx_rest_api_client.client import USER_AGENT from opaque_keys.edx.keys import CourseKey -from pytz import UTC from common.djangoapps.entitlements.utils import is_course_run_entitlement_fulfillable from common.djangoapps.student.models import CourseEnrollment @@ -593,7 +593,7 @@ def get_fulfillable_course_runs_for_entitlement(entitlement, course_runs): enrollable_sessions = [] # Only retrieve list of published course runs that can still be enrolled and upgraded - search_time = datetime.datetime.now(UTC) + search_time = datetime.datetime.now(ZoneInfo("UTC")) for course_run in course_runs: course_id = CourseKey.from_string(course_run.get("key")) (user_enrollment_mode, is_active) = CourseEnrollment.enrollment_mode_for_user( diff --git a/openedx/core/djangoapps/ccxcon/tests/test_api.py b/openedx/core/djangoapps/ccxcon/tests/test_api.py index 762dba70e8..d2d133ba20 100644 --- a/openedx/core/djangoapps/ccxcon/tests/test_api.py +++ b/openedx/core/djangoapps/ccxcon/tests/test_api.py @@ -4,9 +4,9 @@ Unit tests for the API module import datetime from unittest import mock from urllib import parse +from zoneinfo import ZoneInfo import pytest -import pytz from opaque_keys.edx.keys import CourseKey from xmodule.modulestore.django import modulestore from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase @@ -44,10 +44,10 @@ class APIsTestCase(SharedModuleStoreTestCase): # Create a course outline start = datetime.datetime( - 2010, 5, 12, 2, 42, tzinfo=pytz.UTC + 2010, 5, 12, 2, 42, tzinfo=ZoneInfo("UTC") ) due = datetime.datetime( - 2010, 7, 7, 0, 0, tzinfo=pytz.UTC + 2010, 7, 7, 0, 0, tzinfo=ZoneInfo("UTC") ) cls.chapters = [ diff --git a/openedx/core/djangoapps/content/course_overviews/models.py b/openedx/core/djangoapps/content/course_overviews/models.py index c7264fd9d9..c5b453f82e 100644 --- a/openedx/core/djangoapps/content/course_overviews/models.py +++ b/openedx/core/djangoapps/content/course_overviews/models.py @@ -7,8 +7,8 @@ import json import logging from datetime import datetime from urllib.parse import urlparse, urlunparse +from zoneinfo import ZoneInfo -import pytz from ccx_keys.locator import CCXLocator from config_models.models import ConfigurationModel from django.conf import settings @@ -705,7 +705,7 @@ class CourseOverview(TimeStampedModel): course_overviews = course_overviews.filter(**filter_) if active_only: course_overviews = course_overviews.filter( - Q(end__isnull=True) | Q(end__gte=datetime.now().replace(tzinfo=pytz.UTC)) + Q(end__isnull=True) | Q(end__gte=datetime.now().replace(tzinfo=ZoneInfo("UTC"))) ) return course_overviews @@ -737,11 +737,11 @@ class CourseOverview(TimeStampedModel): """ if active_only: return course_overviews.filter( - Q(end__isnull=True) | Q(end__gte=datetime.now().replace(tzinfo=pytz.UTC)) + Q(end__isnull=True) | Q(end__gte=datetime.now().replace(tzinfo=ZoneInfo("UTC"))) ) if archived_only: return course_overviews.filter( - end__lt=datetime.now().replace(tzinfo=pytz.UTC) + end__lt=datetime.now().replace(tzinfo=ZoneInfo("UTC")) ) return course_overviews diff --git a/openedx/core/djangoapps/content/course_overviews/tests/test_course_overviews.py b/openedx/core/djangoapps/content/course_overviews/tests/test_course_overviews.py index 8e95c44825..dd43dcaee8 100644 --- a/openedx/core/djangoapps/content/course_overviews/tests/test_course_overviews.py +++ b/openedx/core/djangoapps/content/course_overviews/tests/test_course_overviews.py @@ -10,8 +10,9 @@ import pytest import datetime # lint-amnesty, pylint: disable=wrong-import-order import itertools # lint-amnesty, pylint: disable=wrong-import-order import math # lint-amnesty, pylint: disable=wrong-import-order +from zoneinfo import ZoneInfo + import ddt -import pytz from django.conf import settings from django.db.utils import IntegrityError from django.test.utils import override_settings @@ -93,7 +94,7 @@ class CourseOverviewTestCase(CatalogIntegrationMixin, ModuleStoreTestCase, Cache """ if date_time is None: return None - epoch = datetime.datetime.utcfromtimestamp(0).replace(tzinfo=pytz.utc) + epoch = datetime.datetime.utcfromtimestamp(0).replace(tzinfo=ZoneInfo("UTC")) return math.floor((date_time - epoch).total_seconds()) # Load the CourseOverview from the cache twice. The first load will be a cache miss (because the cache diff --git a/openedx/core/djangoapps/content/course_overviews/tests/test_signals.py b/openedx/core/djangoapps/content/course_overviews/tests/test_signals.py index 960be6c6ea..ad071acaf9 100644 --- a/openedx/core/djangoapps/content/course_overviews/tests/test_signals.py +++ b/openedx/core/djangoapps/content/course_overviews/tests/test_signals.py @@ -6,10 +6,10 @@ Tests for the course_overviews app's signal functionality. import datetime from unittest.mock import patch from collections import namedtuple +from zoneinfo import ZoneInfo import pytest import ddt -from pytz import UTC from xmodule.data import CertificatesDisplayBehaviors from xmodule.modulestore import ModuleStoreEnum @@ -33,7 +33,7 @@ class CourseOverviewSignalsTestCase(ImmediateOnCommitMixin, ModuleStoreTestCase) """ MODULESTORE = TEST_DATA_ONLY_SPLIT_MODULESTORE_DRAFT_PREFERRED ENABLED_SIGNALS = ['course_deleted', 'course_published'] - TODAY = datetime.datetime.utcnow().replace(tzinfo=UTC) + TODAY = datetime.datetime.utcnow().replace(tzinfo=ZoneInfo("UTC")) NEXT_WEEK = TODAY + datetime.timedelta(days=7) def assert_changed_signal_sent(self, changes, mock_signal): diff --git a/openedx/core/djangoapps/credentials/management/commands/notify_credentials.py b/openedx/core/djangoapps/credentials/management/commands/notify_credentials.py index 7d28ca5197..374975cf3a 100644 --- a/openedx/core/djangoapps/credentials/management/commands/notify_credentials.py +++ b/openedx/core/djangoapps/credentials/management/commands/notify_credentials.py @@ -12,12 +12,13 @@ import logging import shlex from datetime import datetime, timedelta +from zoneinfo import ZoneInfo + import dateutil.parser from django.conf import settings from django.core.management.base import BaseCommand, CommandError from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey -from pytz import UTC from openedx.core.djangoapps.credentials.models import NotifyCredentialsConfig from openedx.core.djangoapps.credentials.tasks.v1.tasks import handle_notify_credentials @@ -32,7 +33,7 @@ log = logging.getLogger(__name__) def parsetime(timestr): dt = dateutil.parser.parse(timestr) if dt.tzinfo is None: - dt = dt.replace(tzinfo=UTC) + dt = dt.replace(tzinfo=ZoneInfo("UTC")) return dt diff --git a/openedx/core/djangoapps/credit/api/provider.py b/openedx/core/djangoapps/credit/api/provider.py index 9875a7d0f5..5628130b37 100644 --- a/openedx/core/djangoapps/credit/api/provider.py +++ b/openedx/core/djangoapps/credit/api/provider.py @@ -7,7 +7,7 @@ import datetime import logging import uuid -import pytz +from zoneinfo import ZoneInfo from django.db import transaction from edx_proctoring.api import get_last_exam_completion_date @@ -296,7 +296,7 @@ def create_credit_request(course_key, provider_id, username): parameters = { "request_uuid": credit_request.uuid, - "timestamp": to_timestamp(datetime.datetime.now(pytz.UTC)), + "timestamp": to_timestamp(datetime.datetime.now(ZoneInfo("UTC"))), "course_org": course_key.org, "course_num": course_key.course, "course_run": course_key.run, diff --git a/openedx/core/djangoapps/credit/models.py b/openedx/core/djangoapps/credit/models.py index 9c14a15104..d6be338281 100644 --- a/openedx/core/djangoapps/credit/models.py +++ b/openedx/core/djangoapps/credit/models.py @@ -10,7 +10,7 @@ import datetime import logging from collections import defaultdict -import pytz +from zoneinfo import ZoneInfo from config_models.models import ConfigurationModel from django.conf import settings from django.core.cache import cache @@ -536,7 +536,7 @@ def default_deadline_for_credit_eligibility(): """ The default deadline to use when creating a new CreditEligibility model. """ - return datetime.datetime.now(pytz.UTC) + datetime.timedelta( + return datetime.datetime.now(ZoneInfo("UTC")) + datetime.timedelta( days=getattr(settings, "CREDIT_ELIGIBILITY_EXPIRATION_DAYS", 365) ) @@ -617,7 +617,7 @@ class CreditEligibility(TimeStampedModel): return cls.objects.filter( username=username, course__enabled=True, - deadline__gt=datetime.datetime.now(pytz.UTC) + deadline__gt=datetime.datetime.now(ZoneInfo("UTC")) ).select_related('course') @classmethod @@ -636,7 +636,7 @@ class CreditEligibility(TimeStampedModel): course__course_key=course_key, course__enabled=True, username=username, - deadline__gt=datetime.datetime.now(pytz.UTC), + deadline__gt=datetime.datetime.now(ZoneInfo("UTC")), ).exists() def __str__(self): diff --git a/openedx/core/djangoapps/credit/serializers.py b/openedx/core/djangoapps/credit/serializers.py index 85e8fed44e..56ffe7a960 100644 --- a/openedx/core/djangoapps/credit/serializers.py +++ b/openedx/core/djangoapps/credit/serializers.py @@ -4,7 +4,7 @@ import datetime import logging -import pytz +from zoneinfo import ZoneInfo from django.conf import settings from rest_framework import serializers from rest_framework.exceptions import PermissionDenied @@ -78,7 +78,7 @@ class CreditProviderCallbackSerializer(serializers.Serializer): # pylint:disabl log.warning(msg) raise serializers.ValidationError(msg) - elapsed = (datetime.datetime.now(pytz.UTC) - date_time).total_seconds() + elapsed = (datetime.datetime.now(ZoneInfo("UTC")) - date_time).total_seconds() if elapsed > settings.CREDIT_PROVIDER_TIMESTAMP_EXPIRATION: msg = f'[{value}] is too far in the past (over [{elapsed}] seconds).' log.warning(msg) diff --git a/openedx/core/djangoapps/credit/tests/factories.py b/openedx/core/djangoapps/credit/tests/factories.py index cd777bdfe9..5489b7bb06 100644 --- a/openedx/core/djangoapps/credit/tests/factories.py +++ b/openedx/core/djangoapps/credit/tests/factories.py @@ -7,7 +7,7 @@ import uuid import factory from factory.fuzzy import FuzzyText -import pytz +from zoneinfo import ZoneInfo from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user from openedx.core.djangoapps.credit.models import ( @@ -80,7 +80,7 @@ class CreditRequestFactory(factory.django.DjangoModelFactory): obj.parameters = json.dumps({ "request_uuid": obj.uuid, - "timestamp": to_timestamp(datetime.datetime.now(pytz.UTC)), + "timestamp": to_timestamp(datetime.datetime.now(ZoneInfo("UTC"))), "course_org": course_key.org, "course_num": course_key.course, "course_run": course_key.run, diff --git a/openedx/core/djangoapps/credit/tests/test_api.py b/openedx/core/djangoapps/credit/tests/test_api.py index 7dc644dc09..2ec31641dd 100644 --- a/openedx/core/djangoapps/credit/tests/test_api.py +++ b/openedx/core/djangoapps/credit/tests/test_api.py @@ -9,7 +9,7 @@ from unittest import mock import pytest import ddt import httpretty -import pytz +from zoneinfo import ZoneInfo from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user from django.core import mail from django.db import connection @@ -400,7 +400,7 @@ class CreditRequirementApiTests(CreditApiTestBase): CreditEligibility.objects.create( course=credit_course, username="staff", - deadline=datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=1) + deadline=datetime.datetime.now(ZoneInfo("UTC")) - datetime.timedelta(days=1) ) # The user should NOT be eligible for credit @@ -960,7 +960,7 @@ class CreditProviderIntegrationApiTests(CreditApiTestBase): # Validate the timestamp assert 'timestamp' in parameters parsed_date = from_timestamp(parameters['timestamp']) - assert parsed_date < datetime.datetime.now(pytz.UTC) + assert parsed_date < datetime.datetime.now(ZoneInfo("UTC")) # Validate course information assert parameters['course_org'] == self.course_key.org diff --git a/openedx/core/djangoapps/credit/tests/test_signals.py b/openedx/core/djangoapps/credit/tests/test_signals.py index c3331c0ecf..592c042a05 100644 --- a/openedx/core/djangoapps/credit/tests/test_signals.py +++ b/openedx/core/djangoapps/credit/tests/test_signals.py @@ -7,7 +7,7 @@ from unittest import mock from uuid import uuid4 import ddt -import pytz +from zoneinfo import ZoneInfo from django.test.client import RequestFactory from opaque_keys.edx.keys import UsageKey from openedx_events.data import EventsMetadata @@ -47,8 +47,8 @@ class TestMinGradedRequirementStatus(ModuleStoreTestCase): satisfied. But if student grade is less than and deadline is passed then user will be marked as failed. """ - VALID_DUE_DATE = datetime.now(pytz.UTC) + timedelta(days=20) - EXPIRED_DUE_DATE = datetime.now(pytz.UTC) - timedelta(days=20) + VALID_DUE_DATE = datetime.now(ZoneInfo("UTC")) + timedelta(days=20) + EXPIRED_DUE_DATE = datetime.now(ZoneInfo("UTC")) - timedelta(days=20) DATES = { 'valid': VALID_DUE_DATE, diff --git a/openedx/core/djangoapps/credit/tests/test_views.py b/openedx/core/djangoapps/credit/tests/test_views.py index deb3c8726a..02428557de 100644 --- a/openedx/core/djangoapps/credit/tests/test_views.py +++ b/openedx/core/djangoapps/credit/tests/test_views.py @@ -7,7 +7,7 @@ import datetime import json import ddt -import pytz +from zoneinfo import ZoneInfo from django.conf import settings from django.test import Client, TestCase from django.test.utils import override_settings @@ -523,7 +523,7 @@ class CreditProviderCallbackViewTests(UserMixin, TestCase): """ provider_id = kwargs.get('provider_id', self.provider.provider_id) secret_key = kwargs.get('secret_key', '931433d583c84ca7ba41784bad3232e6') - timestamp = kwargs.get('timestamp', to_timestamp(datetime.datetime.now(pytz.UTC))) + timestamp = kwargs.get('timestamp', to_timestamp(datetime.datetime.now(ZoneInfo("UTC")))) keys = kwargs.get('keys', {self.provider.provider_id: secret_key}) url = reverse('credit:provider_callback', args=[provider_id]) @@ -577,7 +577,7 @@ class CreditProviderCallbackViewTests(UserMixin, TestCase): if timedelta == 'invalid': timestamp = timedelta else: - timestamp = to_timestamp(datetime.datetime.now(pytz.UTC) + timedelta) + timestamp = to_timestamp(datetime.datetime.now(ZoneInfo("UTC")) + timedelta) request_uuid = self._create_credit_request_and_get_uuid() response = self._credit_provider_callback(request_uuid, 'approved', timestamp=timestamp) assert response.status_code == 400 @@ -585,7 +585,7 @@ class CreditProviderCallbackViewTests(UserMixin, TestCase): def test_post_with_string_timestamp(self): """ Verify the endpoint supports timestamps transmitted as strings instead of integers. """ request_uuid = self._create_credit_request_and_get_uuid() - timestamp = str(to_timestamp(datetime.datetime.now(pytz.UTC))) + timestamp = str(to_timestamp(datetime.datetime.now(ZoneInfo("UTC")))) response = self._credit_provider_callback(request_uuid, 'approved', timestamp=timestamp) assert response.status_code == 200 diff --git a/openedx/core/djangoapps/credit/views.py b/openedx/core/djangoapps/credit/views.py index 2a06f85a32..d61316b49c 100644 --- a/openedx/core/djangoapps/credit/views.py +++ b/openedx/core/djangoapps/credit/views.py @@ -6,7 +6,7 @@ Views for the credit Django app. import datetime import logging -import pytz +from zoneinfo import ZoneInfo from django.conf import settings from django.utils.decorators import method_decorator from django.views.decorators.csrf import csrf_exempt @@ -166,7 +166,7 @@ class CreditEligibilityView(generics.ListAPIView): return queryset.filter( username=username, course__course_key=course_key, - deadline__gt=datetime.datetime.now(pytz.UTC) + deadline__gt=datetime.datetime.now(ZoneInfo("UTC")) ) diff --git a/openedx/core/djangoapps/enrollments/tests/test_data.py b/openedx/core/djangoapps/enrollments/tests/test_data.py index 93b299d75a..491be2cc8c 100644 --- a/openedx/core/djangoapps/enrollments/tests/test_data.py +++ b/openedx/core/djangoapps/enrollments/tests/test_data.py @@ -8,7 +8,7 @@ from unittest.mock import patch import ddt import pytest -from pytz import UTC +from zoneinfo import ZoneInfo from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.course_modes.tests.factories import CourseModeFactory @@ -369,7 +369,7 @@ class EnrollmentDataTest(ModuleStoreTestCase): def _update_verified_mode_as_expired(self, course_id): """Dry method to change verified mode expiration.""" mode = CourseMode.objects.get(course_id=course_id, mode_slug=CourseMode.VERIFIED) - mode.expiration_datetime = datetime.datetime(year=1970, month=1, day=1, tzinfo=UTC) + mode.expiration_datetime = datetime.datetime(year=1970, month=1, day=1, tzinfo=ZoneInfo("UTC")) mode.save() def assert_enrollment_modes(self, expected_modes, include_expired): diff --git a/openedx/core/djangoapps/enrollments/tests/test_views.py b/openedx/core/djangoapps/enrollments/tests/test_views.py index a6b34cbfc6..31510c3b93 100644 --- a/openedx/core/djangoapps/enrollments/tests/test_views.py +++ b/openedx/core/djangoapps/enrollments/tests/test_views.py @@ -12,7 +12,7 @@ from urllib.parse import quote import ddt import httpretty import pytest -import pytz +from zoneinfo import ZoneInfo from django.conf import settings from django.core.cache import cache from django.core.exceptions import ImproperlyConfigured @@ -356,8 +356,8 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase, Ente @ddt.unpack def test_force_enrollment(self, course_modes, enrollment_mode, force_enrollment): # Create the course modes (if any) required for this test case - start_date = datetime.datetime(2021, 12, 1, 5, 0, 0, tzinfo=pytz.UTC) - end_date = datetime.datetime(2022, 12, 1, 5, 0, 0, tzinfo=pytz.UTC) + start_date = datetime.datetime(2021, 12, 1, 5, 0, 0, tzinfo=ZoneInfo("UTC")) + end_date = datetime.datetime(2022, 12, 1, 5, 0, 0, tzinfo=ZoneInfo("UTC")) self.course = CourseFactory.create( emit_signals=True, start=start_date, @@ -658,11 +658,11 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase, Ente # enforced at the data layer, so we need to handle the case # in which no dates are specified. (None, None, None, None), - (datetime.datetime(2015, 1, 2, 3, 4, 5, tzinfo=pytz.UTC), None, "2015-01-02T03:04:05Z", None), - (None, datetime.datetime(2015, 1, 2, 3, 4, 5, tzinfo=pytz.UTC), None, "2015-01-02T03:04:05Z"), + (datetime.datetime(2015, 1, 2, 3, 4, 5, tzinfo=ZoneInfo("UTC")), None, "2015-01-02T03:04:05Z", None), + (None, datetime.datetime(2015, 1, 2, 3, 4, 5, tzinfo=ZoneInfo("UTC")), None, "2015-01-02T03:04:05Z"), ( - datetime.datetime(2014, 6, 7, 8, 9, 10, tzinfo=pytz.UTC), - datetime.datetime(2015, 1, 2, 3, 4, 5, tzinfo=pytz.UTC), + datetime.datetime(2014, 6, 7, 8, 9, 10, tzinfo=ZoneInfo("UTC")), + datetime.datetime(2015, 1, 2, 3, 4, 5, tzinfo=ZoneInfo("UTC")), "2014-06-07T08:09:10Z", "2015-01-02T03:04:05Z", ), @@ -1078,7 +1078,7 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase, Ente # Change verified mode expiration. mode = CourseMode.objects.get(course_id=self.course.id, mode_slug=CourseMode.VERIFIED) - mode.expiration_datetime = datetime.datetime(year=1970, month=1, day=1, tzinfo=pytz.utc) + mode.expiration_datetime = datetime.datetime(year=1970, month=1, day=1, tzinfo=ZoneInfo("UTC")) mode.save() # Deactivate enrollment. @@ -1198,7 +1198,7 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase, Ente # Change verified mode expiration. mode = CourseMode.objects.get(course_id=self.course.id, mode_slug=CourseMode.VERIFIED) - mode.expiration_datetime = datetime.datetime(year=1970, month=1, day=1, tzinfo=pytz.utc) + mode.expiration_datetime = datetime.datetime(year=1970, month=1, day=1, tzinfo=ZoneInfo("UTC")) mode.save() self.assert_enrollment_status( as_server=using_api_key, @@ -1784,7 +1784,7 @@ class CourseEnrollmentsApiListTest(APITestCase, ModuleStoreTestCase): """ Test the course enrollments list API. """ - CREATED_DATA = datetime.datetime(2018, 1, 1, 0, 0, 1, tzinfo=pytz.UTC) + CREATED_DATA = datetime.datetime(2018, 1, 1, 0, 0, 1, tzinfo=ZoneInfo("UTC")) def setUp(self): super().setUp() diff --git a/openedx/core/djangoapps/models/tests/test_course_details.py b/openedx/core/djangoapps/models/tests/test_course_details.py index 41e739ecb4..b23b56c88a 100644 --- a/openedx/core/djangoapps/models/tests/test_course_details.py +++ b/openedx/core/djangoapps/models/tests/test_course_details.py @@ -7,7 +7,7 @@ import datetime from django.test import override_settings import pytest import ddt -from pytz import UTC +from zoneinfo import ZoneInfo from django.conf import settings from xmodule.modulestore import ModuleStoreEnum @@ -86,13 +86,13 @@ class CourseDetailsTestCase(ModuleStoreTestCase): jsondetails.self_paced = True assert CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).self_paced ==\ jsondetails.self_paced - jsondetails.start_date = datetime.datetime(2010, 10, 1, 0, tzinfo=UTC) + jsondetails.start_date = datetime.datetime(2010, 10, 1, 0, tzinfo=ZoneInfo("UTC")) assert CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).start_date ==\ jsondetails.start_date - jsondetails.end_date = datetime.datetime(2011, 10, 1, 0, tzinfo=UTC) + jsondetails.end_date = datetime.datetime(2011, 10, 1, 0, tzinfo=ZoneInfo("UTC")) assert CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).end_date ==\ jsondetails.end_date - jsondetails.certificate_available_date = datetime.datetime(2010, 10, 1, 0, tzinfo=UTC) + jsondetails.certificate_available_date = datetime.datetime(2010, 10, 1, 0, tzinfo=ZoneInfo("UTC")) assert CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user)\ .certificate_available_date == jsondetails.certificate_available_date jsondetails.course_image_name = "an_image.jpg" @@ -126,7 +126,7 @@ class CourseDetailsTestCase(ModuleStoreTestCase): jsondetails.instructor_info def test_toggle_pacing_during_course_run(self): - self.course.start = datetime.datetime.now(UTC) + self.course.start = datetime.datetime.now(ZoneInfo("UTC")) self.store.update_item(self.course, self.user.id) details = CourseDetails.fetch(self.course.id) diff --git a/openedx/core/djangoapps/notifications/tasks.py b/openedx/core/djangoapps/notifications/tasks.py index fb9f95990d..290a7c02b3 100644 --- a/openedx/core/djangoapps/notifications/tasks.py +++ b/openedx/core/djangoapps/notifications/tasks.py @@ -10,7 +10,7 @@ from django.conf import settings from django.core.exceptions import ValidationError from edx_django_utils.monitoring import set_code_owner_attribute from opaque_keys.edx.keys import CourseKey -from pytz import UTC +from zoneinfo import ZoneInfo from openedx.core.djangoapps.notifications.audience_filters import NotificationFilter from openedx.core.djangoapps.notifications.base_notification import ( @@ -75,7 +75,7 @@ def delete_expired_notifications(): This task deletes all expired notifications """ batch_size = settings.EXPIRED_NOTIFICATIONS_DELETE_BATCH_SIZE - expiry_date = datetime.now(UTC) - timedelta(days=settings.NOTIFICATIONS_EXPIRY) + expiry_date = datetime.now(ZoneInfo("UTC")) - timedelta(days=settings.NOTIFICATIONS_EXPIRY) start_time = datetime.now() total_deleted = 0 delete_count = None diff --git a/openedx/core/djangoapps/notifications/tests/test_notification_grouping.py b/openedx/core/djangoapps/notifications/tests/test_notification_grouping.py index debd72d901..fd28bb999e 100644 --- a/openedx/core/djangoapps/notifications/tests/test_notification_grouping.py +++ b/openedx/core/djangoapps/notifications/tests/test_notification_grouping.py @@ -6,7 +6,7 @@ import ddt import unittest from unittest.mock import MagicMock, patch from datetime import datetime -from pytz import utc +from zoneinfo import ZoneInfo from common.djangoapps.student.tests.factories import UserFactory from openedx.core.djangoapps.notifications.grouping_notifications import ( @@ -128,7 +128,7 @@ class TestGroupUserNotifications(ModuleStoreTestCase): self.assertFalse(old_notification.save.called) - @ddt.data(datetime(2023, 1, 1, tzinfo=utc), None) + @ddt.data(datetime(2023, 1, 1, tzinfo=ZoneInfo("UTC")), None) def test_not_grouped_when_notification_is_seen(self, last_seen): """ Notification is not grouped if the notification is marked as seen @@ -172,11 +172,11 @@ class TestGetUserExistingNotifications(unittest.TestCase): # Mock the notification objects returned by the filter mock_notification1 = MagicMock(spec=Notification) mock_notification1.user_id = 1 - mock_notification1.created = datetime(2023, 9, 1, tzinfo=utc) + mock_notification1.created = datetime(2023, 9, 1, tzinfo=ZoneInfo("UTC")) mock_notification2 = MagicMock(spec=Notification) mock_notification2.user_id = 1 - mock_notification2.created = datetime(2023, 9, 2, tzinfo=utc) + mock_notification2.created = datetime(2023, 9, 2, tzinfo=ZoneInfo("UTC")) mock_filter.return_value = [mock_notification1, mock_notification2] diff --git a/openedx/core/djangoapps/notifications/tests/test_views.py b/openedx/core/djangoapps/notifications/tests/test_views.py index 06d615f07d..7148295dcf 100644 --- a/openedx/core/djangoapps/notifications/tests/test_views.py +++ b/openedx/core/djangoapps/notifications/tests/test_views.py @@ -11,7 +11,7 @@ from django.core.cache import cache from django.test.utils import override_settings from django.urls import reverse from edx_toggles.toggles.testutils import override_waffle_flag -from pytz import UTC +from zoneinfo import ZoneInfo from rest_framework import status from rest_framework.test import APIClient, APITestCase @@ -192,7 +192,7 @@ class NotificationListAPIViewTest(APITestCase): """ Test that the view can filter notifications by expiry date. """ - today = datetime.now(UTC) + today = datetime.now(ZoneInfo("UTC")) # Create two notifications for the user, one with current date and other with expiry date. Notification.objects.create( diff --git a/openedx/core/djangoapps/notifications/views.py b/openedx/core/djangoapps/notifications/views.py index 091be365d4..041f68f956 100644 --- a/openedx/core/djangoapps/notifications/views.py +++ b/openedx/core/djangoapps/notifications/views.py @@ -8,7 +8,7 @@ from django.db.models import Count from django_ratelimit.core import is_ratelimited from django.shortcuts import get_object_or_404 from django.utils.translation import gettext as _ -from pytz import UTC +from zoneinfo import ZoneInfo from rest_framework import generics, status from rest_framework.decorators import api_view from rest_framework.generics import UpdateAPIView @@ -80,7 +80,7 @@ class NotificationListAPIView(generics.ListAPIView): """ Override the get_queryset method to filter the queryset by app name, request.user and created """ - expiry_date = datetime.now(UTC) - timedelta(days=settings.NOTIFICATIONS_EXPIRY) + expiry_date = datetime.now(ZoneInfo("UTC")) - timedelta(days=settings.NOTIFICATIONS_EXPIRY) app_name = self.request.query_params.get('app_name') if self.request.query_params.get('tray_opened'): @@ -212,7 +212,7 @@ class NotificationReadAPIView(APIView): - 404: Not Found status code if the notification was not found. """ notification_id = request.data.get('notification_id', None) - read_at = datetime.now(UTC) + read_at = datetime.now(ZoneInfo("UTC")) if notification_id: notification = get_object_or_404(Notification, pk=notification_id, user=request.user) diff --git a/openedx/core/djangoapps/oauth_dispatch/dot_overrides/validators.py b/openedx/core/djangoapps/oauth_dispatch/dot_overrides/validators.py index f8cf053814..773c344db0 100644 --- a/openedx/core/djangoapps/oauth_dispatch/dot_overrides/validators.py +++ b/openedx/core/djangoapps/oauth_dispatch/dot_overrides/validators.py @@ -11,7 +11,7 @@ from django.dispatch import receiver from oauth2_provider.models import AccessToken from oauth2_provider.oauth2_validators import OAuth2Validator from oauth2_provider.scopes import get_scopes_backend -from pytz import utc +from zoneinfo import ZoneInfo from ..models import RestrictedApplication # pylint: disable=W0223 @@ -23,7 +23,7 @@ def on_access_token_presave(sender, instance, *args, **kwargs): # pylint: disab Mark AccessTokens as expired for 'restricted applications' if required. """ if RestrictedApplication.should_expire_access_token(instance.application): - instance.expires = datetime(1970, 1, 1, tzinfo=utc) + instance.expires = datetime(1970, 1, 1, tzinfo=ZoneInfo("UTC")) class EdxOAuth2Validator(OAuth2Validator): @@ -152,4 +152,4 @@ def _get_utc_now(): """ Return current time in UTC. """ - return datetime.utcnow().replace(tzinfo=utc) + return datetime.utcnow().replace(tzinfo=ZoneInfo("UTC")) diff --git a/openedx/core/djangoapps/oauth_dispatch/models.py b/openedx/core/djangoapps/oauth_dispatch/models.py index 2e635167e4..3869698af4 100644 --- a/openedx/core/djangoapps/oauth_dispatch/models.py +++ b/openedx/core/djangoapps/oauth_dispatch/models.py @@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _ from django_mysql.models import ListCharField from oauth2_provider.settings import oauth2_settings from organizations.models import Organization -from pytz import utc +from zoneinfo import ZoneInfo from openedx.core.djangolib.markup import HTML from openedx.core.lib.request_utils import get_request_or_stub @@ -53,7 +53,7 @@ class RestrictedApplication(models.Model): For access_tokens for RestrictedApplications, make sure that the expiry date is set at the beginning of the epoch which is Jan. 1, 1970 """ - return access_token.expires == datetime(1970, 1, 1, tzinfo=utc) + return access_token.expires == datetime(1970, 1, 1, tzinfo=ZoneInfo("UTC")) class ApplicationAccess(models.Model): diff --git a/openedx/core/djangoapps/oauth_dispatch/tests/factories.py b/openedx/core/djangoapps/oauth_dispatch/tests/factories.py index 473bcd4ced..7d7be8dd7f 100644 --- a/openedx/core/djangoapps/oauth_dispatch/tests/factories.py +++ b/openedx/core/djangoapps/oauth_dispatch/tests/factories.py @@ -4,7 +4,7 @@ from datetime import datetime, timedelta import factory -import pytz +from zoneinfo import ZoneInfo from factory.django import DjangoModelFactory from factory.fuzzy import FuzzyText from oauth2_provider.models import AccessToken, Application, RefreshToken @@ -39,7 +39,7 @@ class AccessTokenFactory(DjangoModelFactory): django_get_or_create = ('user', 'application') token = FuzzyText(length=32) - expires = datetime.now(pytz.UTC) + timedelta(days=1) + expires = datetime.now(ZoneInfo("UTC")) + timedelta(days=1) class RefreshTokenFactory(DjangoModelFactory): diff --git a/openedx/core/djangoapps/password_policy/compliance.py b/openedx/core/djangoapps/password_policy/compliance.py index fdd103d243..8601a55d65 100644 --- a/openedx/core/djangoapps/password_policy/compliance.py +++ b/openedx/core/djangoapps/password_policy/compliance.py @@ -4,7 +4,7 @@ Utilities for enforcing and tracking compliance with password policy rules. from datetime import datetime -import pytz +from zoneinfo import ZoneInfo from django.conf import settings from django.utils.translation import gettext as _ @@ -69,7 +69,7 @@ def enforce_compliance_on_login(user, password): if deadline is None: return - now = datetime.now(pytz.UTC) + now = datetime.now(ZoneInfo("UTC")) if now >= deadline: # lint-amnesty, pylint: disable=no-else-raise raise NonCompliantPasswordException( HTML(_( diff --git a/openedx/core/djangoapps/password_policy/tests/test_compliance.py b/openedx/core/djangoapps/password_policy/tests/test_compliance.py index cb803bed99..5d8a56d60c 100644 --- a/openedx/core/djangoapps/password_policy/tests/test_compliance.py +++ b/openedx/core/djangoapps/password_policy/tests/test_compliance.py @@ -6,7 +6,7 @@ from datetime import datetime, timedelta from unittest.mock import patch import pytest -import pytz +from zoneinfo import ZoneInfo from dateutil.parser import parse as parse_date from django.test import TestCase, override_settings @@ -75,7 +75,7 @@ class TestCompliance(TestCase): mock_check_user_compliance.return_value = False with patch('openedx.core.djangoapps.password_policy.compliance._get_compliance_deadline_for_user') as \ mock_get_compliance_deadline_for_user: - mock_get_compliance_deadline_for_user.return_value = datetime.now(pytz.UTC) - timedelta(1) + mock_get_compliance_deadline_for_user.return_value = datetime.now(ZoneInfo("UTC")) - timedelta(1) pytest.raises(NonCompliantPasswordException, enforce_compliance_on_login, user, password) # Test deadline is in the future @@ -84,7 +84,7 @@ class TestCompliance(TestCase): mock_check_user_compliance.return_value = False with patch('openedx.core.djangoapps.password_policy.compliance._get_compliance_deadline_for_user') as \ mock_get_compliance_deadline_for_user: - mock_get_compliance_deadline_for_user.return_value = datetime.now(pytz.UTC) + timedelta(1) + mock_get_compliance_deadline_for_user.return_value = datetime.now(ZoneInfo("UTC")) + timedelta(1) assert pytest.raises(NonCompliantPasswordWarning, enforce_compliance_on_login, user, password) def test_check_user_compliance(self): diff --git a/openedx/core/djangoapps/profile_images/tests/test_views.py b/openedx/core/djangoapps/profile_images/tests/test_views.py index 0a27637758..963c0956c2 100644 --- a/openedx/core/djangoapps/profile_images/tests/test_views.py +++ b/openedx/core/djangoapps/profile_images/tests/test_views.py @@ -7,7 +7,7 @@ from unittest.mock import patch import pytest import datetime # lint-amnesty, pylint: disable=wrong-import-order -from pytz import UTC +from zoneinfo import ZoneInfo from django.urls import reverse from django.http import HttpResponse @@ -30,8 +30,8 @@ from ..views import LOG_MESSAGE_CREATE, LOG_MESSAGE_DELETE from .helpers import make_image_file TEST_PASSWORD = "test" -TEST_UPLOAD_DT = datetime.datetime(2002, 1, 9, 15, 43, 1, tzinfo=UTC) -TEST_UPLOAD_DT2 = datetime.datetime(2003, 1, 9, 15, 43, 1, tzinfo=UTC) +TEST_UPLOAD_DT = datetime.datetime(2002, 1, 9, 15, 43, 1, tzinfo=ZoneInfo("UTC")) +TEST_UPLOAD_DT2 = datetime.datetime(2003, 1, 9, 15, 43, 1, tzinfo=ZoneInfo("UTC")) class ProfileImageEndpointMixin(UserSettingsEventTestMixin): diff --git a/openedx/core/djangoapps/profile_images/views.py b/openedx/core/djangoapps/profile_images/views.py index b88b3ad32b..1c9d4fcf3b 100644 --- a/openedx/core/djangoapps/profile_images/views.py +++ b/openedx/core/djangoapps/profile_images/views.py @@ -11,7 +11,7 @@ from contextlib import closing from django.utils.translation import gettext as _ from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser -from pytz import UTC +from zoneinfo import ZoneInfo from rest_framework import permissions, status from rest_framework.parsers import FormParser, MultiPartParser from rest_framework.response import Response @@ -38,7 +38,7 @@ def _make_upload_dt(): Generate a server-side timestamp for the upload. This is in a separate function so its behavior can be overridden in tests. """ - return datetime.datetime.utcnow().replace(tzinfo=UTC) + return datetime.datetime.utcnow().replace(tzinfo=ZoneInfo("UTC")) class ProfileImageView(DeveloperErrorViewMixin, APIView): diff --git a/openedx/core/djangoapps/programs/tests/test_tasks.py b/openedx/core/djangoapps/programs/tests/test_tasks.py index e2b1c554c8..943c8c0dd4 100644 --- a/openedx/core/djangoapps/programs/tests/test_tasks.py +++ b/openedx/core/djangoapps/programs/tests/test_tasks.py @@ -10,7 +10,7 @@ from unittest import mock import ddt import httpretty import pytest -import pytz +from zoneinfo import ZoneInfo import requests from celery.exceptions import MaxRetriesExceededError from django.conf import settings @@ -520,7 +520,7 @@ class AwardCourseCertificatesTestCase(CredentialsApiConfigMixin, TestCase): def setUp(self): super().setUp() - self.available_date = datetime.now(pytz.UTC) + timedelta(days=1) + self.available_date = datetime.now(ZoneInfo("UTC")) + timedelta(days=1) self.course = CourseOverviewFactory.create( self_paced=True, # Any option to allow the certificate to be viewable for the course certificate_available_date=self.available_date, @@ -1023,7 +1023,7 @@ class UpdateCertificateAvailableDateOnCourseUpdateTestCase(CredentialsApiConfigM def setUp(self): super().setUp() - self.end_date = datetime.now(pytz.UTC) + timedelta(days=90) + self.end_date = datetime.now(ZoneInfo("UTC")) + timedelta(days=90) self.credentials_api_config = self.create_credentials_config(enabled=False) def tearDown(self): @@ -1135,7 +1135,7 @@ class UpdateCertificateAvailableDateOnCourseUpdateTestCase(CredentialsApiConfigM explicitly set as part of the course overview. """ self._update_credentials_api_config(True) - certificate_available_date = datetime.now(pytz.UTC) + timedelta(days=120) + certificate_available_date = datetime.now(ZoneInfo("UTC")) + timedelta(days=120) course_overview = self._create_course_overview( False, @@ -1168,7 +1168,7 @@ class UpdateCertificateAvailableDateOnCourseUpdateTestCase(CredentialsApiConfigM invalid data is set in a course overview, we don't pass it to Credentials. """ self._update_credentials_api_config(True) - certificate_available_date = datetime.now(pytz.UTC) + timedelta(days=120) + certificate_available_date = datetime.now(ZoneInfo("UTC")) + timedelta(days=120) course_overview = self._create_course_overview( True, diff --git a/openedx/core/djangoapps/programs/tests/test_utils.py b/openedx/core/djangoapps/programs/tests/test_utils.py index 264f1a6aee..18058c827a 100644 --- a/openedx/core/djangoapps/programs/tests/test_utils.py +++ b/openedx/core/djangoapps/programs/tests/test_utils.py @@ -15,7 +15,7 @@ from django.test.utils import override_settings from django.urls import reverse from edx_toggles.toggles.testutils import override_waffle_switch from opaque_keys.edx.keys import CourseKey # lint-amnesty, pylint: disable=wrong-import-order -from pytz import utc +from zoneinfo import ZoneInfo from testfixtures import LogCapture from common.djangoapps.course_modes.models import CourseMode @@ -209,7 +209,7 @@ class TestProgramProgressMeter(ModuleStoreTestCase): CourseEntitlementFactory.create( user=self.user, course_uuid=course_uuid, - expired_at=datetime.datetime.now(utc), + expired_at=datetime.datetime.now(ZoneInfo("UTC")), mode=CourseMode.VERIFIED, enrollment_course_run=enrollment @@ -308,7 +308,7 @@ class TestProgramProgressMeter(ModuleStoreTestCase): the right type for which the upgrade deadline has not passed. """ course_run_key = generate_course_run_key() - now = datetime.datetime.now(utc) + now = datetime.datetime.now(ZoneInfo("UTC")) upgrade_deadline = None if not offset else str(now + datetime.timedelta(days=offset)) required_seat = SeatFactory(type=CourseMode.VERIFIED, upgrade_deadline=upgrade_deadline) enrolled_seat = SeatFactory(type=CourseMode.AUDIT) @@ -488,7 +488,7 @@ class TestProgramProgressMeter(ModuleStoreTestCase): def test_simulate_progress(self, mock_get_programs): # lint-amnesty, pylint: disable=too-many-statements """Simulate the entirety of a user's progress through a program.""" - today = datetime.datetime.now(utc) + today = datetime.datetime.now(ZoneInfo("UTC")) two_days_ago = today - datetime.timedelta(days=2) three_days_ago = today - datetime.timedelta(days=3) yesterday = today - datetime.timedelta(days=1) @@ -862,8 +862,8 @@ def _create_course(self, course_price, course_run_count=1, make_entitlement=Fals course_runs = [] for x in range(course_run_count): course = ModuleStoreCourseFactory.create(run='Run_' + str(x)) - course.start = datetime.datetime.now(utc) - datetime.timedelta(days=1) - course.end = datetime.datetime.now(utc) + datetime.timedelta(days=1) + course.start = datetime.datetime.now(ZoneInfo("UTC")) - datetime.timedelta(days=1) + course.end = datetime.datetime.now(ZoneInfo("UTC")) + datetime.timedelta(days=1) course.instructor_info = self.instructors course = self.update_course(course, self.user.id) @@ -899,8 +899,8 @@ class TestProgramDataExtender(ModuleStoreTestCase): super().setUp() self.course = ModuleStoreCourseFactory() - self.course.start = datetime.datetime.now(utc) - datetime.timedelta(days=1) - self.course.end = datetime.datetime.now(utc) + datetime.timedelta(days=1) + self.course.start = datetime.datetime.now(ZoneInfo("UTC")) - datetime.timedelta(days=1) + self.course.end = datetime.datetime.now(ZoneInfo("UTC")) + datetime.timedelta(days=1) self.course = self.update_course(self.course, self.user.id) self.course_run = CourseRunFactory(key=str(self.course.id)) @@ -941,7 +941,7 @@ class TestProgramDataExtender(ModuleStoreTestCase): Verify that changes to the course run end date do not affect our assessment of the course run being open for enrollment. """ - self.course.end = datetime.datetime.now(utc) + datetime.timedelta(days=days_offset) + self.course.end = datetime.datetime.now(ZoneInfo("UTC")) + datetime.timedelta(days=days_offset) self.course = self.update_course(self.course, self.user.id) data = ProgramDataExtender(self.program, self.user).extend() @@ -1022,8 +1022,8 @@ class TestProgramDataExtender(ModuleStoreTestCase): """ Verify that course run enrollment status is reflected correctly. """ - self.course.enrollment_start = datetime.datetime.now(utc) - datetime.timedelta(days=start_offset) - self.course.enrollment_end = datetime.datetime.now(utc) - datetime.timedelta(days=end_offset) + self.course.enrollment_start = datetime.datetime.now(ZoneInfo("UTC")) - datetime.timedelta(days=start_offset) + self.course.enrollment_end = datetime.datetime.now(ZoneInfo("UTC")) - datetime.timedelta(days=end_offset) self.course = self.update_course(self.course, self.user.id) @@ -1040,7 +1040,7 @@ class TestProgramDataExtender(ModuleStoreTestCase): Verify that a closed course run with no explicit enrollment start date doesn't cause an error. Regression test for ECOM-4973. """ - self.course.enrollment_end = datetime.datetime.now(utc) - datetime.timedelta(days=1) + self.course.enrollment_end = datetime.datetime.now(ZoneInfo("UTC")) - datetime.timedelta(days=1) self.course = self.update_course(self.course, self.user.id) data = ProgramDataExtender(self.program, self.user).extend() diff --git a/openedx/core/djangoapps/programs/utils.py b/openedx/core/djangoapps/programs/utils.py index 76263c4b40..49fb054b9b 100644 --- a/openedx/core/djangoapps/programs/utils.py +++ b/openedx/core/djangoapps/programs/utils.py @@ -14,7 +14,7 @@ from django.core.cache import cache from django.urls import reverse from django.utils.functional import cached_property from opaque_keys.edx.keys import CourseKey -from pytz import utc +from zoneinfo import ZoneInfo from requests.exceptions import RequestException from common.djangoapps.course_modes.api import get_paid_modes_for_course @@ -43,7 +43,7 @@ from openedx.core.djangoapps.site_configuration import helpers as configuration_ from xmodule.modulestore.django import modulestore # The datetime module's strftime() methods require a year >= 1900. -DEFAULT_ENROLLMENT_START_DATE = datetime.datetime(1900, 1, 1, tzinfo=utc) +DEFAULT_ENROLLMENT_START_DATE = datetime.datetime(1900, 1, 1, tzinfo=ZoneInfo("UTC")) log = logging.getLogger(__name__) @@ -286,7 +286,7 @@ class ProgramProgressMeter: list of dict, each containing information about a user's progress towards completing a program. """ - now = datetime.datetime.now(utc) + now = datetime.datetime.now(ZoneInfo("UTC")) progress = [] programs = programs or self.engaged_programs @@ -598,15 +598,17 @@ class ProgramDataExtender: run_mode["enrollment_open_date"] = strftime_localized(self.enrollment_start, "SHORT_DATE") def _attach_course_run_is_course_ended(self, run_mode): - end_date = self.course_overview.end or datetime.datetime.max.replace(tzinfo=utc) - run_mode["is_course_ended"] = end_date < datetime.datetime.now(utc) + end_date = self.course_overview.end or datetime.datetime.max.replace(tzinfo=ZoneInfo("UTC")) + run_mode["is_course_ended"] = end_date < datetime.datetime.now(ZoneInfo("UTC")) def _attach_course_run_is_enrolled(self, run_mode): run_mode["is_enrolled"] = CourseEnrollment.is_enrolled(self.user, self.course_run_key) def _attach_course_run_is_enrollment_open(self, run_mode): - enrollment_end = self.course_overview.enrollment_end or datetime.datetime.max.replace(tzinfo=utc) - run_mode["is_enrollment_open"] = self.enrollment_start <= datetime.datetime.now(utc) < enrollment_end + enrollment_end = self.course_overview.enrollment_end or datetime.datetime.max.replace(tzinfo=ZoneInfo("UTC")) + run_mode["is_enrollment_open"] = ( + self.enrollment_start <= datetime.datetime.now(ZoneInfo("UTC")) < enrollment_end + ) def _attach_course_run_advertised_start(self, run_mode): """ diff --git a/openedx/core/djangoapps/schedules/management/commands/__init__.py b/openedx/core/djangoapps/schedules/management/commands/__init__.py index 0b72559765..16484ef367 100644 --- a/openedx/core/djangoapps/schedules/management/commands/__init__.py +++ b/openedx/core/djangoapps/schedules/management/commands/__init__.py @@ -5,7 +5,7 @@ Base management command for sending emails import datetime -import pytz +from zoneinfo import ZoneInfo from django.contrib.sites.models import Site from django.core.management.base import BaseCommand @@ -62,7 +62,7 @@ class SendEmailBaseCommand(PrefixedDebugLoggerMixin, BaseCommand): # lint-amnes current_date = datetime.datetime( *[int(x) for x in options['date'].split('-')], - tzinfo=pytz.UTC + tzinfo=ZoneInfo("UTC") ) self.log_debug('Current date = %s', current_date.isoformat()) override_recipient_email = options.get('override_recipient_email') diff --git a/openedx/core/djangoapps/schedules/management/commands/send_course_next_section_update.py b/openedx/core/djangoapps/schedules/management/commands/send_course_next_section_update.py index 53e2100649..6faa923088 100644 --- a/openedx/core/djangoapps/schedules/management/commands/send_course_next_section_update.py +++ b/openedx/core/djangoapps/schedules/management/commands/send_course_next_section_update.py @@ -3,7 +3,7 @@ Management command to send Schedule course updates """ import datetime -import pytz +from zoneinfo import ZoneInfo from textwrap import dedent # lint-amnesty, pylint: disable=wrong-import-order from django.contrib.sites.models import Site @@ -23,7 +23,7 @@ class Command(SendEmailBaseCommand): def handle(self, *args, ** options): current_date = datetime.datetime( *[int(x) for x in options['date'].split('-')], - tzinfo=pytz.UTC + tzinfo=ZoneInfo("UTC") ) site = Site.objects.get(domain__iexact=options['site_domain_name']) diff --git a/openedx/core/djangoapps/schedules/management/commands/setup_models_to_send_test_emails.py b/openedx/core/djangoapps/schedules/management/commands/setup_models_to_send_test_emails.py index 976f1aa16f..587271fc59 100644 --- a/openedx/core/djangoapps/schedules/management/commands/setup_models_to_send_test_emails.py +++ b/openedx/core/djangoapps/schedules/management/commands/setup_models_to_send_test_emails.py @@ -7,7 +7,7 @@ import datetime from textwrap import dedent import factory -import pytz +from zoneinfo import ZoneInfo from django.contrib.sites.models import Site from django.core.management.base import BaseCommand @@ -26,29 +26,29 @@ class ThreeDayNudgeSchedule(ScheduleFactory): """ A ScheduleFactory that creates a Schedule set up for a 3-day nudge email. """ - start_date = factory.Faker('date_time_between', start_date='-3d', end_date='-3d', tzinfo=pytz.UTC) + start_date = factory.Faker('date_time_between', start_date='-3d', end_date='-3d', tzinfo=ZoneInfo("UTC")) class TenDayNudgeSchedule(ScheduleFactory): """ A ScheduleFactory that creates a Schedule set up for a 10-day nudge email. """ - start_date = factory.Faker('date_time_between', start_date='-10d', end_date='-10d', tzinfo=pytz.UTC) + start_date = factory.Faker('date_time_between', start_date='-10d', end_date='-10d', tzinfo=ZoneInfo("UTC")) class UpgradeReminderSchedule(ScheduleFactory): """ A ScheduleFactory that creates a Schedule set up for a 2-days-remaining upgrade reminder. """ - start_date = factory.Faker('past_datetime', tzinfo=pytz.UTC) - upgrade_deadline = factory.Faker('date_time_between', start_date='+2d', end_date='+2d', tzinfo=pytz.UTC) + start_date = factory.Faker('past_datetime', tzinfo=ZoneInfo("UTC")) + upgrade_deadline = factory.Faker('date_time_between', start_date='+2d', end_date='+2d', tzinfo=ZoneInfo("UTC")) class ContentHighlightSchedule(ScheduleFactory): """ A ScheduleFactory that creates a Schedule set up for a course highlights email. """ - start_date = factory.Faker('date_time_between', start_date='-7d', end_date='-7d', tzinfo=pytz.UTC) + start_date = factory.Faker('date_time_between', start_date='-7d', end_date='-7d', tzinfo=ZoneInfo("UTC")) experience = factory.RelatedFactory(ScheduleExperienceFactory, 'schedule', experience_type=ScheduleExperience.EXPERIENCES.course_updates) # lint-amnesty, pylint: disable=line-too-long diff --git a/openedx/core/djangoapps/schedules/management/commands/tests/send_email_base.py b/openedx/core/djangoapps/schedules/management/commands/tests/send_email_base.py index 774f1f4181..c0a6395464 100644 --- a/openedx/core/djangoapps/schedules/management/commands/tests/send_email_base.py +++ b/openedx/core/djangoapps/schedules/management/commands/tests/send_email_base.py @@ -11,7 +11,7 @@ from unittest.mock import Mock, patch import attr import ddt -import pytz +from zoneinfo import ZoneInfo from django.conf import settings from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user from django.db.models import Max @@ -119,7 +119,7 @@ class ScheduleSendEmailTestMixin(FilteredQueryCountMixin): # lint-amnesty, pyli return max_user_id + num_bins - (max_user_id % num_bins) def _get_dates(self, offset=None): # lint-amnesty, pylint: disable=missing-function-docstring - current_day = _get_datetime_beginning_of_day(datetime.datetime.now(pytz.UTC)) + current_day = _get_datetime_beginning_of_day(datetime.datetime.now(ZoneInfo("UTC"))) offset = offset or self.expected_offsets[0] target_day = current_day + datetime.timedelta(days=offset) if self.resolver.schedule_date_field == 'upgrade_deadline': @@ -148,7 +148,7 @@ class ScheduleSendEmailTestMixin(FilteredQueryCountMixin): # lint-amnesty, pyli CourseModeFactory( course_id=course_id, mode_slug=CourseMode.VERIFIED, - expiration_datetime=datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=30), + expiration_datetime=datetime.datetime.now(ZoneInfo("UTC")) + datetime.timedelta(days=30), ) self._courses_with_verified_modes.add(course_id) return schedule @@ -158,7 +158,7 @@ class ScheduleSendEmailTestMixin(FilteredQueryCountMixin): # lint-amnesty, pyli Updates the schedule config model by making sure the new entry has a later timestamp. """ - later_time = datetime.datetime.now(pytz.UTC) + datetime.timedelta(minutes=1) + later_time = datetime.datetime.now(ZoneInfo("UTC")) + datetime.timedelta(minutes=1) with freeze_time(later_time): ScheduleConfigFactory.create(**schedule_config_kwargs) @@ -167,7 +167,7 @@ class ScheduleSendEmailTestMixin(FilteredQueryCountMixin): # lint-amnesty, pyli def test_handle(self): with patch.object(self.command, 'async_send_task') as mock_send: - test_day = datetime.datetime(2017, 8, 1, tzinfo=pytz.UTC) + test_day = datetime.datetime(2017, 8, 1, tzinfo=ZoneInfo("UTC")) self.command().handle(date='2017-08-01', site_domain_name=self.site_config.site.domain) for offset in self.expected_offsets: @@ -287,7 +287,7 @@ class ScheduleSendEmailTestMixin(FilteredQueryCountMixin): # lint-amnesty, pyli } self._update_schedule_config(schedule_config_kwargs) - current_datetime = datetime.datetime(2017, 8, 1, tzinfo=pytz.UTC) + current_datetime = datetime.datetime(2017, 8, 1, tzinfo=ZoneInfo("UTC")) with patch.object(self.task, 'apply_async') as mock_apply_async: self.task.enqueue(self.site_config.site, current_datetime, 3) diff --git a/openedx/core/djangoapps/schedules/management/commands/tests/test_send_email_base_command.py b/openedx/core/djangoapps/schedules/management/commands/tests/test_send_email_base_command.py index 11b33d5851..eb1f7fe40f 100644 --- a/openedx/core/djangoapps/schedules/management/commands/tests/test_send_email_base_command.py +++ b/openedx/core/djangoapps/schedules/management/commands/tests/test_send_email_base_command.py @@ -8,7 +8,7 @@ from unittest import skipUnless from unittest.mock import DEFAULT, Mock, patch import ddt -import pytz +from zoneinfo import ZoneInfo from django.conf import settings from django.contrib.sites.models import Site @@ -33,7 +33,7 @@ class TestSendEmailBaseCommand(CacheIsolationTestCase): # lint-amnesty, pylint: self.command.handle(site_domain_name=self.site.domain, date='2017-09-29') send_emails.assert_called_once_with( self.site, - datetime.datetime(2017, 9, 29, tzinfo=pytz.UTC), + datetime.datetime(2017, 9, 29, tzinfo=ZoneInfo("UTC")), None, None ) @@ -45,7 +45,7 @@ class TestSendEmailBaseCommand(CacheIsolationTestCase): # lint-amnesty, pylint: for expected_site in expected_sites: send_emails.assert_any_call( expected_site, - datetime.datetime(2017, 9, 29, tzinfo=pytz.UTC), + datetime.datetime(2017, 9, 29, tzinfo=ZoneInfo("UTC")), None, None ) diff --git a/openedx/core/djangoapps/schedules/tests/factories.py b/openedx/core/djangoapps/schedules/tests/factories.py index 882b62fb8b..c3ffcad246 100644 --- a/openedx/core/djangoapps/schedules/tests/factories.py +++ b/openedx/core/djangoapps/schedules/tests/factories.py @@ -4,7 +4,7 @@ Factories for schedules tests import factory -import pytz +from zoneinfo import ZoneInfo from openedx.core.djangoapps.schedules import models from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory @@ -22,8 +22,8 @@ class ScheduleFactory(factory.django.DjangoModelFactory): # lint-amnesty, pylin class Meta: model = models.Schedule - start_date = factory.Faker('future_datetime', tzinfo=pytz.UTC) - upgrade_deadline = factory.Faker('future_datetime', tzinfo=pytz.UTC) + start_date = factory.Faker('future_datetime', tzinfo=ZoneInfo("UTC")) + upgrade_deadline = factory.Faker('future_datetime', tzinfo=ZoneInfo("UTC")) enrollment = factory.SubFactory(CourseEnrollmentFactory) experience = factory.RelatedFactory(ScheduleExperienceFactory, 'schedule') diff --git a/openedx/core/djangoapps/schedules/tests/test_resolvers.py b/openedx/core/djangoapps/schedules/tests/test_resolvers.py index 2c37608e5c..20536abcbd 100644 --- a/openedx/core/djangoapps/schedules/tests/test_resolvers.py +++ b/openedx/core/djangoapps/schedules/tests/test_resolvers.py @@ -8,7 +8,7 @@ from unittest.mock import Mock import crum import ddt -import pytz +from zoneinfo import ZoneInfo from django.test import TestCase from django.test.client import RequestFactory from django.test.utils import override_settings @@ -123,7 +123,7 @@ class TestBinnedSchedulesBaseResolver(SchedulesResolverTestMixin, TestCase): # experiment. Note that the experiment waffle is currently inactive, but they should still be excluded because # they were bucketed at enrollment time. bin_num = BinnedSchedulesBaseResolver.bin_num_for_user_id(user.id) - resolver = BinnedSchedulesBaseResolver(None, self.site, datetime.datetime.now(pytz.UTC), 0, bin_num) + resolver = BinnedSchedulesBaseResolver(None, self.site, datetime.datetime.now(ZoneInfo("UTC")), 0, bin_num) resolver.schedule_date_field = 'created' schedules = resolver.get_schedules_with_target_date_by_bin_and_orgs() @@ -235,7 +235,7 @@ class TestCourseNextSectionUpdateResolver(SchedulesResolverTestMixin, ModuleStor def setUp(self): super().setUp() - self.today = datetime.datetime.utcnow() + self.today = datetime.datetime.now(ZoneInfo("UTC")) self.yesterday = self.today - datetime.timedelta(days=1) self.course = CourseFactory.create( highlights_enabled_for_messaging=True, self_paced=True, diff --git a/openedx/core/djangoapps/schedules/tests/test_signals.py b/openedx/core/djangoapps/schedules/tests/test_signals.py index 023fbbdbaf..b862f8e00e 100644 --- a/openedx/core/djangoapps/schedules/tests/test_signals.py +++ b/openedx/core/djangoapps/schedules/tests/test_signals.py @@ -8,7 +8,7 @@ from unittest.mock import patch import ddt import pytest -from pytz import utc +from zoneinfo import ZoneInfo from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.course_modes.tests.factories import CourseModeFactory @@ -188,7 +188,7 @@ def _create_course_run(self_paced=True, start_day_offset=-1): Both audit and verified `CourseMode` objects will be created for the course run. """ - now = datetime.datetime.now(utc) + now = datetime.datetime.now(ZoneInfo("UTC")) start = now + datetime.timedelta(days=start_day_offset) course = CourseFactory.create(start=start, self_paced=self_paced) diff --git a/openedx/core/djangoapps/schedules/tests/test_utils.py b/openedx/core/djangoapps/schedules/tests/test_utils.py index f1d0cd9fcb..2b48550b42 100644 --- a/openedx/core/djangoapps/schedules/tests/test_utils.py +++ b/openedx/core/djangoapps/schedules/tests/test_utils.py @@ -5,7 +5,7 @@ Tests for schedules utils import datetime import ddt -from pytz import utc +from zoneinfo import ZoneInfo from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory @@ -26,7 +26,7 @@ class ResetSelfPacedScheduleTests(SharedModuleStoreTestCase): # pylint: disable=attribute-defined-outside-init self.config = ScheduleConfigFactory() - start = datetime.datetime.now(utc) + datetime.timedelta(days=course_start_offset) + start = datetime.datetime.now(ZoneInfo("UTC")) + datetime.timedelta(days=course_start_offset) self.course = CourseFactory.create(start=start, self_paced=True) self.enrollment = CourseEnrollmentFactory( diff --git a/openedx/core/djangoapps/schedules/utils.py b/openedx/core/djangoapps/schedules/utils.py index c2565a1c87..344e7566b1 100644 --- a/openedx/core/djangoapps/schedules/utils.py +++ b/openedx/core/djangoapps/schedules/utils.py @@ -3,7 +3,7 @@ import datetime import logging -import pytz +from zoneinfo import ZoneInfo from django.db import transaction from openedx.core.djangoapps.schedules.models import Schedule @@ -59,7 +59,7 @@ def reset_self_paced_schedule(user, course_key, use_enrollment_date=False): if use_enrollment_date: new_start_date = schedule.enrollment.created else: - new_start_date = datetime.datetime.now(pytz.utc) + new_start_date = datetime.datetime.now(ZoneInfo("UTC")) # Make sure we don't start the clock on the learner's schedule before the course even starts new_start_date = max(new_start_date, schedule.enrollment.course.start) diff --git a/openedx/core/djangoapps/user_api/accounts/api.py b/openedx/core/djangoapps/user_api/accounts/api.py index 6970ea6f85..c3fd805a31 100644 --- a/openedx/core/djangoapps/user_api/accounts/api.py +++ b/openedx/core/djangoapps/user_api/accounts/api.py @@ -12,7 +12,7 @@ from django.core.validators import ValidationError, validate_email from django.utils.translation import gettext as _ from django.utils.translation import override as override_language from eventtracking import tracker -from pytz import UTC +from zoneinfo import ZoneInfo from common.djangoapps.student import views as student_views from common.djangoapps.student.models import ( @@ -375,7 +375,7 @@ def _store_old_name_if_needed(old_name, user_profile, requesting_user): meta['old_names'].append([ old_name, f"Name change requested through account API by {requesting_user.username}", - datetime.datetime.now(UTC).isoformat() + datetime.datetime.now(ZoneInfo("UTC")).isoformat() ]) user_profile.set_meta(meta) user_profile.save() diff --git a/openedx/core/djangoapps/user_api/accounts/tests/retirement_helpers.py b/openedx/core/djangoapps/user_api/accounts/tests/retirement_helpers.py index a3c40002f6..d91ba37399 100644 --- a/openedx/core/djangoapps/user_api/accounts/tests/retirement_helpers.py +++ b/openedx/core/djangoapps/user_api/accounts/tests/retirement_helpers.py @@ -6,7 +6,7 @@ Helpers for testing retirement functionality import datetime import pytest -import pytz +from zoneinfo import ZoneInfo from django.test import TestCase from social_django.models import UserSocialAuth @@ -67,7 +67,7 @@ def create_retirement_status(user, state=None, create_datetime=None): Assumes that retirement states have been setup before calling. """ if create_datetime is None: - create_datetime = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=8) + create_datetime = datetime.datetime.now(ZoneInfo("UTC")) - datetime.timedelta(days=8) retirement = UserRetirementStatus.create_retirement(user) if state: diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_api.py b/openedx/core/djangoapps/user_api/accounts/tests/test_api.py index f9071c06a5..8cdb7a6341 100644 --- a/openedx/core/djangoapps/user_api/accounts/tests/test_api.py +++ b/openedx/core/djangoapps/user_api/accounts/tests/test_api.py @@ -17,7 +17,7 @@ from django.http import HttpResponse from django.test import TestCase from django.test.client import RequestFactory from django.urls import reverse -from pytz import UTC +from zoneinfo import ZoneInfo from social_django.models import UserSocialAuth from common.djangoapps.student.models import ( @@ -381,7 +381,7 @@ class TestAccountApi(UserSettingsEventTestMixin, EmailTemplateTagMixin, CreateAc meta['old_names'] = [] for num in range(3): meta['old_names'].append( - [f'old_name_{num}', 'test', datetime.datetime.now(UTC).isoformat()] + [f'old_name_{num}', 'test', datetime.datetime.now(ZoneInfo("UTC")).isoformat()] ) user_profile.set_meta(meta) user_profile.save() diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_image_helpers.py b/openedx/core/djangoapps/user_api/accounts/tests/test_image_helpers.py index 3608073a52..c02069ab7b 100644 --- a/openedx/core/djangoapps/user_api/accounts/tests/test_image_helpers.py +++ b/openedx/core/djangoapps/user_api/accounts/tests/test_image_helpers.py @@ -8,7 +8,7 @@ import hashlib from unittest.mock import patch from django.test import TestCase -from pytz import UTC +from zoneinfo import ZoneInfo from openedx.core.djangolib.testing.utils import skip_unless_lms from common.djangoapps.student.tests.factories import UserFactory @@ -16,7 +16,7 @@ from common.djangoapps.student.tests.factories import UserFactory from ..image_helpers import get_profile_image_urls_for_user TEST_SIZES = {'full': 50, 'small': 10} -TEST_PROFILE_IMAGE_UPLOAD_DT = datetime.datetime(2002, 1, 9, 15, 43, 1, tzinfo=UTC) +TEST_PROFILE_IMAGE_UPLOAD_DT = datetime.datetime(2002, 1, 9, 15, 43, 1, tzinfo=ZoneInfo("UTC")) @patch.dict('django.conf.settings.PROFILE_IMAGE_SIZES_MAP', TEST_SIZES, clear=True) diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_retirement_views.py b/openedx/core/djangoapps/user_api/accounts/tests/test_retirement_views.py index 9d4efb2fa7..4f942417ae 100644 --- a/openedx/core/djangoapps/user_api/accounts/tests/test_retirement_views.py +++ b/openedx/core/djangoapps/user_api/accounts/tests/test_retirement_views.py @@ -7,7 +7,7 @@ import json from unittest import mock import ddt -import pytz +from zoneinfo import ZoneInfo from consent.models import DataSharingConsent from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user from django.contrib.sites.models import Site @@ -516,7 +516,7 @@ class TestPartnerReportingList(ModuleStoreTestCase): self.headers = build_jwt_headers(self.test_superuser) self.url = reverse('accounts_retirement_partner_report') self.maxDiff = None - self.test_created_datetime = datetime.datetime(2018, 1, 1, tzinfo=pytz.UTC) + self.test_created_datetime = datetime.datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC")) ExternalIdType.objects.get_or_create(name=ExternalIdType.CALIPER) def get_user_dict(self, user, enrollments): @@ -769,7 +769,7 @@ class TestAccountRetirementList(RetirementTestCase): # retirements = [2018-04-10..., 2018-04-09..., 2018-04-08...] pending_state = RetirementState.objects.get(state_name='PENDING') for days_back in range(1, days_back_to_test, -1): - create_datetime = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=days_back) + create_datetime = datetime.datetime.now(ZoneInfo("UTC")) - datetime.timedelta(days=days_back) retirements.append(create_retirement_status( UserFactory(), state=pending_state, @@ -927,12 +927,12 @@ class TestAccountRetirementsByStatusAndDate(RetirementTestCase): # Create retirements for the last 10 days for days_back in range(0, 10): # lint-amnesty, pylint: disable=simplifiable-range - create_datetime = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=days_back) + create_datetime = datetime.datetime.now(ZoneInfo("UTC")) - datetime.timedelta(days=days_back) ret = create_retirement_status(UserFactory(), state=complete_state, create_datetime=create_datetime) retirements.append(self._retirement_to_dict(ret)) # Go back in time adding days to the query, assert the correct retirements are present - end_date = datetime.datetime.now(pytz.UTC) + end_date = datetime.datetime.now(ZoneInfo("UTC")) for days_back in range(1, 11): retirement_dicts = retirements[:days_back] start_date = end_date - datetime.timedelta(days=days_back - 1) diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_views.py b/openedx/core/djangoapps/user_api/accounts/tests/test_views.py index 46d6b5232b..45b1773e4f 100644 --- a/openedx/core/djangoapps/user_api/accounts/tests/test_views.py +++ b/openedx/core/djangoapps/user_api/accounts/tests/test_views.py @@ -10,7 +10,7 @@ from unittest import mock from urllib.parse import quote import ddt -import pytz +from zoneinfo import ZoneInfo from django.conf import settings from django.core.files.storage import FileSystemStorage from django.test.testcases import TransactionTestCase @@ -44,7 +44,7 @@ from openedx.features.name_affirmation_api.utils import get_name_affirmation_ser from .. import ALL_USERS_VISIBILITY, CUSTOM_VISIBILITY, PRIVATE_VISIBILITY -TEST_PROFILE_IMAGE_UPLOADED_AT = datetime.datetime(2002, 1, 9, 15, 43, 1, tzinfo=pytz.UTC) +TEST_PROFILE_IMAGE_UPLOADED_AT = datetime.datetime(2002, 1, 9, 15, 43, 1, tzinfo=ZoneInfo("UTC")) # this is used in one test to check the behavior of profile image url # generation with a relative url in the config. @@ -304,7 +304,7 @@ class TestCancelAccountRetirementStatusView(UserAPITestCase): current_state=retirement_state, last_state=retirement_state, original_email=self.user.email, - created=datetime.datetime.now(pytz.UTC) + created=datetime.datetime.now(ZoneInfo("UTC")) ) url = reverse("cancel_account_retirement") response = client.post(url, data={'retirement_id': user_retirement_status.id}) @@ -329,7 +329,7 @@ class TestCancelAccountRetirementStatusView(UserAPITestCase): current_state=retirement_state, last_state=retirement_state, original_email=self.user.email, - created=datetime.datetime.now(pytz.UTC) + created=datetime.datetime.now(ZoneInfo("UTC")) ) user_retirement_status.user.set_unusable_password() assert UserRetirementStatus.objects.count() == 1 @@ -585,8 +585,8 @@ class TestAccountsAPI(FilteredQueryCountMixin, CacheIsolationTestCase, UserAPITe @mock.patch('openedx.core.djangoapps.user_api.accounts.views.is_email_retired') @ddt.data( - (datetime.datetime.now(pytz.UTC), True), - (datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=15), False) + (datetime.datetime.now(ZoneInfo("UTC")), True), + (datetime.datetime.now(ZoneInfo("UTC")) - datetime.timedelta(days=15), False) ) @ddt.unpack def test_search_emails_retired_before_cooloff_period(self, created_date, can_cancel, mock_is_email_retired): diff --git a/openedx/core/djangoapps/user_api/accounts/views.py b/openedx/core/djangoapps/user_api/accounts/views.py index 0464187b5d..c3ff6ce7a2 100644 --- a/openedx/core/djangoapps/user_api/accounts/views.py +++ b/openedx/core/djangoapps/user_api/accounts/views.py @@ -9,7 +9,7 @@ import datetime import logging from functools import wraps -import pytz +from zoneinfo import ZoneInfo from consent.models import DataSharingConsent from django.apps import apps from django.conf import settings @@ -198,11 +198,11 @@ class AccountViewSet(ViewSet): if is_email_retired(user_email): can_cancel_retirement = True retirement_id = None - earliest_datetime = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=settings.COOL_OFF_DAYS) + earliest_datetime = datetime.datetime.now(ZoneInfo("UTC")) - datetime.timedelta(days=settings.COOL_OFF_DAYS) try: retirement_status = UserRetirementStatus.objects.get( created__gt=earliest_datetime, - created__lt=datetime.datetime.now(pytz.UTC), + created__lt=datetime.datetime.now(ZoneInfo("UTC")), original_email=user_email, ) retirement_id = retirement_status.id @@ -891,7 +891,7 @@ class AccountRetirementStatusView(ViewSet): status=status.HTTP_400_BAD_REQUEST, ) - earliest_datetime = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=cool_off_days) + earliest_datetime = datetime.datetime.now(ZoneInfo("UTC")) - datetime.timedelta(days=cool_off_days) retirements = ( UserRetirementStatus.objects.select_related("user", "current_state", "last_state") @@ -921,9 +921,12 @@ class AccountRetirementStatusView(ViewSet): so to get one day you would set both dates to that day. """ try: - start_date = datetime.datetime.strptime(request.GET["start_date"], "%Y-%m-%d").replace(tzinfo=pytz.UTC) - end_date = datetime.datetime.strptime(request.GET["end_date"], "%Y-%m-%d").replace(tzinfo=pytz.UTC) - now = datetime.datetime.now(pytz.UTC) + start_date = ( + datetime.datetime.strptime(request.GET["start_date"], "%Y-%m-%d") + .replace(tzinfo=ZoneInfo("UTC")) + ) + end_date = datetime.datetime.strptime(request.GET["end_date"], "%Y-%m-%d").replace(tzinfo=ZoneInfo("UTC")) + now = datetime.datetime.now(ZoneInfo("UTC")) if start_date > now or end_date > now or start_date > end_date: raise RetirementStateError("Dates must be today or earlier, and start must be earlier than end.") diff --git a/openedx/core/djangoapps/user_api/management/commands/create_user_gdpr_testing.py b/openedx/core/djangoapps/user_api/management/commands/create_user_gdpr_testing.py index 2008ce8652..fd574cb83f 100644 --- a/openedx/core/djangoapps/user_api/management/commands/create_user_gdpr_testing.py +++ b/openedx/core/djangoapps/user_api/management/commands/create_user_gdpr_testing.py @@ -20,7 +20,7 @@ from enterprise.models import ( ) from integrated_channels.sap_success_factors.models import SapSuccessFactorsLearnerDataTransmissionAudit from opaque_keys.edx.keys import CourseKey -from pytz import UTC +from zoneinfo import ZoneInfo from common.djangoapps.entitlements.models import CourseEntitlement, CourseEntitlementSupportDetail from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification @@ -82,7 +82,7 @@ class Command(BaseCommand): user.save() # UserProfile - profile_image_uploaded_date = datetime(2018, 5, 3, tzinfo=UTC) + profile_image_uploaded_date = datetime(2018, 5, 3, tzinfo=ZoneInfo("UTC")) user_profile, __ = UserProfile.objects.get_or_create( user=user ) diff --git a/openedx/core/djangoapps/user_authn/tests/utils.py b/openedx/core/djangoapps/user_authn/tests/utils.py index 09ca85145f..31b4be7c40 100644 --- a/openedx/core/djangoapps/user_authn/tests/utils.py +++ b/openedx/core/djangoapps/user_authn/tests/utils.py @@ -6,7 +6,7 @@ from enum import Enum from unittest.mock import patch import ddt -import pytz +from zoneinfo import ZoneInfo from django.conf import settings from oauth2_provider import models as dot_models from rest_framework import status @@ -42,7 +42,7 @@ def utcnow(): """ Helper function to return the current UTC time localized to the UTC timezone. """ - return datetime.now(pytz.UTC) + return datetime.now(ZoneInfo("UTC")) @ddt.ddt diff --git a/openedx/core/djangoapps/user_authn/views/register.py b/openedx/core/djangoapps/user_authn/views/register.py index aff210e7b2..2fc0818a3a 100644 --- a/openedx/core/djangoapps/user_authn/views/register.py +++ b/openedx/core/djangoapps/user_authn/views/register.py @@ -26,7 +26,7 @@ from edx_django_utils.monitoring import set_custom_attribute from openedx_events.learning.data import UserData, UserPersonalData from openedx_events.learning.signals import STUDENT_REGISTRATION_COMPLETED from openedx_filters.learning.filters import StudentRegistrationRequested -from pytz import UTC +from zoneinfo import ZoneInfo from django_ratelimit.decorators import ratelimit from requests import HTTPError from rest_framework.response import Response @@ -371,7 +371,7 @@ def _track_user_registration(user, profile, params, third_party_provider, regist 'name': profile.name, # Mailchimp requires the age & yearOfBirth to be integers, we send a sane integer default if falsey. 'age': profile.age or -1, - 'yearOfBirth': profile.year_of_birth or datetime.datetime.now(UTC).year, + 'yearOfBirth': profile.year_of_birth or datetime.datetime.now(ZoneInfo("UTC")).year, 'education': profile.level_of_education_display, 'address': profile.mailing_address, 'gender': profile.gender_display, @@ -530,7 +530,9 @@ def _record_utm_registration_attribution(request, user): # We divide by 1000 here because the javascript timestamp generated is in milliseconds not seconds. # PYTHON: time.time() => 1475590280.823698 # JS: new Date().getTime() => 1475590280823 - created_at_datetime = datetime.datetime.fromtimestamp(int(created_at_unixtime) / float(1000), tz=UTC) + created_at_datetime = datetime.datetime.fromtimestamp( + int(created_at_unixtime) / float(1000), tz=ZoneInfo("UTC") + ) UserAttribute.set_user_attribute( user, REGISTRATION_UTM_CREATED_AT, diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_password.py b/openedx/core/djangoapps/user_authn/views/tests/test_password.py index a403298a6e..57a988c0b3 100644 --- a/openedx/core/djangoapps/user_authn/views/tests/test_password.py +++ b/openedx/core/djangoapps/user_authn/views/tests/test_password.py @@ -19,7 +19,7 @@ from django.urls import reverse from freezegun import freeze_time from oauth2_provider.models import AccessToken as dot_access_token from oauth2_provider.models import RefreshToken as dot_refresh_token -from pytz import UTC +from zoneinfo import ZoneInfo from testfixtures import LogCapture from openedx.core.djangoapps.oauth_dispatch.tests import factories as dot_factories @@ -319,7 +319,7 @@ class TestPasswordChange(CreateAccountMixin, CacheIsolationTestCase): # now reset the time to 1 min from now in future and change the email and # verify that it will allow another request from same IP - reset_time = datetime.now(UTC) + timedelta(seconds=61) + reset_time = datetime.now(ZoneInfo("UTC")) + timedelta(seconds=61) with freeze_time(reset_time): response = self._change_password(email=self.OLD_EMAIL) assert response.status_code == 200 diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_register.py b/openedx/core/djangoapps/user_authn/views/tests/test_register.py index 54d42efa55..e79b8ae6da 100644 --- a/openedx/core/djangoapps/user_authn/views/tests/test_register.py +++ b/openedx/core/djangoapps/user_authn/views/tests/test_register.py @@ -16,7 +16,7 @@ from django.test.client import RequestFactory from django.test.utils import override_settings from django.urls import reverse from openedx_events.tests.utils import OpenEdxEventsTestMixin -from pytz import UTC +from zoneinfo import ZoneInfo from social_django.models import Partial, UserSocialAuth from testfixtures import LogCapture @@ -949,7 +949,7 @@ class RegistrationViewTestV1( ) def test_register_form_year_of_birth(self): - this_year = datetime.now(UTC).year + this_year = datetime.now(ZoneInfo("UTC")).year year_options = ( [ { diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_reset_password.py b/openedx/core/djangoapps/user_authn/views/tests/test_reset_password.py index b89b458ed1..aed040a8a2 100644 --- a/openedx/core/djangoapps/user_authn/views/tests/test_reset_password.py +++ b/openedx/core/djangoapps/user_authn/views/tests/test_reset_password.py @@ -24,7 +24,7 @@ from django.urls import reverse from django.utils.http import int_to_base36 from freezegun import freeze_time from oauth2_provider import models as dot_models -from pytz import UTC +from zoneinfo import ZoneInfo from openedx.core.djangoapps.oauth_dispatch.tests import factories as dot_factories from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers @@ -267,7 +267,7 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase): self.request_password_reset(200) # now reset the time to 1 min from now in future and change the email and # verify that it will allow another request from same IP - reset_time = datetime.now(UTC) + timedelta(seconds=61) + reset_time = datetime.now(ZoneInfo("UTC")) + timedelta(seconds=61) with freeze_time(reset_time): for status in [200, 403]: self.request_password_reset(status) diff --git a/openedx/core/djangoapps/util/testing.py b/openedx/core/djangoapps/util/testing.py index 040a2b5af1..4686bdc6e7 100644 --- a/openedx/core/djangoapps/util/testing.py +++ b/openedx/core/djangoapps/util/testing.py @@ -3,7 +3,7 @@ from datetime import datetime -from pytz import UTC +from zoneinfo import ZoneInfo from openedx.core.djangoapps.course_groups.models import CourseUserGroupPartitionGroup from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory @@ -32,7 +32,7 @@ class ContentGroupTestCase(ModuleStoreTestCase): # This test needs to use a course that has already started -- # discussion topics only show up if the course has already started, # and the default start date for courses is Jan 1, 2030. - start=datetime(2012, 2, 3, tzinfo=UTC), + start=datetime(2012, 2, 3, tzinfo=ZoneInfo("UTC")), user_partitions=[ UserPartition( 0, diff --git a/openedx/core/djangoapps/verified_track_content/tests/test_partition_scheme.py b/openedx/core/djangoapps/verified_track_content/tests/test_partition_scheme.py index a2667bfa1c..f7de6042ef 100644 --- a/openedx/core/djangoapps/verified_track_content/tests/test_partition_scheme.py +++ b/openedx/core/djangoapps/verified_track_content/tests/test_partition_scheme.py @@ -5,7 +5,7 @@ Tests for verified_track_content/partition_scheme.py. from datetime import datetime, timedelta -import pytz +from zoneinfo import ZoneInfo import pytest from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.student.models import CourseEnrollment @@ -38,7 +38,7 @@ class EnrollmentTrackUserPartitionTest(SharedModuleStoreTestCase): # Note that the verified mode is expired-- this is intentional. create_mode( self.course, CourseMode.VERIFIED, "Verified Enrollment Track", min_price=1, - expiration_datetime=datetime.now(pytz.UTC) + timedelta(days=-1) + expiration_datetime=datetime.now(ZoneInfo("UTC")) + timedelta(days=-1) ) # Note that the credit mode is not selectable-- this is intentional so we # can test that it is filtered out. @@ -128,7 +128,7 @@ class EnrollmentTrackPartitionSchemeTest(SharedModuleStoreTestCase): def test_enrolled_in_expired(self): create_mode( self.course, CourseMode.VERIFIED, "Verified Enrollment Track", - min_price=1, expiration_datetime=datetime.now(pytz.UTC) + timedelta(days=-1) + min_price=1, expiration_datetime=datetime.now(ZoneInfo("UTC")) + timedelta(days=-1) ) CourseEnrollment.enroll(self.student, self.course.id, mode=CourseMode.VERIFIED) assert 'Verified Enrollment Track' == self._get_user_group().name @@ -153,7 +153,7 @@ class EnrollmentTrackPartitionSchemeTest(SharedModuleStoreTestCase): # the upgrade deadline has passed (see EDUCATOR-1511 for why this matters). create_mode( self.course, CourseMode.VERIFIED, "Verified Enrollment Track", min_price=1, - expiration_datetime=datetime.now(pytz.UTC) + timedelta(days=-1) + expiration_datetime=datetime.now(ZoneInfo("UTC")) + timedelta(days=-1) ) assert 'Verified Enrollment Track' == self._get_user_group().name diff --git a/openedx/core/lib/xblock_utils/__init__.py b/openedx/core/lib/xblock_utils/__init__.py index a8b76541b6..d398a159f9 100644 --- a/openedx/core/lib/xblock_utils/__init__.py +++ b/openedx/core/lib/xblock_utils/__init__.py @@ -19,7 +19,7 @@ from django.utils.html import escape from edx_django_utils.plugins import pluggable_override from lxml import etree, html from opaque_keys.edx.asides import AsideUsageKeyV1, AsideUsageKeyV2 -from pytz import UTC +from zoneinfo import ZoneInfo from web_fragments.fragment import Fragment from xblock.core import XBlock from xblock.exceptions import InvalidScopeError @@ -310,7 +310,7 @@ def add_staff_markup(user, disable_staff_debug_info, block, view, frag, context) # Useful to indicate to staff if problem has been released or not. # TODO (ichuang): use _has_access_block.can_load in lms.courseware.access, # instead of now>mstart comparison here. - now = datetime.datetime.now(UTC) + now = datetime.datetime.now(ZoneInfo("UTC")) is_released = "unknown" mstart = block.start diff --git a/openedx/features/calendar_sync/ics.py b/openedx/features/calendar_sync/ics.py index fc465443e7..c6d3a03a1c 100644 --- a/openedx/features/calendar_sync/ics.py +++ b/openedx/features/calendar_sync/ics.py @@ -2,7 +2,7 @@ from datetime import datetime, timedelta -import pytz +from zoneinfo import ZoneInfo from django.conf import settings from django.utils.translation import gettext as _ from icalendar import Calendar, Event, vCalAddress, vText @@ -59,7 +59,7 @@ def generate_ics_files_for_user_course(course, user, user_calendar_sync_config_i assignments = get_course_assignments(course.id, user) platform_name = get_value('platform_name', settings.PLATFORM_NAME) platform_email = get_value('email_from_address', settings.DEFAULT_FROM_EMAIL) - now = datetime.now(pytz.utc) + now = datetime.now(ZoneInfo("UTC")) site_config = SiteConfiguration.get_configuration_for_org(course.org) ics_files = {} diff --git a/openedx/features/calendar_sync/tests/test_ics.py b/openedx/features/calendar_sync/tests/test_ics.py index 0230107928..73327b09a9 100644 --- a/openedx/features/calendar_sync/tests/test_ics.py +++ b/openedx/features/calendar_sync/tests/test_ics.py @@ -3,7 +3,7 @@ from datetime import datetime, timedelta from unittest.mock import patch -import pytz +from zoneinfo import ZoneInfo from django.test import RequestFactory, TestCase from freezegun import freeze_time @@ -21,7 +21,7 @@ class TestIcsGeneration(TestCase): def setUp(self): super().setUp() - freezer = freeze_time(datetime(2013, 10, 3, 8, 24, 55, tzinfo=pytz.utc)) + freezer = freeze_time(datetime(2013, 10, 3, 8, 24, 55, tzinfo=ZoneInfo("UTC"))) self.addCleanup(freezer.stop) freezer.start() @@ -103,7 +103,7 @@ END:VCALENDAR def test_generate_ics_for_user_course(self): """ Tests that a simple sample set of course assignments is generated correctly """ - now = datetime.now(pytz.utc) + now = datetime.now(ZoneInfo("UTC")) day1 = now + timedelta(1) day2 = now + timedelta(1) diff --git a/openedx/features/content_type_gating/partitions.py b/openedx/features/content_type_gating/partitions.py index 61851ec43b..a5e833f60b 100644 --- a/openedx/features/content_type_gating/partitions.py +++ b/openedx/features/content_type_gating/partitions.py @@ -10,7 +10,7 @@ import datetime import logging import crum -import pytz +from zoneinfo import ZoneInfo from django.template.loader import render_to_string from django.utils.translation import gettext_lazy as _ from web_fragments.fragment import Fragment @@ -88,7 +88,7 @@ class ContentTypeGatingPartition(UserPartition): return None expiration_datetime = verified_mode.expiration_datetime - if expiration_datetime and expiration_datetime < datetime.datetime.now(pytz.UTC): + if expiration_datetime and expiration_datetime < datetime.datetime.now(ZoneInfo("UTC")): ecommerce_checkout_link = None else: ecommerce_checkout_link = self._get_checkout_link(user, verified_mode.sku, str(course_key)) diff --git a/openedx/features/content_type_gating/tests/test_models.py b/openedx/features/content_type_gating/tests/test_models.py index 673ce805a7..adb9bd6737 100644 --- a/openedx/features/content_type_gating/tests/test_models.py +++ b/openedx/features/content_type_gating/tests/test_models.py @@ -6,7 +6,7 @@ import itertools # lint-amnesty, pylint: disable=wrong-import-order from datetime import datetime, timedelta # lint-amnesty, pylint: disable=wrong-import-order import ddt -import pytz +from zoneinfo import ZoneInfo from django.utils import timezone from edx_django_utils.cache import RequestCache from unittest.mock import Mock # lint-amnesty, pylint: disable=wrong-import-order @@ -217,17 +217,17 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase): # pylint: disable=mi # Point-test some of the final configurations assert all_configs[CourseLocator('7-True', 'test_course', 'run-None')] == { 'enabled': (True, Provenance.org), - 'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=pytz.UTC), Provenance.run), + 'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=ZoneInfo("UTC")), Provenance.run), 'studio_override_enabled': (None, Provenance.default) } assert all_configs[CourseLocator('7-True', 'test_course', 'run-False')] == { 'enabled': (False, Provenance.run), - 'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=pytz.UTC), Provenance.run), + 'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=ZoneInfo("UTC")), Provenance.run), 'studio_override_enabled': (None, Provenance.default) } assert all_configs[CourseLocator('7-None', 'test_course', 'run-None')] == { 'enabled': (True, Provenance.site), - 'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=pytz.UTC), Provenance.run), + 'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=ZoneInfo("UTC")), Provenance.run), 'studio_override_enabled': (None, Provenance.default) } diff --git a/openedx/features/course_duration_limits/tests/test_access.py b/openedx/features/course_duration_limits/tests/test_access.py index 558afec22a..f948324b48 100644 --- a/openedx/features/course_duration_limits/tests/test_access.py +++ b/openedx/features/course_duration_limits/tests/test_access.py @@ -8,7 +8,7 @@ import ddt from crum import set_current_request from django.test import RequestFactory from django.utils import timezone -from pytz import UTC +from zoneinfo import ZoneInfo from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from common.djangoapps.course_modes.models import CourseMode @@ -34,9 +34,12 @@ class TestAccess(ModuleStoreTestCase): def setUp(self): super().setUp() # lint-amnesty, pylint: disable=super-with-arguments - CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=UTC)) + CourseDurationLimitConfig.objects.create( + enabled=True, + enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC")) + ) DynamicUpgradeDeadlineConfiguration.objects.create(enabled=True) - self.course = CourseOverviewFactory.create(start=datetime(2018, 1, 1, tzinfo=UTC), self_paced=True) + self.course = CourseOverviewFactory.create(start=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC")), self_paced=True) def assertDateInMessage(self, date, message): # lint-amnesty, pylint: disable=missing-function-docstring # First, check that the formatted version is in there @@ -148,7 +151,7 @@ class TestAccess(ModuleStoreTestCase): course_id=enrollment.course.id, mode_slug=CourseMode.AUDIT, ) - Schedule.objects.update(start_date=datetime(2017, 1, 1, tzinfo=UTC)) + Schedule.objects.update(start_date=datetime(2017, 1, 1, tzinfo=ZoneInfo("UTC"))) content_availability_date = max(enrollment.created, enrollment.course.start) access_duration = get_user_course_duration(enrollment.user, enrollment.course) diff --git a/openedx/features/course_duration_limits/tests/test_models.py b/openedx/features/course_duration_limits/tests/test_models.py index 0473faefd3..f596f35344 100644 --- a/openedx/features/course_duration_limits/tests/test_models.py +++ b/openedx/features/course_duration_limits/tests/test_models.py @@ -8,7 +8,7 @@ from unittest.mock import Mock import ddt import pytest -import pytz +from zoneinfo import ZoneInfo from django.utils import timezone from edx_django_utils.cache import RequestCache from opaque_keys.edx.locator import CourseLocator @@ -178,13 +178,18 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase): def test_all_current_course_configs(self): # Set up test objects for global_setting in (True, False, None): - CourseDurationLimitConfig.objects.create(enabled=global_setting, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC)) # lint-amnesty, pylint: disable=line-too-long + CourseDurationLimitConfig.objects.create( + enabled=global_setting, + enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC")) + ) for site_setting in (True, False, None): test_site_cfg = SiteConfigurationFactory.create( site_values={'course_org_filter': []} ) CourseDurationLimitConfig.objects.create( - site=test_site_cfg.site, enabled=site_setting, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC) + site=test_site_cfg.site, + enabled=site_setting, + enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC")) ) for org_setting in (True, False, None): @@ -193,7 +198,7 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase): test_site_cfg.save() CourseDurationLimitConfig.objects.create( - org=test_org, enabled=org_setting, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC) + org=test_org, enabled=org_setting, enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC")) ) for course_setting in (True, False, None): @@ -202,7 +207,7 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase): id=CourseLocator(test_org, 'test_course', f'run-{course_setting}') ) CourseDurationLimitConfig.objects.create( - course=test_course, enabled=course_setting, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC) # lint-amnesty, pylint: disable=line-too-long + course=test_course, enabled=course_setting, enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC")) # lint-amnesty, pylint: disable=line-too-long ) with self.assertNumQueries(4): @@ -216,22 +221,25 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase): # Point-test some of the final configurations assert all_configs[CourseLocator('7-True', 'test_course', 'run-None')] == { 'enabled': (True, Provenance.org), - 'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=pytz.UTC), + 'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=ZoneInfo("UTC")), Provenance.run) } assert all_configs[CourseLocator('7-True', 'test_course', 'run-False')] == { 'enabled': (False, Provenance.run), - 'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=pytz.UTC), + 'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=ZoneInfo("UTC")), Provenance.run) } assert all_configs[CourseLocator('7-None', 'test_course', 'run-None')] == { 'enabled': (True, Provenance.site), - 'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=pytz.UTC), + 'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=ZoneInfo("UTC")), Provenance.run) } def test_caching_global(self): - global_config = CourseDurationLimitConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC)) + global_config = CourseDurationLimitConfig( + enabled=True, + enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC")) + ) global_config.save() RequestCache.clear_all_namespaces() @@ -257,7 +265,7 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase): def test_caching_site(self): site_cfg = SiteConfigurationFactory() - site_config = CourseDurationLimitConfig(site=site_cfg.site, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC)) # lint-amnesty, pylint: disable=line-too-long + site_config = CourseDurationLimitConfig(site=site_cfg.site, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))) # lint-amnesty, pylint: disable=line-too-long site_config.save() RequestCache.clear_all_namespaces() @@ -281,7 +289,10 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase): with self.assertNumQueries(1): assert not CourseDurationLimitConfig.current(site=site_cfg.site).enabled - global_config = CourseDurationLimitConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC)) + global_config = CourseDurationLimitConfig( + enabled=True, + enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC")) + ) global_config.save() RequestCache.clear_all_namespaces() @@ -295,7 +306,7 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase): site_cfg = SiteConfigurationFactory.create( site_values={'course_org_filter': course.org} ) - org_config = CourseDurationLimitConfig(org=course.org, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC)) # lint-amnesty, pylint: disable=line-too-long + org_config = CourseDurationLimitConfig(org=course.org, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))) # lint-amnesty, pylint: disable=line-too-long org_config.save() RequestCache.clear_all_namespaces() @@ -319,7 +330,10 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase): with self.assertNumQueries(2): assert not CourseDurationLimitConfig.current(org=course.org).enabled - global_config = CourseDurationLimitConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC)) + global_config = CourseDurationLimitConfig( + enabled=True, + enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC")) + ) global_config.save() RequestCache.clear_all_namespaces() @@ -328,7 +342,7 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase): with self.assertNumQueries(0): assert not CourseDurationLimitConfig.current(org=course.org).enabled - site_config = CourseDurationLimitConfig(site=site_cfg.site, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC)) # lint-amnesty, pylint: disable=line-too-long + site_config = CourseDurationLimitConfig(site=site_cfg.site, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))) # lint-amnesty, pylint: disable=line-too-long site_config.save() RequestCache.clear_all_namespaces() @@ -342,7 +356,7 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase): site_cfg = SiteConfigurationFactory.create( site_values={'course_org_filter': course.org} ) - course_config = CourseDurationLimitConfig(course=course, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC)) # lint-amnesty, pylint: disable=line-too-long + course_config = CourseDurationLimitConfig(course=course, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))) # lint-amnesty, pylint: disable=line-too-long course_config.save() RequestCache.clear_all_namespaces() @@ -366,7 +380,10 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase): with self.assertNumQueries(2): assert not CourseDurationLimitConfig.current(course_key=course.id).enabled - global_config = CourseDurationLimitConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC)) + global_config = CourseDurationLimitConfig( + enabled=True, + enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC")) + ) global_config.save() RequestCache.clear_all_namespaces() @@ -375,7 +392,7 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase): with self.assertNumQueries(0): assert not CourseDurationLimitConfig.current(course_key=course.id).enabled - site_config = CourseDurationLimitConfig(site=site_cfg.site, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC)) # lint-amnesty, pylint: disable=line-too-long + site_config = CourseDurationLimitConfig(site=site_cfg.site, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))) # lint-amnesty, pylint: disable=line-too-long site_config.save() RequestCache.clear_all_namespaces() @@ -384,7 +401,7 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase): with self.assertNumQueries(0): assert not CourseDurationLimitConfig.current(course_key=course.id).enabled - org_config = CourseDurationLimitConfig(org=course.org, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC)) # lint-amnesty, pylint: disable=line-too-long + org_config = CourseDurationLimitConfig(org=course.org, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))) # lint-amnesty, pylint: disable=line-too-long org_config.save() RequestCache.clear_all_namespaces() diff --git a/openedx/features/course_experience/tests/views/test_course_updates.py b/openedx/features/course_experience/tests/views/test_course_updates.py index 379be52ed4..6ca701f146 100644 --- a/openedx/features/course_experience/tests/views/test_course_updates.py +++ b/openedx/features/course_experience/tests/views/test_course_updates.py @@ -5,7 +5,7 @@ Tests for the course updates page. from datetime import datetime from django.urls import reverse -from pytz import UTC +from zoneinfo import ZoneInfo from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES from openedx.features.content_type_gating.models import ContentTypeGatingConfig @@ -41,7 +41,7 @@ class TestCourseUpdatesPage(BaseCourseUpdatesTestCase): self.assertContains(response, 'Second Message') def test_queries(self): - ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=UTC)) + ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))) self.create_course_update('First Message') # Pre-fetch the view to populate any caches diff --git a/openedx/features/discounts/applicability.py b/openedx/features/discounts/applicability.py index 97d6f74403..c930ae2da8 100644 --- a/openedx/features/discounts/applicability.py +++ b/openedx/features/discounts/applicability.py @@ -11,7 +11,7 @@ not other discounts like coupons or enterprise/program offers configured in ecom from datetime import datetime, timedelta -import pytz +from zoneinfo import ZoneInfo from crum import get_current_request, impersonate from django.conf import settings from django.utils import timezone @@ -197,7 +197,7 @@ def _is_in_holdback_and_bucket(user): Return whether the specified user is in the first-purchase-discount holdback group. This will also stable bucket the user. """ - if datetime(2020, 8, 1, tzinfo=pytz.UTC) <= datetime.now(tz=pytz.UTC): + if datetime(2020, 8, 1, tzinfo=ZoneInfo("UTC")) <= datetime.now(tz=ZoneInfo("UTC")): return False # Holdback is 10% diff --git a/openedx/features/discounts/tests/test_applicability.py b/openedx/features/discounts/tests/test_applicability.py index 60dbe7a67e..472120b387 100644 --- a/openedx/features/discounts/tests/test_applicability.py +++ b/openedx/features/discounts/tests/test_applicability.py @@ -6,7 +6,7 @@ from unittest.mock import Mock, patch import ddt import pytest -import pytz +from zoneinfo import ZoneInfo from django.contrib.sites.models import Site from django.utils.timezone import now from edx_toggles.toggles.testutils import override_waffle_flag @@ -39,7 +39,7 @@ class TestApplicability(ModuleStoreTestCase): self.user = UserFactory.create() self.course = CourseFactory.create(run='test', display_name='test') CourseModeFactory.create(course_id=self.course.id, mode_slug='verified') - now_time = datetime.now(tz=pytz.UTC).strftime("%Y-%m-%d %H:%M:%S%z") + now_time = datetime.now(tz=ZoneInfo("UTC")).strftime("%Y-%m-%d %H:%M:%S%z") ExperimentData.objects.create( user=self.user, experiment_id=REV1008_EXPERIMENT_ID, key=str(self.course.id), value=now_time ) @@ -175,6 +175,6 @@ class TestApplicability(ModuleStoreTestCase): with patch('openedx.features.discounts.applicability.stable_bucketing_hash_group', return_value=0): with patch( 'openedx.features.discounts.applicability.datetime', - Mock(now=Mock(return_value=datetime(2020, 8, 1, 0, 1, tzinfo=pytz.UTC)), wraps=datetime), + Mock(now=Mock(return_value=datetime(2020, 8, 1, 0, 1, tzinfo=ZoneInfo("UTC"))), wraps=datetime), ): assert not _is_in_holdback_and_bucket(self.user) diff --git a/openedx/features/discounts/utils.py b/openedx/features/discounts/utils.py index 92490f19bc..00b60f6cad 100644 --- a/openedx/features/discounts/utils.py +++ b/openedx/features/discounts/utils.py @@ -4,7 +4,7 @@ Utility functions for working with discounts and discounted pricing. from datetime import datetime -import pytz +from zoneinfo import ZoneInfo from django.conf import settings from django.utils.translation import get_language from django.utils.translation import gettext as _ @@ -89,7 +89,7 @@ def generate_offer_data(user, course): ExperimentData.objects.get_or_create( user=user, experiment_id=REV1008_EXPERIMENT_ID, key=str(course), defaults={ - 'value': datetime.now(tz=pytz.UTC).strftime('%Y-%m-%d %H:%M:%S%z'), + 'value': datetime.now(tz=ZoneInfo("UTC")).strftime('%Y-%m-%d %H:%M:%S%z'), }, ) diff --git a/openedx/tests/completion_integration/test_handlers.py b/openedx/tests/completion_integration/test_handlers.py index 677a751462..4522768739 100644 --- a/openedx/tests/completion_integration/test_handlers.py +++ b/openedx/tests/completion_integration/test_handlers.py @@ -11,7 +11,7 @@ from completion import handlers from completion.models import BlockCompletion from completion.test_utils import CompletionSetUpMixin from django.test import TestCase -from pytz import utc +from zoneinfo import ZoneInfo from xblock.completable import XBlockCompletionMode from xblock.core import XBlock @@ -66,7 +66,7 @@ class ScorableCompletionHandlerTestCase(CompletionSetUpMixin, TestCase): usage_id=str(block_key), weighted_earned=0.0, weighted_possible=3.0, - modified=datetime.utcnow().replace(tzinfo=utc), + modified=datetime.utcnow().replace(tzinfo=ZoneInfo("UTC")), score_db_table='submissions', **params ) @@ -127,7 +127,7 @@ class ScorableCompletionHandlerTestCase(CompletionSetUpMixin, TestCase): usage_id=str(self.block_key), weighted_earned=0.0, weighted_possible=3.0, - modified=datetime.utcnow().replace(tzinfo=utc), + modified=datetime.utcnow().replace(tzinfo=ZoneInfo("UTC")), score_db_table='submissions', ) mock_handler.assert_called() @@ -153,7 +153,7 @@ class DisabledCompletionHandlerTestCase(CompletionSetUpMixin, TestCase): usage_id=str(self.block_key), weighted_earned=0.0, weighted_possible=3.0, - modified=datetime.utcnow().replace(tzinfo=utc), + modified=datetime.utcnow().replace(tzinfo=ZoneInfo("UTC")), score_db_table='submissions', ) with pytest.raises(BlockCompletion.DoesNotExist): diff --git a/openedx/tests/xblock_integration/xblock_testcase.py b/openedx/tests/xblock_integration/xblock_testcase.py index 6f598a342c..af2a3410b7 100644 --- a/openedx/tests/xblock_integration/xblock_testcase.py +++ b/openedx/tests/xblock_integration/xblock_testcase.py @@ -44,7 +44,7 @@ from datetime import datetime, timedelta import html from unittest import mock -import pytz +from zoneinfo import ZoneInfo from bs4 import BeautifulSoup from django.conf import settings from django.urls import reverse @@ -199,7 +199,7 @@ class GradePublishTestMixin: 'score': score, 'max_score': max_score}) # Shim a return time, defaults to 1 hour before now - return datetime.now().replace(tzinfo=pytz.UTC) - timedelta(hours=1) + return datetime.now().replace(tzinfo=ZoneInfo("UTC")) - timedelta(hours=1) self.scores = [] patcher = mock.patch("lms.djangoapps.grades.signals.handlers.set_score", capture_score)