Commit 5db02ef6 by Clinton Blackburn Committed by Clinton Blackburn

Added support for dynamic upgrade deadlines

The verified seat upgrade deadline for self-paced course runs is now
dependent on when the learner was first able to access the content--the
latest of enrollment date and course run start date.
parent 80538211
''' from config_models.admin import ConfigurationModelAdmin, KeyedConfigurationModelAdmin
django admin pages for courseware model
'''
from ratelimitbackend import admin from ratelimitbackend import admin
from courseware.models import OfflineComputedGrade, OfflineComputedGradeLog, StudentModule from courseware import models
admin.site.register(StudentModule)
admin.site.register(OfflineComputedGrade)
admin.site.register(OfflineComputedGradeLog) admin.site.register(models.DynamicUpgradeDeadlineConfiguration, ConfigurationModelAdmin)
admin.site.register(models.OfflineComputedGrade)
admin.site.register(models.OfflineComputedGradeLog)
admin.site.register(models.CourseDynamicUpgradeDeadlineConfiguration, KeyedConfigurationModelAdmin)
admin.site.register(models.StudentModule)
...@@ -18,7 +18,7 @@ from courseware.date_summary import ( ...@@ -18,7 +18,7 @@ from courseware.date_summary import (
VerifiedUpgradeDeadlineDate VerifiedUpgradeDeadlineDate
) )
from courseware.model_data import FieldDataCache from courseware.model_data import FieldDataCache
from courseware.module_render import get_module, get_module_for_descriptor from courseware.module_render import get_module
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import Http404, QueryDict from django.http import Http404, QueryDict
......
...@@ -3,18 +3,21 @@ This module provides date summary blocks for the Course Info ...@@ -3,18 +3,21 @@ This module provides date summary blocks for the Course Info
page. Each block gives information about a particular page. Each block gives information about a particular
course-run-specific date which will be displayed to the user. course-run-specific date which will be displayed to the user.
""" """
from datetime import datetime import datetime
from babel.dates import format_timedelta from babel.dates import format_timedelta
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _ from django.utils.functional import cached_property
from django.utils.translation import get_language, to_locale, ugettext_lazy from django.utils.translation import get_language, to_locale, ugettext_lazy
from django.utils.translation import ugettext as _
from lazy import lazy from lazy import lazy
from pytz import timezone, utc from pytz import timezone, utc
from course_modes.models import CourseMode from course_modes.models import CourseMode
from courseware.models import CourseDynamicUpgradeDeadlineConfiguration, DynamicUpgradeDeadlineConfiguration
from lms.djangoapps.commerce.utils import EcommerceService from lms.djangoapps.commerce.utils import EcommerceService
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, VerificationDeadline from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, VerificationDeadline
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from student.models import CourseEnrollment from student.models import CourseEnrollment
...@@ -85,7 +88,7 @@ class DateSummary(object): ...@@ -85,7 +88,7 @@ class DateSummary(object):
if self.date is None: if self.date is None:
return '' return ''
locale = to_locale(get_language()) locale = to_locale(get_language())
delta = self.date - datetime.now(utc) delta = self.date - datetime.datetime.now(utc)
try: try:
relative_date = format_timedelta(delta, locale=locale) relative_date = format_timedelta(delta, locale=locale)
# Babel doesn't have translations for Esperanto, so we get # Babel doesn't have translations for Esperanto, so we get
...@@ -115,7 +118,7 @@ class DateSummary(object): ...@@ -115,7 +118,7 @@ class DateSummary(object):
future. future.
""" """
if self.date is not None: if self.date is not None:
return datetime.now(utc).date() <= self.date.date() return datetime.datetime.now(utc).date() <= self.date.date()
return False return False
def deadline_has_passed(self): def deadline_has_passed(self):
...@@ -124,7 +127,7 @@ class DateSummary(object): ...@@ -124,7 +127,7 @@ class DateSummary(object):
Returns False otherwise. Returns False otherwise.
""" """
deadline = self.date deadline = self.date
return deadline is not None and deadline <= datetime.now(utc) return deadline is not None and deadline <= datetime.datetime.now(utc)
def __repr__(self): def __repr__(self):
return u'DateSummary: "{title}" {date} is_enabled={is_enabled}'.format( return u'DateSummary: "{title}" {date} is_enabled={is_enabled}'.format(
...@@ -149,7 +152,7 @@ class TodaysDate(DateSummary): ...@@ -149,7 +152,7 @@ class TodaysDate(DateSummary):
@property @property
def date(self): def date(self):
return datetime.now(utc) return datetime.datetime.now(utc)
@property @property
def title(self): def title(self):
...@@ -181,7 +184,7 @@ class CourseEndDate(DateSummary): ...@@ -181,7 +184,7 @@ class CourseEndDate(DateSummary):
@property @property
def description(self): def description(self):
if datetime.now(utc) <= self.date: if datetime.datetime.now(utc) <= self.date:
mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course_id) mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course_id)
if is_active and CourseMode.is_eligible_for_certificate(mode): if is_active and CourseMode.is_eligible_for_certificate(mode):
return _('To earn a certificate, you must complete all requirements before this date.') return _('To earn a certificate, you must complete all requirements before this date.')
...@@ -217,6 +220,14 @@ class VerifiedUpgradeDeadlineDate(DateSummary): ...@@ -217,6 +220,14 @@ class VerifiedUpgradeDeadlineDate(DateSummary):
return ecommerce_service.get_checkout_page_url(course_mode.sku) return ecommerce_service.get_checkout_page_url(course_mode.sku)
return reverse('verify_student_upgrade_and_verify', args=(self.course_id,)) return reverse('verify_student_upgrade_and_verify', args=(self.course_id,))
@cached_property
def enrollment(self):
return CourseEnrollment.get_enrollment(self.user, self.course_id)
@cached_property
def course_overview(self):
return CourseOverview.get_from_id(self.course_id)
@property @property
def is_enabled(self): def is_enabled(self):
""" """
...@@ -229,7 +240,12 @@ class VerifiedUpgradeDeadlineDate(DateSummary): ...@@ -229,7 +240,12 @@ class VerifiedUpgradeDeadlineDate(DateSummary):
if not is_enabled: if not is_enabled:
return False return False
enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course_id) enrollment_mode = None
is_active = None
if self.enrollment:
enrollment_mode = self.enrollment.mode
is_active = self.enrollment.is_active
# Return `true` if user is not enrolled in course # Return `true` if user is not enrolled in course
if enrollment_mode is None and is_active is None: if enrollment_mode is None and is_active is None:
...@@ -240,13 +256,40 @@ class VerifiedUpgradeDeadlineDate(DateSummary): ...@@ -240,13 +256,40 @@ class VerifiedUpgradeDeadlineDate(DateSummary):
@lazy @lazy
def date(self): def date(self):
deadline = None
try: try:
verified_mode = CourseMode.objects.get( verified_mode = CourseMode.objects.get(course_id=self.course_id, mode_slug=CourseMode.VERIFIED)
course_id=self.course_id, mode_slug=CourseMode.VERIFIED deadline = verified_mode.expiration_datetime
)
return verified_mode.expiration_datetime
except CourseMode.DoesNotExist: except CourseMode.DoesNotExist:
return None pass
if self.course and self.course_overview.self_paced and self.enrollment:
global_config = DynamicUpgradeDeadlineConfiguration.current()
if global_config.enabled:
delta = global_config.deadline_days
# Check if the given course has opted out of the feature
course_config = CourseDynamicUpgradeDeadlineConfiguration.current(self.course.id)
if course_config.enabled:
if course_config.opt_out:
return deadline
delta = course_config.deadline_days
# This represents the first date at which the learner can access the content. This will be the
# latter of either the enrollment date or the course's start date.
content_availability_date = max(self.enrollment.created, self.course_overview.start)
user_deadline = content_availability_date + datetime.timedelta(days=delta)
# If the deadline from above is None, make sure we have a value for comparison
deadline = deadline or datetime.date.max
# The user-specific deadline should never occur after the verified mode's expiration date,
# if one is set.
deadline = min(deadline, user_deadline)
return deadline
class VerificationDeadlineDate(DateSummary): class VerificationDeadlineDate(DateSummary):
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings
import django.db.models.deletion
import openedx.core.djangoapps.xmodule_django.models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('courseware', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='CourseDynamicUpgradeDeadlineConfiguration',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
('course_id', openedx.core.djangoapps.xmodule_django.models.CourseKeyField(max_length=255, db_index=True)),
('deadline_days', models.PositiveSmallIntegerField(default=21, help_text='Number of days a learner has to upgrade after content is made available')),
('opt_out', models.BooleanField(default=False, help_text='Disable the dynamic upgrade deadline for this course run.')),
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to=settings.AUTH_USER_MODEL, null=True, verbose_name='Changed by')),
],
),
migrations.CreateModel(
name='DynamicUpgradeDeadlineConfiguration',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
('deadline_days', models.PositiveSmallIntegerField(default=21, help_text='Number of days a learner has to upgrade after content is made available')),
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to=settings.AUTH_USER_MODEL, null=True, verbose_name='Changed by')),
],
),
]
...@@ -15,10 +15,12 @@ ASSUMPTIONS: modules have unique IDs, even across different module_types ...@@ -15,10 +15,12 @@ ASSUMPTIONS: modules have unique IDs, even across different module_types
import itertools import itertools
import logging import logging
from config_models.models import ConfigurationModel
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import models from django.db import models
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.utils.translation import ugettext_lazy as _
from model_utils.models import TimeStampedModel from model_utils.models import TimeStampedModel
import coursewarehistoryextended import coursewarehistoryextended
...@@ -40,6 +42,7 @@ class ChunkingManager(models.Manager): ...@@ -40,6 +42,7 @@ class ChunkingManager(models.Manager):
:class:`~Manager` that adds an additional method :meth:`chunked_filter` to provide :class:`~Manager` that adds an additional method :meth:`chunked_filter` to provide
the ability to make select queries with specific chunk sizes. the ability to make select queries with specific chunk sizes.
""" """
class Meta(object): class Meta(object):
app_label = "courseware" app_label = "courseware"
...@@ -130,7 +133,8 @@ class StudentModule(models.Model): ...@@ -130,7 +133,8 @@ class StudentModule(models.Model):
return queryset return queryset
def __repr__(self): def __repr__(self):
return 'StudentModule<%r>' % ({ return 'StudentModule<%r>' % (
{
'course_id': self.course_id, 'course_id': self.course_id,
'module_type': self.module_type, 'module_type': self.module_type,
# We use the student_id instead of username to avoid a database hop. # We use the student_id instead of username to avoid a database hop.
...@@ -267,6 +271,7 @@ class XModuleUserStateSummaryField(XBlockFieldBase): ...@@ -267,6 +271,7 @@ class XModuleUserStateSummaryField(XBlockFieldBase):
""" """
Stores data set in the Scope.user_state_summary scope by an xmodule field Stores data set in the Scope.user_state_summary scope by an xmodule field
""" """
class Meta(object): class Meta(object):
app_label = "courseware" app_label = "courseware"
unique_together = (('usage_id', 'field_name'),) unique_together = (('usage_id', 'field_name'),)
...@@ -279,6 +284,7 @@ class XModuleStudentPrefsField(XBlockFieldBase): ...@@ -279,6 +284,7 @@ class XModuleStudentPrefsField(XBlockFieldBase):
""" """
Stores data set in the Scope.preferences scope by an xmodule field Stores data set in the Scope.preferences scope by an xmodule field
""" """
class Meta(object): class Meta(object):
app_label = "courseware" app_label = "courseware"
unique_together = (('student', 'module_type', 'field_name'),) unique_together = (('student', 'module_type', 'field_name'),)
...@@ -293,6 +299,7 @@ class XModuleStudentInfoField(XBlockFieldBase): ...@@ -293,6 +299,7 @@ class XModuleStudentInfoField(XBlockFieldBase):
""" """
Stores data set in the Scope.preferences scope by an xmodule field Stores data set in the Scope.preferences scope by an xmodule field
""" """
class Meta(object): class Meta(object):
app_label = "courseware" app_label = "courseware"
unique_together = (('student', 'field_name'),) unique_together = (('student', 'field_name'),)
...@@ -314,7 +321,7 @@ class OfflineComputedGrade(models.Model): ...@@ -314,7 +321,7 @@ class OfflineComputedGrade(models.Model):
class Meta(object): class Meta(object):
app_label = "courseware" app_label = "courseware"
unique_together = (('user', 'course_id'), ) unique_together = (('user', 'course_id'),)
def __unicode__(self): def __unicode__(self):
return "[OfflineComputedGrade] %s: %s (%s) = %s" % (self.user, self.course_id, self.created, self.gradeset) return "[OfflineComputedGrade] %s: %s (%s) = %s" % (self.user, self.course_id, self.created, self.gradeset)
...@@ -325,6 +332,7 @@ class OfflineComputedGradeLog(models.Model): ...@@ -325,6 +332,7 @@ class OfflineComputedGradeLog(models.Model):
Log of when offline grades are computed. Log of when offline grades are computed.
Use this to be able to show instructor when the last computed grades were done. Use this to be able to show instructor when the last computed grades were done.
""" """
class Meta(object): class Meta(object):
app_label = "courseware" app_label = "courseware"
ordering = ["-created"] ordering = ["-created"]
...@@ -355,3 +363,40 @@ class StudentFieldOverride(TimeStampedModel): ...@@ -355,3 +363,40 @@ class StudentFieldOverride(TimeStampedModel):
field = models.CharField(max_length=255) field = models.CharField(max_length=255)
value = models.TextField(default='null') value = models.TextField(default='null')
class DynamicUpgradeDeadlineConfiguration(ConfigurationModel):
""" Dynamic upgrade deadline configuration.
This model controls the behavior of the dynamic upgrade deadline for self-paced courses.
"""
class Meta(object):
app_label = 'courseware'
deadline_days = models.PositiveSmallIntegerField(
default=21,
help_text=_('Number of days a learner has to upgrade after content is made available')
)
class CourseDynamicUpgradeDeadlineConfiguration(ConfigurationModel):
"""
Per-course run configuration for dynamic upgrade deadlines.
This model controls dynamic upgrade deadlines on a per-course run level, allowing course runs to
have different deadlines or opt out of the functionality altogether.
"""
class Meta(object):
app_label = 'courseware'
KEY_FIELDS = ('course_id',)
course_id = CourseKeyField(max_length=255, db_index=True)
deadline_days = models.PositiveSmallIntegerField(
default=21,
help_text=_('Number of days a learner has to upgrade after content is made available')
)
opt_out = models.BooleanField(
default=False,
help_text=_('Disable the dynamic upgrade deadline for this course run.')
)
...@@ -15,18 +15,19 @@ from courseware.courses import get_course_date_blocks ...@@ -15,18 +15,19 @@ from courseware.courses import get_course_date_blocks
from courseware.date_summary import ( from courseware.date_summary import (
CourseEndDate, CourseEndDate,
CourseStartDate, CourseStartDate,
DateSummary,
TodaysDate, TodaysDate,
VerificationDeadlineDate, VerificationDeadlineDate,
VerifiedUpgradeDeadlineDate VerifiedUpgradeDeadlineDate
) )
from courseware.models import DynamicUpgradeDeadlineConfiguration, CourseDynamicUpgradeDeadlineConfiguration
from lms.djangoapps.verify_student.models import VerificationDeadline from lms.djangoapps.verify_student.models import VerificationDeadline
from lms.djangoapps.verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory from lms.djangoapps.verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from openedx.core.djangoapps.user_api.preferences.api import set_user_preference from openedx.core.djangoapps.user_api.preferences.api import set_user_preference
from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG
from student.tests.factories import CourseEnrollmentFactory, UserFactory from student.tests.factories import CourseEnrollmentFactory, UserFactory, TEST_PASSWORD
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
...@@ -56,12 +57,12 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -56,12 +57,12 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
): ):
"""Set up the course and user for this test.""" """Set up the course and user for this test."""
now = datetime.now(utc) now = datetime.now(utc)
# pylint: disable=attribute-defined-outside-init
if create_user: if create_user:
self.user = UserFactory.create(username='mrrobot', password='test') # pylint: disable=attribute-defined-outside-init self.user = UserFactory()
self.course = CourseFactory.create( # pylint: disable=attribute-defined-outside-init self.course = CourseFactory.create(start=now + timedelta(days=days_till_start))
start=now + timedelta(days=days_till_start)
)
if days_till_end is not None: if days_till_end is not None:
self.course.end = now + timedelta(days=days_till_end) self.course.end = now + timedelta(days=days_till_end)
...@@ -96,7 +97,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -96,7 +97,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
def test_course_info_feature_flag(self): def test_course_info_feature_flag(self):
SelfPacedConfiguration(enable_course_home_improvements=False).save() SelfPacedConfiguration(enable_course_home_improvements=False).save()
self.setup_course_and_user() self.setup_course_and_user()
self.client.login(username='mrrobot', password='test') self.client.login(username=self.user.username, password=TEST_PASSWORD)
url = reverse('info', args=(self.course.id,)) url = reverse('info', args=(self.course.id,))
response = self.client.get(url) response = self.client.get(url)
self.assertNotIn('date-summary', response.content) self.assertNotIn('date-summary', response.content)
...@@ -198,7 +199,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -198,7 +199,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
def test_todays_date_no_timezone(self, url_name): def test_todays_date_no_timezone(self, url_name):
with freeze_time('2015-01-02'): with freeze_time('2015-01-02'):
self.setup_course_and_user() self.setup_course_and_user()
self.client.login(username='mrrobot', password='test') self.client.login(username=self.user.username, password=TEST_PASSWORD)
html_elements = [ html_elements = [
'<h3 class="hd hd-6 handouts-header">Important Course Dates</h3>', '<h3 class="hd hd-6 handouts-header">Important Course Dates</h3>',
...@@ -209,7 +210,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -209,7 +210,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
'data-string="Today is {date}"', 'data-string="Today is {date}"',
'data-timezone="None"' 'data-timezone="None"'
] ]
url = reverse(url_name, args=(self.course.id, )) url = reverse(url_name, args=(self.course.id,))
response = self.client.get(url, follow=True) response = self.client.get(url, follow=True)
for html in html_elements: for html in html_elements:
self.assertContains(response, html) self.assertContains(response, html)
...@@ -222,7 +223,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -222,7 +223,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
def test_todays_date_timezone(self, url_name): def test_todays_date_timezone(self, url_name):
with freeze_time('2015-01-02'): with freeze_time('2015-01-02'):
self.setup_course_and_user() self.setup_course_and_user()
self.client.login(username='mrrobot', password='test') self.client.login(username=self.user.username, password=TEST_PASSWORD)
set_user_preference(self.user, "time_zone", "America/Los_Angeles") set_user_preference(self.user, "time_zone", "America/Los_Angeles")
url = reverse(url_name, args=(self.course.id,)) url = reverse(url_name, args=(self.course.id,))
response = self.client.get(url, follow=True) response = self.client.get(url, follow=True)
...@@ -253,7 +254,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -253,7 +254,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
def test_start_date_render(self, url_name): def test_start_date_render(self, url_name):
with freeze_time('2015-01-02'): with freeze_time('2015-01-02'):
self.setup_course_and_user() self.setup_course_and_user()
self.client.login(username='mrrobot', password='test') self.client.login(username=self.user.username, password=TEST_PASSWORD)
url = reverse(url_name, args=(self.course.id,)) url = reverse(url_name, args=(self.course.id,))
response = self.client.get(url, follow=True) response = self.client.get(url, follow=True)
html_elements = [ html_elements = [
...@@ -271,7 +272,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -271,7 +272,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
def test_start_date_render_time_zone(self, url_name): def test_start_date_render_time_zone(self, url_name):
with freeze_time('2015-01-02'): with freeze_time('2015-01-02'):
self.setup_course_and_user() self.setup_course_and_user()
self.client.login(username='mrrobot', password='test') self.client.login(username=self.user.username, password=TEST_PASSWORD)
set_user_preference(self.user, "time_zone", "America/Los_Angeles") set_user_preference(self.user, "time_zone", "America/Los_Angeles")
url = reverse(url_name, args=(self.course.id,)) url = reverse(url_name, args=(self.course.id,))
response = self.client.get(url, follow=True) response = self.client.get(url, follow=True)
...@@ -389,3 +390,62 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -389,3 +390,62 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
) )
block = VerificationDeadlineDate(self.course, self.user) block = VerificationDeadlineDate(self.course, self.user)
self.assertEqual(block.relative_datestring, expected_date_string) self.assertEqual(block.relative_datestring, expected_date_string)
def create_self_paced_course_run(self, **kwargs):
defaults = {
'enroll_user': False,
'days_till_upgrade_deadline': 100,
}
defaults.update(kwargs)
self.setup_course_and_user(**defaults)
self.course.self_paced = True
self.store.update_item(self.course, self.user.id)
overview = CourseOverview.get_from_id(self.course.id)
self.assertTrue(overview.self_paced)
def test_date_with_self_paced(self):
""" The date returned for self-paced course runs should be dependent on the learner's enrollment date. """
global_config = DynamicUpgradeDeadlineConfiguration.objects.create(enabled=True)
# Enrollments made before the course start should use the course start date as the content availability date
self.create_self_paced_course_run(days_till_start=3)
CourseEnrollmentFactory.create(course_id=self.course.id, user=self.user, mode=CourseMode.AUDIT)
block = VerifiedUpgradeDeadlineDate(self.course, self.user)
overview = CourseOverview.get_from_id(self.course.id)
expected = overview.start + timedelta(days=global_config.deadline_days)
self.assertEqual(block.date, expected)
# Enrollments made after the course start should use the enrollment date as the content availability date
self.create_self_paced_course_run(days_till_start=-1)
enrollment = CourseEnrollmentFactory.create(course_id=self.course.id, user=self.user, mode=CourseMode.AUDIT)
block = VerifiedUpgradeDeadlineDate(self.course, self.user)
expected = enrollment.created + timedelta(days=global_config.deadline_days)
self.assertEqual(block.date, expected)
# Courses should be able to override the deadline
course_config = CourseDynamicUpgradeDeadlineConfiguration.objects.create(
enabled=True, course_id=self.course.id, opt_out=False, deadline_days=3
)
block = VerifiedUpgradeDeadlineDate(self.course, self.user)
expected = enrollment.created + timedelta(days=course_config.deadline_days)
self.assertEqual(block.date, expected)
# Disabling the functionality should result in the verified mode's expiration date being returned.
global_config.enabled = False
global_config.save()
block = VerifiedUpgradeDeadlineDate(self.course, self.user)
expected = CourseMode.objects.get(course_id=self.course.id, mode_slug=CourseMode.VERIFIED).expiration_datetime
self.assertEqual(block.date, expected)
def test_date_with_self_paced_with_course_opt_out(self):
""" If the course run has opted out of the dynamic deadline, the course mode's deadline should be used. """
self.create_self_paced_course_run(days_till_start=-1)
DynamicUpgradeDeadlineConfiguration.objects.create(enabled=True)
CourseEnrollmentFactory.create(course_id=self.course.id, user=self.user, mode=CourseMode.AUDIT)
# Opt the course out of the dynamic upgrade deadline
CourseDynamicUpgradeDeadlineConfiguration.objects.create(enabled=True, course_id=self.course.id, opt_out=True)
block = VerifiedUpgradeDeadlineDate(self.course, self.user)
expected = CourseMode.objects.get(course_id=self.course.id, mode_slug=CourseMode.VERIFIED).expiration_datetime
self.assertEqual(block.date, expected)
...@@ -211,8 +211,8 @@ class IndexQueryTestCase(ModuleStoreTestCase): ...@@ -211,8 +211,8 @@ class IndexQueryTestCase(ModuleStoreTestCase):
NUM_PROBLEMS = 20 NUM_PROBLEMS = 20
@ddt.data( @ddt.data(
(ModuleStoreEnum.Type.mongo, 10, 143), (ModuleStoreEnum.Type.mongo, 10, 144),
(ModuleStoreEnum.Type.split, 4, 143), (ModuleStoreEnum.Type.split, 4, 144),
) )
@ddt.unpack @ddt.unpack
def test_index_query_counts(self, store_type, expected_mongo_query_count, expected_mysql_query_count): def test_index_query_counts(self, store_type, expected_mongo_query_count, expected_mysql_query_count):
......
...@@ -148,9 +148,9 @@ class RenderXBlockTestMixin(object): ...@@ -148,9 +148,9 @@ class RenderXBlockTestMixin(object):
return response return response
@ddt.data( @ddt.data(
('vertical_block', ModuleStoreEnum.Type.mongo, 10), ('vertical_block', ModuleStoreEnum.Type.mongo, 14),
('vertical_block', ModuleStoreEnum.Type.split, 6), ('vertical_block', ModuleStoreEnum.Type.split, 6),
('html_block', ModuleStoreEnum.Type.mongo, 11), ('html_block', ModuleStoreEnum.Type.mongo, 15),
('html_block', ModuleStoreEnum.Type.split, 6), ('html_block', ModuleStoreEnum.Type.split, 6),
) )
@ddt.unpack @ddt.unpack
......
...@@ -160,7 +160,7 @@ class TestCourseHomePage(CourseHomePageTestCase): ...@@ -160,7 +160,7 @@ class TestCourseHomePage(CourseHomePageTestCase):
course_home_url(self.course) course_home_url(self.course)
# Fetch the view and verify the query counts # Fetch the view and verify the query counts
with self.assertNumQueries(38, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST): with self.assertNumQueries(40, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
with check_mongo_calls(4): with check_mongo_calls(4):
url = course_home_url(self.course) url = course_home_url(self.course)
self.client.get(url) self.client.get(url)
......
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