Commit 1e7f7df7 by Clinton Blackburn Committed by Clinton Blackburn

Added CourseEnrollment.upgrade_deadline

This new property knows how to rely on schedule deadliens and fallback to course mode deadlines, when necessary.
parent 7274a20f
......@@ -134,6 +134,8 @@ class CourseMode(models.Model):
)
DEFAULT_MODE_SLUG = settings.COURSE_MODE_DEFAULTS['slug']
ALL_MODES = [AUDIT, CREDIT_MODE, HONOR, NO_ID_PROFESSIONAL_MODE, PROFESSIONAL, VERIFIED, ]
# Modes utilized for audit/free enrollments
AUDIT_MODES = [AUDIT, HONOR]
......@@ -490,6 +492,16 @@ class CourseMode(models.Model):
return slug in [cls.PROFESSIONAL, cls.NO_ID_PROFESSIONAL_MODE]
@classmethod
def is_mode_upgradeable(cls, mode_slug):
"""
Returns True if the given mode can be upgraded to another.
Note: Although, in practice, learners "upgrade" from verified to credit,
that particular upgrade path is excluded by this method.
"""
return mode_slug in cls.AUDIT_MODES
@classmethod
def is_verified_mode(cls, course_mode_tuple):
"""Check whether the given modes is_verified or not.
......
......@@ -1678,6 +1678,55 @@ class CourseEnrollment(models.Model):
self._course_overview = None
return self._course_overview
@property
def upgrade_deadline(self):
"""
Returns the upgrade deadline for this enrollment, if it is upgradeable.
If the seat cannot be upgraded, None is returned.
Note:
When loading this model, use `select_related` to retrieve the associated schedule object.
Returns:
datetime|None
"""
log.debug('Schedules: Determining upgrade deadline for CourseEnrollment %d...', self.id)
if not CourseMode.is_mode_upgradeable(self.mode):
log.debug(
'Schedules: %s mode of %s is not upgradeable. Returning None for upgrade deadline.',
self.mode, self.course_id
)
return None
try:
if self.schedule:
log.debug(
'Schedules: Pulling upgrade deadline for CourseEnrollment %d from Schedule %d.',
self.id, self.schedule.id
)
return self.schedule.upgrade_deadline
except ObjectDoesNotExist:
# NOTE: Schedule has a one-to-one mapping with CourseEnrollment. If no schedule is associated
# with this enrollment, Django will raise an exception rather than return None.
log.debug('Schedules: No schedule exists for CourseEnrollment %d.', self.id)
pass
try:
verified_mode = CourseMode.verified_mode_for_course(self.course_id)
if verified_mode:
log.debug('Schedules: Defaulting to verified mode expiration date-time for %s.', self.course_id)
return verified_mode.expiration_datetime
else:
log.debug('Schedules: No verified mode located for %s.', self.course_id)
except CourseMode.DoesNotExist:
log.debug('Schedules: %s has no verified mode.', self.course_id)
pass
log.debug('Schedules: Returning default of `None`')
return None
def is_verified_enrollment(self):
"""
Check the course enrollment mode is verified or not
......
# pylint: disable=missing-docstring
import datetime
import hashlib
import ddt
import pytz
from django.contrib.auth.models import AnonymousUser
from django.core.cache import cache
from django.db.models.functions import Lower
from course_modes.models import CourseMode
from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory
from openedx.core.djangolib.testing.utils import skip_unless_lms
from student.models import CourseEnrollment
from student.tests.factories import CourseEnrollmentFactory, UserFactory
from student.tests.factories import CourseEnrollmentFactory, UserFactory, CourseModeFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
@ddt.ddt
class CourseEnrollmentTests(SharedModuleStoreTestCase):
@classmethod
def setUpClass(cls):
......@@ -20,8 +27,8 @@ class CourseEnrollmentTests(SharedModuleStoreTestCase):
def setUp(self):
super(CourseEnrollmentTests, self).setUp()
self.user = UserFactory.create()
self.user_2 = UserFactory.create()
self.user = UserFactory()
self.user_2 = UserFactory()
def test_enrollment_status_hash_cache_key(self):
username = 'test-user'
......@@ -103,3 +110,25 @@ class CourseEnrollmentTests(SharedModuleStoreTestCase):
CourseEnrollment.objects.users_enrolled_in(self.course.id, include_inactive=True)
)
self.assertListEqual([self.user, self.user_2], all_enrolled_users)
@skip_unless_lms
def test_upgrade_deadline(self):
""" The property should use either the CourseMode or related Schedule to determine the deadline. """
course_mode = CourseModeFactory(
course_id=self.course.id,
mode_slug=CourseMode.VERIFIED,
expiration_datetime=datetime.datetime.now(pytz.UTC)
)
enrollment = CourseEnrollmentFactory(course_id=self.course.id, mode=CourseMode.AUDIT)
self.assertEqual(enrollment.upgrade_deadline, course_mode.expiration_datetime)
# The schedule's upgrade deadline should be used if a schedule exists
schedule = ScheduleFactory(enrollment=enrollment)
self.assertEqual(enrollment.upgrade_deadline, schedule.upgrade_deadline)
@skip_unless_lms
@ddt.data(*(set(CourseMode.ALL_MODES) - set(CourseMode.AUDIT_MODES)))
def test_upgrade_deadline_for_non_upgradeable_enrollment(self, mode):
""" The property should return None if an upgrade cannot be upgraded. """
enrollment = CourseEnrollmentFactory(course_id=self.course.id, mode=mode)
self.assertIsNone(enrollment.upgrade_deadline)
import factory
import pytz
from openedx.core.djangoapps.schedules import models
class ScheduleFactory(factory.DjangoModelFactory):
class Meta(object):
model = models.Schedule
start = factory.Faker('future_datetime', tzinfo=pytz.UTC)
upgrade_deadline = factory.Faker('future_datetime', tzinfo=pytz.UTC)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment