Merge pull request #23357 from edx/mikix/reset-on-mode

Reset schedule when mode changes
This commit is contained in:
Calen Pennington
2020-03-11 13:05:39 -04:00
committed by GitHub
7 changed files with 184 additions and 22 deletions

View File

@@ -1303,7 +1303,8 @@ class CourseEnrollment(models.Model):
sender=None,
user=self.user,
course_key=self.course_id,
countdown=SCORE_RECALCULATION_DELAY_ON_ENROLLMENT_UPDATE
mode=self.mode,
countdown=SCORE_RECALCULATION_DELAY_ON_ENROLLMENT_UPDATE,
)
def send_signal(self, event, cost=None, currency=None):

View File

@@ -5,7 +5,7 @@ Enrollment track related signals.
from django.dispatch import Signal
ENROLLMENT_TRACK_UPDATED = Signal(providing_args=['user', 'course_key'])
ENROLLMENT_TRACK_UPDATED = Signal(providing_args=['user', 'course_key', 'mode', 'countdown'])
UNENROLL_DONE = Signal(providing_args=["course_enrollment", "skip_refund"])
ENROLL_STATUS_CHANGE = Signal(providing_args=["event", "user", "course_id", "mode", "cost", "currency"])
REFUND_ORDER = Signal(providing_args=["course_enrollment"])

View File

@@ -20,8 +20,10 @@ from lms.djangoapps.courseware.models import (
from openedx.core.djangoapps.content.course_overviews.signals import COURSE_START_DATE_CHANGED
from openedx.core.djangoapps.schedules.content_highlights import course_has_highlights
from openedx.core.djangoapps.schedules.models import ScheduleExperience
from openedx.core.djangoapps.schedules.utils import reset_self_paced_schedule
from openedx.core.djangoapps.theming.helpers import get_current_site
from student.models import CourseEnrollment
from student.signals import ENROLLMENT_TRACK_UPDATED
from track import segment
from .config import CREATE_SCHEDULE_WAFFLE_FLAG
@@ -78,6 +80,18 @@ def update_schedules_on_course_start_changed(sender, updated_course_overview, pr
)
@receiver(ENROLLMENT_TRACK_UPDATED)
def reset_schedule_on_mode_change(sender, user, course_key, mode, **kwargs): # pylint: disable=unused-argument
"""
When a CourseEnrollment's mode is changed, reset the user's schedule if self-paced.
"""
# 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)
def _calculate_upgrade_deadline(course_id, content_availability_date):
upgrade_deadline = None

View File

@@ -226,6 +226,49 @@ class UpdateScheduleTests(SharedModuleStoreTestCase):
self.assert_schedule_dates(enrollment.schedule, course.start) # start set to new course start
@skip_unless_lms
@override_waffle_flag(CREATE_SCHEDULE_WAFFLE_FLAG, True)
class ResetScheduleTests(SharedModuleStoreTestCase):
def setUp(self):
super().setUp()
self.config = ScheduleConfigFactory(create_schedules=True)
site_patch = patch('openedx.core.djangoapps.schedules.signals.get_current_site', return_value=self.config.site)
self.addCleanup(site_patch.stop)
site_patch.start()
self.course = _create_course_run(self_paced=True)
self.enrollment = CourseEnrollmentFactory(
course_id=self.course.id,
mode=CourseMode.AUDIT,
)
self.schedule = self.enrollment.schedule
def test_schedule_is_reset_after_enrollment_change(self):
""" Test that an update in enrollment causes a schedule reset. """
original_start = self.schedule.start_date
CourseEnrollment.enroll(self.enrollment.user, self.course.id, mode=CourseMode.VERIFIED)
self.schedule.refresh_from_db()
self.assertGreater(self.schedule.start_date, original_start) # should have been reset to current time
def test_schedule_is_reset_to_availabilty_date(self):
""" Test that a switch to audit enrollment resets to the availabilty date, not current time. """
original_start = self.schedule.start_date
# Switch to verified, confirm we change start date
CourseEnrollment.enroll(self.enrollment.user, self.course.id, mode=CourseMode.VERIFIED)
self.schedule.refresh_from_db()
self.assertNotEqual(self.schedule.start_date, original_start)
# Switch back to audit, confirm we change back to original availability date
CourseEnrollment.enroll(self.enrollment.user, self.course.id, mode=CourseMode.AUDIT)
self.schedule.refresh_from_db()
self.assertEqual(self.schedule.start_date, original_start)
def _create_course_run(self_paced=True, start_day_offset=-1):
""" Create a new course run and course modes.

View File

@@ -0,0 +1,80 @@
"""
Tests for schedules utils
"""
import datetime
import ddt
from course_modes.models import CourseMode
from mock import patch
from pytz import utc
from openedx.core.djangoapps.schedules.models import Schedule
from openedx.core.djangoapps.schedules.tests.factories import ScheduleConfigFactory
from openedx.core.djangoapps.schedules.utils import reset_self_paced_schedule
from openedx.core.djangolib.testing.utils import skip_unless_lms
from student.tests.factories import CourseEnrollmentFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
@ddt.ddt
@skip_unless_lms
class ResetSelfPacedScheduleTests(SharedModuleStoreTestCase):
def create_schedule(self, offset=0):
self.config = ScheduleConfigFactory(create_schedules=True)
site_patch = patch('openedx.core.djangoapps.schedules.signals.get_current_site', return_value=self.config.site)
self.addCleanup(site_patch.stop)
site_patch.start()
start = datetime.datetime.now(utc) - datetime.timedelta(days=100)
self.course = CourseFactory.create(start=start, self_paced=True)
self.enrollment = CourseEnrollmentFactory(
course_id=self.course.id,
mode=CourseMode.AUDIT,
)
self.enrollment.created = start + datetime.timedelta(days=offset)
self.enrollment.save()
self.schedule = self.enrollment.schedule
self.schedule.start_date = self.enrollment.created
self.schedule.save()
self.user = self.enrollment.user
def test_reset_to_now(self):
self.create_schedule()
original_start = self.schedule.start_date
with self.assertNumQueries(1):
reset_self_paced_schedule(self.user, self.course.id, use_availability_date=False)
self.schedule.refresh_from_db()
self.assertGreater(self.schedule.start_date, original_start)
@ddt.data(
(-1, 0), # enrolled before course started (will reset to start date)
(1, 1), # enrolled after course started (will reset to enroll date)
)
@ddt.unpack
def test_reset_to_start_date(self, offset, expected_offset):
self.create_schedule(offset=offset)
expected_start = self.course.start + datetime.timedelta(days=expected_offset)
with self.assertNumQueries(1):
reset_self_paced_schedule(self.user, self.course.id, use_availability_date=True)
self.schedule.refresh_from_db()
self.assertEqual(self.schedule.start_date.replace(microsecond=0), expected_start.replace(microsecond=0))
def test_safe_without_schedule(self):
""" Just ensure that we don't raise exceptions or create any schedules """
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)
self.assertEqual(Schedule.objects.count(), 0)

View File

@@ -1,7 +1,14 @@
import datetime
import logging
import pytz
from django.db.models import F, Subquery
from django.db.models.functions import Greatest
from openedx.core.djangoapps.schedules.models import Schedule
LOG = logging.getLogger(__name__)
@@ -26,3 +33,28 @@ class PrefixedDebugLoggerMixin(object):
Wrapper around LOG.info that prefixes the message.
"""
LOG.info(self.log_prefix + ': ' + message, *args, **kwargs)
def reset_self_paced_schedule(user, course_key, use_availability_date=False):
"""
Reset the user's schedule if self-paced.
It does not create a new schedule, just resets an existing one.
This is used, for example, when a user requests it or when an enrollment mode changes.
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
"""
schedule = Schedule.objects.filter(
enrollment__user=user,
enrollment__course__id=course_key,
enrollment__course__self_paced=True,
)
if use_availability_date:
schedule = schedule.annotate(start_of_access=Greatest(F('enrollment__created'), F('enrollment__course__start')))
schedule.update(start_date=Subquery(schedule.values('start_of_access')[:1]))
else:
schedule.update(start_date=datetime.datetime.now(pytz.utc))

View File

@@ -5,7 +5,6 @@ Views to show a course outline.
import datetime
import re
import pytz
import six
from completion import waffle as completion_waffle
@@ -24,9 +23,8 @@ from web_fragments.fragment import Fragment
from lms.djangoapps.courseware.access import has_access
from lms.djangoapps.courseware.courses import get_course_overview_with_access
from lms.djangoapps.courseware.masquerade import setup_masquerade
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
from student.models import CourseEnrollment
from openedx.core.djangoapps.schedules.utils import reset_self_paced_schedule
from util.milestones_helpers import get_course_content_milestones
from xmodule.course_module import COURSE_VISIBILITY_PUBLIC
from xmodule.modulestore.django import modulestore
@@ -171,21 +169,15 @@ def reset_course_deadlines(request, course_id):
sequentials belonging to a self paced course
"""
course_key = CourseKey.from_string(course_id)
course = CourseOverview.objects.get(id=course_key)
if course.self_paced:
masquerade_details, masquerade_user = setup_masquerade(
request,
course_key,
has_access(request.user, 'staff', course, course_key)
)
if masquerade_details and masquerade_details.role == 'student' and masquerade_details.user_name:
# Masquerading as a specific student, so reset that student's schedule
user = masquerade_user
else:
user = request.user
enrollment = CourseEnrollment.objects.get(user=user, course=course_key)
schedule = enrollment.schedule
if schedule:
schedule.start_date = datetime.datetime.now(pytz.utc)
schedule.save()
masquerade_details, masquerade_user = setup_masquerade(
request,
course_key,
has_access(request.user, 'staff', course_key)
)
if masquerade_details and masquerade_details.role == 'student' and masquerade_details.user_name:
# Masquerading as a specific student, so reset that student's schedule
user = masquerade_user
else:
user = request.user
reset_self_paced_schedule(user, course_key)
return redirect(reverse('openedx.course_experience.course_home', args=[six.text_type(course_key)]))