Files
Tarun Tak 18d5abb2f6 chore: Replace pytz with zoneinfo for UTC handling - Part 1 (#37523)
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
2025-10-28 16:23:22 -04:00

163 lines
6.6 KiB
Python

"""Tests of openedx.features.course_duration_limits.access"""
import itertools
from datetime import datetime, timedelta
import ddt
from crum import set_current_request
from django.test import RequestFactory
from django.utils import timezone
from zoneinfo import ZoneInfo
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
from common.djangoapps.util.date_utils import strftime_localized
from lms.djangoapps.courseware.models import DynamicUpgradeDeadlineConfiguration
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from openedx.core.djangoapps.schedules.models import Schedule
from openedx.core.djangoapps.user_api.preferences.api import set_user_preference
from openedx.features.course_duration_limits.access import (
generate_course_expired_message,
get_access_expiration_data,
get_user_course_duration,
get_user_course_expiration_date
)
from openedx.features.course_duration_limits.models import CourseDurationLimitConfig
@ddt.ddt
class TestAccess(ModuleStoreTestCase):
"""Tests of openedx.features.course_duration_limits.access"""
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=ZoneInfo("UTC"))
)
DynamicUpgradeDeadlineConfiguration.objects.create(enabled=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
assert strftime_localized(date, 'SHORT_DATE') in message
# But also that the machine-readable version is in there
assert 'data-datetime="%s"' % date.isoformat() in message
def test_get_access_expiration_data(self):
enrollment = CourseEnrollmentFactory()
overview = enrollment.course
user = enrollment.user
now = timezone.now()
upgrade_deadline = now + timedelta(days=2)
CourseModeFactory(
course_id=enrollment.course.id,
mode_slug=CourseMode.VERIFIED,
expiration_datetime=upgrade_deadline,
)
CourseModeFactory(
course_id=enrollment.course.id,
mode_slug=CourseMode.AUDIT,
)
expiration_date = get_user_course_expiration_date(user, overview)
assert expiration_date is not None
data = get_access_expiration_data(user, overview)
assert data == \
{
'expiration_date': expiration_date,
'masquerading_expired_course': False,
'upgrade_deadline': upgrade_deadline,
'upgrade_url': '/dashboard'
}
@ddt.data(
*itertools.product(
itertools.product([None, -2, -1, 1, 2], repeat=2),
)
)
@ddt.unpack
def test_generate_course_expired_message(self, offsets):
now = timezone.now()
schedule_offset, course_offset = offsets
# Set a timezone and request, to test that the message looks at the user's setting
request = RequestFactory().get('/')
request.user = UserFactory()
set_current_request(request)
self.addCleanup(set_current_request, None)
set_user_preference(request.user, 'time_zone', 'Asia/Tokyo')
if schedule_offset is not None:
schedule_upgrade_deadline = now + timedelta(days=schedule_offset)
else:
schedule_upgrade_deadline = None
if course_offset is not None:
course_upgrade_deadline = now + timedelta(days=course_offset)
else:
course_upgrade_deadline = None
enrollment = CourseEnrollmentFactory.create(course=self.course)
CourseModeFactory.create(
course_id=enrollment.course.id,
mode_slug=CourseMode.VERIFIED,
expiration_datetime=course_upgrade_deadline,
)
CourseModeFactory.create(
course_id=enrollment.course.id,
mode_slug=CourseMode.AUDIT,
)
Schedule.objects.update(upgrade_deadline=schedule_upgrade_deadline)
duration_limit_upgrade_deadline = get_user_course_expiration_date(enrollment.user, enrollment.course)
assert duration_limit_upgrade_deadline is not None
message = generate_course_expired_message(enrollment.user, enrollment.course)
self.assertDateInMessage(duration_limit_upgrade_deadline, message)
assert 'data-timezone="Asia/Tokyo"' in message
soft_upgradeable = schedule_upgrade_deadline is not None and now < schedule_upgrade_deadline
upgradeable = course_upgrade_deadline is None or now < course_upgrade_deadline
has_upgrade_deadline = course_upgrade_deadline is not None
if upgradeable and soft_upgradeable:
self.assertDateInMessage(schedule_upgrade_deadline, message)
elif upgradeable and has_upgrade_deadline:
self.assertDateInMessage(course_upgrade_deadline, message)
else:
assert 'Upgrade by' not in message
def test_schedule_start_date_in_past(self):
"""
Test that when schedule start date is before course start or
enrollment date, content_availability_date is set to max of course start
or enrollment date
"""
enrollment = CourseEnrollmentFactory.create(course=self.course)
CourseModeFactory.create(
course_id=enrollment.course.id,
mode_slug=CourseMode.VERIFIED,
)
CourseModeFactory.create(
course_id=enrollment.course.id,
mode_slug=CourseMode.AUDIT,
)
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)
expected_course_expiration_date = content_availability_date + access_duration
duration_limit_upgrade_deadline = get_user_course_expiration_date(enrollment.user, enrollment.course)
assert duration_limit_upgrade_deadline is not None
assert duration_limit_upgrade_deadline == expected_course_expiration_date