Merge pull request #29842 from openedx/mikix/respect-course-start-when-resetting-schedule

fix: avoid resetting a learner schedule to before a course starts
This commit is contained in:
Michael Terry
2022-01-28 15:13:46 -05:00
committed by GitHub
5 changed files with 43 additions and 28 deletions

View File

@@ -82,8 +82,8 @@ def reset_schedule_on_mode_change(sender, user, course_key, mode, **kwargs): #
# If switching to audit, reset to when the user got access to course. This is for the case where a user
# upgrades to verified (resetting their date), then later refunds the order and goes back to audit. We want
# to make sure that audit users go back to their normal audit schedule access.
use_availability_date = mode in CourseMode.AUDIT_MODES
reset_self_paced_schedule(user, course_key, use_availability_date=use_availability_date)
use_enrollment_date = mode in CourseMode.AUDIT_MODES
reset_self_paced_schedule(user, course_key, use_enrollment_date=use_enrollment_date)
def _calculate_upgrade_deadline(course_id, content_availability_date): # lint-amnesty, pylint: disable=missing-function-docstring

View File

@@ -6,6 +6,8 @@ import datetime
import ddt
from pytz import utc
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from common.djangoapps.course_modes.models import CourseMode
from openedx.core.djangoapps.schedules.models import Schedule
@@ -13,38 +15,39 @@ from openedx.core.djangoapps.schedules.tests.factories import ScheduleConfigFact
from openedx.core.djangoapps.schedules.utils import reset_self_paced_schedule
from openedx.core.djangolib.testing.utils import skip_unless_lms
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order
@ddt.ddt
@skip_unless_lms
class ResetSelfPacedScheduleTests(SharedModuleStoreTestCase): # lint-amnesty, pylint: disable=missing-class-docstring
def create_schedule(self, offset=0): # lint-amnesty, pylint: disable=missing-function-docstring
self.config = ScheduleConfigFactory() # lint-amnesty, pylint: disable=attribute-defined-outside-init
class ResetSelfPacedScheduleTests(SharedModuleStoreTestCase):
"""Tests for reset_self_paced_schedule"""
def create_schedule(self, enrollment_offset=0, course_start_offset=-100):
"""Makes a course, schedule, and enrollment ready to test"""
# pylint: disable=attribute-defined-outside-init
self.config = ScheduleConfigFactory()
start = datetime.datetime.now(utc) - datetime.timedelta(days=100)
self.course = CourseFactory.create(start=start, self_paced=True) # lint-amnesty, pylint: disable=attribute-defined-outside-init
start = datetime.datetime.now(utc) + datetime.timedelta(days=course_start_offset)
self.course = CourseFactory.create(start=start, self_paced=True)
self.enrollment = CourseEnrollmentFactory( # lint-amnesty, pylint: disable=attribute-defined-outside-init
self.enrollment = CourseEnrollmentFactory(
course_id=self.course.id,
mode=CourseMode.AUDIT,
)
self.enrollment.created = start + datetime.timedelta(days=offset)
self.enrollment.created = start + datetime.timedelta(days=enrollment_offset)
self.enrollment.save()
self.schedule = self.enrollment.schedule # lint-amnesty, pylint: disable=attribute-defined-outside-init
self.schedule = self.enrollment.schedule
self.schedule.start_date = self.enrollment.created
self.schedule.save()
self.user = self.enrollment.user # lint-amnesty, pylint: disable=attribute-defined-outside-init
self.user = self.enrollment.user
def test_reset_to_now(self):
self.create_schedule()
original_start = self.schedule.start_date
with self.assertNumQueries(3):
reset_self_paced_schedule(self.user, self.course.id, use_availability_date=False)
reset_self_paced_schedule(self.user, self.course.id, use_enrollment_date=False)
self.schedule.refresh_from_db()
assert self.schedule.start_date > original_start
@@ -55,11 +58,11 @@ class ResetSelfPacedScheduleTests(SharedModuleStoreTestCase): # lint-amnesty, p
)
@ddt.unpack
def test_reset_to_start_date(self, offset, expected_offset):
self.create_schedule(offset=offset)
self.create_schedule(enrollment_offset=offset)
expected_start = self.course.start + datetime.timedelta(days=expected_offset)
with self.assertNumQueries(3):
reset_self_paced_schedule(self.user, self.course.id, use_availability_date=True)
reset_self_paced_schedule(self.user, self.course.id, use_enrollment_date=True)
self.schedule.refresh_from_db()
assert self.schedule.start_date.replace(microsecond=0) == expected_start.replace(microsecond=0)
@@ -69,7 +72,16 @@ class ResetSelfPacedScheduleTests(SharedModuleStoreTestCase): # lint-amnesty, p
self.create_schedule()
self.schedule.delete()
reset_self_paced_schedule(self.user, self.course.id, use_availability_date=False)
reset_self_paced_schedule(self.user, self.course.id, use_availability_date=True)
reset_self_paced_schedule(self.user, self.course.id, use_enrollment_date=False)
reset_self_paced_schedule(self.user, self.course.id, use_enrollment_date=True)
assert Schedule.objects.count() == 0
@ddt.data(True, False)
def test_reset_before_course_starts(self, use_enrollment_date):
self.create_schedule(enrollment_offset=-5, course_start_offset=5) # starts in 5 days, enrollment is now
reset_self_paced_schedule(self.user, self.course.id, use_enrollment_date=use_enrollment_date)
self.schedule.refresh_from_db()
assert self.schedule.start_date == self.enrollment.course.start

View File

@@ -34,7 +34,7 @@ class PrefixedDebugLoggerMixin: # lint-amnesty, pylint: disable=missing-class-d
LOG.info(self.log_prefix + ': ' + message, *args, **kwargs)
def reset_self_paced_schedule(user, course_key, use_availability_date=False):
def reset_self_paced_schedule(user, course_key, use_enrollment_date=False):
"""
Reset the user's schedule if self-paced.
@@ -44,7 +44,7 @@ def reset_self_paced_schedule(user, course_key, use_availability_date=False):
Arguments:
user (User)
course_key (CourseKey or str)
use_availability_date (bool): if False, reset to now, else reset to when user got access to course material
use_enrollment_date (bool): if False, reset to now, else reset to original enrollment creation date
"""
with transaction.atomic(savepoint=False):
try:
@@ -56,10 +56,13 @@ def reset_self_paced_schedule(user, course_key, use_availability_date=False):
except Schedule.DoesNotExist:
return
if use_availability_date:
enrollment = schedule.enrollment
schedule.start_date = max(enrollment.created, enrollment.course.start)
schedule.save()
if use_enrollment_date:
new_start_date = schedule.enrollment.created
else:
schedule.start_date = datetime.datetime.now(pytz.utc)
schedule.save()
new_start_date = datetime.datetime.now(pytz.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)
schedule.start_date = new_start_date
schedule.save()

View File

@@ -47,7 +47,7 @@ class ResetCourseDeadlinesViewTests(EventTestMixin, BaseCourseHomeTests, Masquer
def test_reset_deadlines_with_masquerade(self):
""" Staff users should be able to masquerade as a learner and reset the learner's schedule """
course = CourseFactory.create(self_paced=True)
course = CourseFactory.create(self_paced=True, start=timezone.now() - datetime.timedelta(days=1))
student_username = self.user.username
student_user_id = self.user.id
student_enrollment = CourseEnrollment.enroll(self.user, course.id)

View File

@@ -71,7 +71,7 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase, MasqueradeMixin):
# pylint: disable=super-method-not-called
with super().setUpClassAndTestData():
cls.courses = []
course = CourseFactory.create(self_paced=True)
course = CourseFactory.create(self_paced=True, start=timezone.now() - datetime.timedelta(days=1))
with cls.store.bulk_operations(course.id):
chapter = ItemFactory.create(category='chapter', parent_location=course.location)
sequential = ItemFactory.create(category='sequential', parent_location=chapter.location, graded=True, format="Homework") # lint-amnesty, pylint: disable=line-too-long