Flesh out expiration date for user-course combo logic
This commit is contained in:
@@ -1433,8 +1433,8 @@ class ProgressPageTests(ProgressPageBaseTests):
|
||||
|
||||
@override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True)
|
||||
@ddt.data(
|
||||
(True, 40),
|
||||
(False, 39)
|
||||
(True, 39),
|
||||
(False, 38)
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_progress_queries_paced_courses(self, self_paced, query_count):
|
||||
@@ -1446,8 +1446,8 @@ class ProgressPageTests(ProgressPageBaseTests):
|
||||
@override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True)
|
||||
@patch.dict(settings.FEATURES, {'ASSUME_ZERO_GRADE_IF_ABSENT_FOR_ALL_TESTS': False})
|
||||
@ddt.data(
|
||||
(False, 47, 30),
|
||||
(True, 39, 26)
|
||||
(False, 46, 29),
|
||||
(True, 38, 25)
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_progress_queries(self, enable_waffle, initial, subsequent):
|
||||
|
||||
@@ -14,7 +14,7 @@ from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
from six import text_type
|
||||
|
||||
from lms.djangoapps.courseware.tests.factories import GlobalStaffFactory, StaffFactory
|
||||
from lms.djangoapps.courseware.tests.factories import GlobalStaffFactory
|
||||
from lms.djangoapps.grades.api.v1.views import CourseGradesView
|
||||
from lms.djangoapps.grades.config.waffle import waffle_flags, WRITABLE_GRADEBOOK
|
||||
from lms.djangoapps.grades.course_data import CourseData
|
||||
|
||||
@@ -12,9 +12,14 @@ from django.utils.translation import ugettext as _
|
||||
from util.date_utils import DEFAULT_SHORT_DATE_FORMAT, strftime_localized
|
||||
from lms.djangoapps.courseware.access_response import AccessError
|
||||
from lms.djangoapps.courseware.access_utils import ACCESS_GRANTED
|
||||
from openedx.core.djangoapps.catalog.utils import get_course_run_details
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
|
||||
|
||||
MIN_DURATION = timedelta(weeks=4)
|
||||
MAX_DURATION = timedelta(weeks=12)
|
||||
|
||||
|
||||
class AuditExpiredError(AccessError):
|
||||
"""
|
||||
Access denied because the user's audit timespan has expired
|
||||
@@ -43,11 +48,19 @@ def get_user_course_expiration_date(user, course):
|
||||
"""
|
||||
Return course expiration date for given user course pair.
|
||||
Return None if the course does not expire.
|
||||
Defaults to MIN_DURATION.
|
||||
|
||||
Business Logic:
|
||||
-
|
||||
- should be bounded with min / max
|
||||
- if fields are missing, default to minimum time
|
||||
"""
|
||||
# TODO: Update business logic based on REV-531
|
||||
|
||||
access_duration = MIN_DURATION
|
||||
|
||||
CourseEnrollment = apps.get_model('student.CourseEnrollment')
|
||||
enrollment = CourseEnrollment.get_enrollment(user, course.id)
|
||||
if enrollment is None or enrollment.mode == 'verified':
|
||||
if enrollment is None or enrollment.mode != 'audit':
|
||||
return None
|
||||
|
||||
try:
|
||||
@@ -55,13 +68,19 @@ def get_user_course_expiration_date(user, course):
|
||||
except CourseEnrollment.schedule.RelatedObjectDoesNotExist:
|
||||
start_date = max(enrollment.created, course.start)
|
||||
|
||||
access_duration = timedelta(weeks=8)
|
||||
if hasattr(course, 'pacing') and course.pacing == 'instructor':
|
||||
if course.end and course.start:
|
||||
access_duration = course.end - course.start
|
||||
if course.self_paced:
|
||||
# self-paced expirations should be start date plus the marketing course length discovery
|
||||
discovery_course_details = get_course_run_details(course.id, ['weeks_to_complete'])
|
||||
expected_weeks = discovery_course_details['weeks_to_complete'] or int(MIN_DURATION.days / 7)
|
||||
access_duration = timedelta(weeks=expected_weeks)
|
||||
elif not course.self_paced and course.end and course.start:
|
||||
# instructor-paced expirations should be the start date plus the length of the course
|
||||
access_duration = course.end - course.start
|
||||
|
||||
expiration_date = start_date + access_duration
|
||||
return expiration_date
|
||||
# available course time should bound my the min and max duration
|
||||
access_duration = max(MIN_DURATION, min(MAX_DURATION, access_duration))
|
||||
|
||||
return start_date + access_duration
|
||||
|
||||
|
||||
def check_course_expired(user, course):
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
Contains tests to verify correctness of course expiration functionality
|
||||
"""
|
||||
from datetime import timedelta
|
||||
from django.utils.timezone import now
|
||||
import ddt
|
||||
import mock
|
||||
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from openedx.features.course_duration_limits.access import get_user_course_expiration_date, MIN_DURATION, MAX_DURATION
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class CourseExpirationTestCase(ModuleStoreTestCase):
|
||||
"""Tests to verify the get_user_course_expiration_date function is working correctly"""
|
||||
def setUp(self):
|
||||
super(CourseExpirationTestCase, self).setUp()
|
||||
self.course = CourseFactory(
|
||||
start=now() - timedelta(weeks=10),
|
||||
)
|
||||
self.user = UserFactory()
|
||||
|
||||
def tearDown(self):
|
||||
CourseEnrollment.unenroll(self.user, self.course.id)
|
||||
super(CourseExpirationTestCase, self).tearDown()
|
||||
|
||||
def test_enrollment_mode(self):
|
||||
"""Tests that verified enrollments do not have an expiration"""
|
||||
CourseEnrollment.enroll(self.user, self.course.id, CourseMode.VERIFIED)
|
||||
result = get_user_course_expiration_date(self.user, self.course)
|
||||
self.assertEqual(result, None)
|
||||
|
||||
def test_instructor_paced(self):
|
||||
"""Tests that instructor paced courses give the learner start_date - end_date time in the course"""
|
||||
expected_difference = timedelta(weeks=6)
|
||||
self.course.self_paced = False
|
||||
self.course.end = self.course.start + expected_difference
|
||||
enrollment = CourseEnrollment.enroll(self.user, self.course.id, CourseMode.AUDIT)
|
||||
result = get_user_course_expiration_date(self.user, self.course)
|
||||
self.assertEqual(result, enrollment.created + expected_difference)
|
||||
|
||||
def test_instructor_paced_no_end_date(self):
|
||||
"""Tests that instructor paced with no end dates returns default (minimum)"""
|
||||
self.course.self_paced = False
|
||||
enrollment = CourseEnrollment.enroll(self.user, self.course.id, CourseMode.AUDIT)
|
||||
result = get_user_course_expiration_date(self.user, self.course)
|
||||
self.assertEqual(result, enrollment.created + MIN_DURATION)
|
||||
|
||||
@mock.patch("openedx.features.course_duration_limits.access.get_course_run_details")
|
||||
@ddt.data(
|
||||
[int(MIN_DURATION.days / 7) - 1, MIN_DURATION],
|
||||
[7, timedelta(weeks=7)],
|
||||
[int(MAX_DURATION.days / 7) + 1, MAX_DURATION],
|
||||
[None, MIN_DURATION],
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_self_paced_with_weeks_to_complete(
|
||||
self,
|
||||
weeks_to_complete,
|
||||
expected_difference,
|
||||
mock_get_course_run_details,
|
||||
):
|
||||
"""
|
||||
Tests that self paced courses allow for a (bounded) # of weeks in courses determined via
|
||||
weeks_to_complete field in discovery. If the field doesn't exist, it should return default (minimum)
|
||||
"""
|
||||
self.course.self_paced = True
|
||||
mock_get_course_run_details.return_value = {'weeks_to_complete': weeks_to_complete}
|
||||
enrollment = CourseEnrollment.enroll(self.user, self.course.id, CourseMode.AUDIT)
|
||||
result = get_user_course_expiration_date(self.user, self.course)
|
||||
self.assertEqual(result, enrollment.created + expected_difference)
|
||||
@@ -337,7 +337,7 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
|
||||
url = course_home_url(course)
|
||||
response = self.client.get(url)
|
||||
|
||||
expiration_date = strftime_localized(course.start + timedelta(weeks=8), 'SHORT_DATE')
|
||||
expiration_date = strftime_localized(course.start + timedelta(weeks=4), 'SHORT_DATE')
|
||||
expected_params = QueryDict(mutable=True)
|
||||
course_name = CourseOverview.get_from_id(course.id).display_name_with_default
|
||||
expected_params['access_response_error'] = 'Access to {run} expired on {expiration_date}'.format(
|
||||
|
||||
Reference in New Issue
Block a user