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): ...@@ -134,6 +134,8 @@ class CourseMode(models.Model):
) )
DEFAULT_MODE_SLUG = settings.COURSE_MODE_DEFAULTS['slug'] 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 # Modes utilized for audit/free enrollments
AUDIT_MODES = [AUDIT, HONOR] AUDIT_MODES = [AUDIT, HONOR]
...@@ -490,6 +492,16 @@ class CourseMode(models.Model): ...@@ -490,6 +492,16 @@ class CourseMode(models.Model):
return slug in [cls.PROFESSIONAL, cls.NO_ID_PROFESSIONAL_MODE] return slug in [cls.PROFESSIONAL, cls.NO_ID_PROFESSIONAL_MODE]
@classmethod @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): def is_verified_mode(cls, course_mode_tuple):
"""Check whether the given modes is_verified or not. """Check whether the given modes is_verified or not.
......
...@@ -1678,6 +1678,55 @@ class CourseEnrollment(models.Model): ...@@ -1678,6 +1678,55 @@ class CourseEnrollment(models.Model):
self._course_overview = None self._course_overview = None
return self._course_overview 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): def is_verified_enrollment(self):
""" """
Check the course enrollment mode is verified or not Check the course enrollment mode is verified or not
......
# pylint: disable=missing-docstring # pylint: disable=missing-docstring
import datetime
import hashlib import hashlib
import ddt
import pytz
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.core.cache import cache from django.core.cache import cache
from django.db.models.functions import Lower 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.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.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
@ddt.ddt
class CourseEnrollmentTests(SharedModuleStoreTestCase): class CourseEnrollmentTests(SharedModuleStoreTestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
...@@ -20,8 +27,8 @@ class CourseEnrollmentTests(SharedModuleStoreTestCase): ...@@ -20,8 +27,8 @@ class CourseEnrollmentTests(SharedModuleStoreTestCase):
def setUp(self): def setUp(self):
super(CourseEnrollmentTests, self).setUp() super(CourseEnrollmentTests, self).setUp()
self.user = UserFactory.create() self.user = UserFactory()
self.user_2 = UserFactory.create() self.user_2 = UserFactory()
def test_enrollment_status_hash_cache_key(self): def test_enrollment_status_hash_cache_key(self):
username = 'test-user' username = 'test-user'
...@@ -103,3 +110,25 @@ class CourseEnrollmentTests(SharedModuleStoreTestCase): ...@@ -103,3 +110,25 @@ class CourseEnrollmentTests(SharedModuleStoreTestCase):
CourseEnrollment.objects.users_enrolled_in(self.course.id, include_inactive=True) CourseEnrollment.objects.users_enrolled_in(self.course.id, include_inactive=True)
) )
self.assertListEqual([self.user, self.user_2], all_enrolled_users) 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