Adds support for beta users to the course outline API
Adds days_early_for_beta to the ScheduleData class, and its loading and usage to ScheduleOutlineProcessor.
This commit is contained in:
committed by
Kshitij Sobti
parent
cb1635a60c
commit
17e69d7881
@@ -103,6 +103,7 @@ def get_course_outline(course_key: CourseKey) -> CourseOutlineData:
|
||||
title=course_context.learning_context.title,
|
||||
published_at=course_context.learning_context.published_at,
|
||||
published_version=course_context.learning_context.published_version,
|
||||
days_early_for_beta=course_context.days_early_for_beta,
|
||||
sections=sections_data,
|
||||
self_paced=course_context.self_paced,
|
||||
course_visibility=CourseVisibility(course_context.course_visibility),
|
||||
@@ -220,8 +221,14 @@ def _get_user_course_outline_and_processors(course_key: CourseKey,
|
||||
**{
|
||||
name: getattr(trimmed_course_outline, name)
|
||||
for name in [
|
||||
'course_key', 'title', 'published_at', 'published_version',
|
||||
'sections', 'self_paced', 'course_visibility'
|
||||
'course_key',
|
||||
'title',
|
||||
'published_at',
|
||||
'published_version',
|
||||
'sections',
|
||||
'self_paced',
|
||||
'course_visibility',
|
||||
'days_early_for_beta',
|
||||
]
|
||||
}
|
||||
)
|
||||
@@ -270,6 +277,7 @@ def _update_course_context(course_outline: CourseOutlineData):
|
||||
learning_context=learning_context,
|
||||
defaults={
|
||||
'course_visibility': course_outline.course_visibility.value,
|
||||
'days_early_for_beta': course_outline.days_early_for_beta,
|
||||
'self_paced': course_outline.self_paced,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import logging
|
||||
from collections import defaultdict, OrderedDict
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from edx_when.api import get_dates_for_course
|
||||
from opaque_keys.edx.keys import CourseKey, UsageKey
|
||||
from student.auth import user_has_role
|
||||
from student.roles import CourseBetaTesterRole
|
||||
|
||||
from ...data import ScheduleData, ScheduleItemData, UserCourseOutlineData
|
||||
from .base import OutlineProcessor
|
||||
@@ -36,6 +38,7 @@ class ScheduleOutlineProcessor(OutlineProcessor):
|
||||
self.keys_to_schedule_fields = defaultdict(dict)
|
||||
self._course_start = None
|
||||
self._course_end = None
|
||||
self._is_beta_tester = False
|
||||
|
||||
def load_data(self):
|
||||
"""Pull dates information from edx-when."""
|
||||
@@ -49,6 +52,7 @@ class ScheduleOutlineProcessor(OutlineProcessor):
|
||||
course_usage_key = self.course_key.make_usage_key('course', 'course')
|
||||
self._course_start = self.keys_to_schedule_fields[course_usage_key].get('start')
|
||||
self._course_end = self.keys_to_schedule_fields[course_usage_key].get('end')
|
||||
self._is_beta_tester = user_has_role(self.user, CourseBetaTesterRole(self.course_key))
|
||||
|
||||
def inaccessible_sequences(self, full_course_outline):
|
||||
"""
|
||||
@@ -57,8 +61,13 @@ class ScheduleOutlineProcessor(OutlineProcessor):
|
||||
Sequences are inaccessible, regardless of the individual Sequence start
|
||||
dates.
|
||||
"""
|
||||
if self._is_beta_tester and full_course_outline.days_early_for_beta is not None:
|
||||
start_offset = timedelta(days=full_course_outline.days_early_for_beta)
|
||||
else:
|
||||
start_offset = timedelta(days=0)
|
||||
|
||||
# If the course hasn't started at all, then everything is inaccessible.
|
||||
if self._course_start is None or self.at_time < self._course_start:
|
||||
if self._course_start is None or self.at_time < self._course_start - start_offset:
|
||||
return set(full_course_outline.sequences)
|
||||
|
||||
self_paced = full_course_outline.self_paced
|
||||
@@ -66,6 +75,8 @@ class ScheduleOutlineProcessor(OutlineProcessor):
|
||||
inaccessible = set()
|
||||
for section in full_course_outline.sections:
|
||||
section_start = self.keys_to_schedule_fields[section.usage_key].get('start')
|
||||
if section_start is not None:
|
||||
section_start -= start_offset
|
||||
if section_start and self.at_time < section_start:
|
||||
# If the section hasn't started yet, all the sequences it
|
||||
# contains are inaccessible, regardless of the start value for
|
||||
@@ -74,6 +85,8 @@ class ScheduleOutlineProcessor(OutlineProcessor):
|
||||
else:
|
||||
for seq in section.sequences:
|
||||
seq_start = self.keys_to_schedule_fields[seq.usage_key].get('start')
|
||||
if seq_start is not None:
|
||||
seq_start -= start_offset
|
||||
if seq_start and self.at_time < seq_start:
|
||||
inaccessible.add(seq.usage_key)
|
||||
continue
|
||||
@@ -110,12 +123,22 @@ class ScheduleOutlineProcessor(OutlineProcessor):
|
||||
course_usage_key = self.course_key.make_usage_key('course', 'course')
|
||||
course_start = self.keys_to_schedule_fields[course_usage_key].get('start')
|
||||
course_end = self.keys_to_schedule_fields[course_usage_key].get('end')
|
||||
days_early_for_beta = pruned_course_outline.days_early_for_beta
|
||||
|
||||
if days_early_for_beta is not None and self._is_beta_tester:
|
||||
start_offset = timedelta(days=days_early_for_beta)
|
||||
else:
|
||||
start_offset = timedelta(days=0)
|
||||
if course_start is not None:
|
||||
course_start -= start_offset
|
||||
|
||||
sequences = {}
|
||||
sections = {}
|
||||
for section in pruned_course_outline.sections:
|
||||
section_dict = self.keys_to_schedule_fields[section.usage_key]
|
||||
section_start = section_dict.get('start')
|
||||
if section_start is not None:
|
||||
section_start -= start_offset
|
||||
section_effective_start = _effective_start(course_start, section_start)
|
||||
section_due = section_dict.get('due')
|
||||
|
||||
@@ -129,6 +152,8 @@ class ScheduleOutlineProcessor(OutlineProcessor):
|
||||
for seq in section.sequences:
|
||||
seq_dict = self.keys_to_schedule_fields[seq.usage_key]
|
||||
seq_start = seq_dict.get('start')
|
||||
if seq_start is not None:
|
||||
seq_start -= start_offset
|
||||
seq_due = seq_dict.get('due')
|
||||
sequences[seq.usage_key] = ScheduleItemData(
|
||||
usage_key=seq.usage_key,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from datetime import datetime, timezone
|
||||
from unittest import TestCase
|
||||
|
||||
import pytest
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
import attr
|
||||
|
||||
@@ -31,6 +32,7 @@ class TestCourseOutlineData(TestCase):
|
||||
title="Exciting Test Course!",
|
||||
published_at=datetime(2020, 5, 19, tzinfo=timezone.utc),
|
||||
published_version="5ebece4b69dd593d82fe2014",
|
||||
days_early_for_beta=None,
|
||||
sections=generate_sections(cls.course_key, [3, 2]),
|
||||
self_paced=False,
|
||||
course_visibility=CourseVisibility.PRIVATE
|
||||
@@ -103,6 +105,26 @@ class TestCourseOutlineData(TestCase):
|
||||
new_outline = self.course_outline.remove({seq_key_to_remove})
|
||||
assert new_outline == self.course_outline
|
||||
|
||||
def test_days_early_for_beta(self):
|
||||
"""
|
||||
Check that days_early_for_beta exists, can be set, and validates correctly.
|
||||
"""
|
||||
assert self.course_outline.days_early_for_beta is None
|
||||
new_outline = attr.evolve(
|
||||
self.course_outline,
|
||||
days_early_for_beta=5
|
||||
)
|
||||
assert new_outline is not None
|
||||
assert new_outline != self.course_outline
|
||||
assert new_outline.days_early_for_beta == 5
|
||||
|
||||
with pytest.raises(ValueError) as error:
|
||||
attr.evolve(self.course_outline, days_early_for_beta=-1)
|
||||
assert error.match(
|
||||
"Provided value -1 for days_early_for_beta is invalid. The value must be positive or zero. "
|
||||
"A positive value will shift back the starting date for Beta users by that many days."
|
||||
)
|
||||
|
||||
|
||||
def generate_sections(course_key, num_sequences):
|
||||
"""
|
||||
|
||||
@@ -10,9 +10,13 @@ from edx_when.api import set_dates_for_course
|
||||
from opaque_keys.edx.keys import CourseKey, UsageKey
|
||||
from opaque_keys.edx.locator import BlockUsageLocator
|
||||
|
||||
from lms.djangoapps.courseware.tests.factories import BetaTesterFactory
|
||||
from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
|
||||
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
|
||||
from openedx.features.course_experience import COURSE_ENABLE_UNENROLLED_ACCESS_FLAG
|
||||
from student.auth import user_has_role
|
||||
from student.models import CourseEnrollment
|
||||
from student.roles import CourseBetaTesterRole
|
||||
|
||||
from ...data import (
|
||||
CourseLearningSequenceData, CourseOutlineData, CourseSectionData, VisibilityData, CourseVisibility
|
||||
@@ -42,6 +46,7 @@ class CourseOutlineTestCase(CacheIsolationTestCase):
|
||||
title="Roundtrip Test Course!",
|
||||
published_at=datetime(2020, 5, 20, tzinfo=timezone.utc),
|
||||
published_version="5ebece4b69dd593d82fe2015",
|
||||
days_early_for_beta=None,
|
||||
sections=generate_sections(cls.course_key, [2, 2]),
|
||||
self_paced=False,
|
||||
course_visibility=CourseVisibility.PRIVATE
|
||||
@@ -117,6 +122,7 @@ class UserCourseOutlineTestCase(CacheIsolationTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
course_key = CourseKey.from_string("course-v1:OpenEdX+Outline+T1")
|
||||
# Users...
|
||||
cls.global_staff = User.objects.create_user(
|
||||
'global_staff', email='gstaff@example.com', is_staff=True
|
||||
@@ -124,15 +130,17 @@ class UserCourseOutlineTestCase(CacheIsolationTestCase):
|
||||
cls.student = User.objects.create_user(
|
||||
'student', email='student@example.com', is_staff=False
|
||||
)
|
||||
cls.beta_tester = BetaTesterFactory(course_key=course_key)
|
||||
cls.anonymous_user = AnonymousUser()
|
||||
|
||||
# Seed with data
|
||||
cls.course_key = CourseKey.from_string("course-v1:OpenEdX+Outline+T1")
|
||||
cls.course_key = course_key
|
||||
cls.simple_outline = CourseOutlineData(
|
||||
course_key=cls.course_key,
|
||||
title="User Outline Test Course!",
|
||||
published_at=datetime(2020, 5, 20, tzinfo=timezone.utc),
|
||||
published_version="5ebece4b69dd593d82fe2020",
|
||||
days_early_for_beta=None,
|
||||
sections=generate_sections(cls.course_key, [2, 1, 3]),
|
||||
self_paced=False,
|
||||
course_visibility=CourseVisibility.PRIVATE
|
||||
@@ -140,20 +148,31 @@ class UserCourseOutlineTestCase(CacheIsolationTestCase):
|
||||
replace_course_outline(cls.simple_outline)
|
||||
|
||||
# Enroll student in the course
|
||||
cls.student.courseenrollment_set.create(course_id=cls.course_key, is_active=True, mode="audit")
|
||||
CourseEnrollment.enroll(user=cls.student, course_key=cls.course_key, mode="audit")
|
||||
# Enroll beta tester in the course
|
||||
CourseEnrollment.enroll(user=cls.beta_tester, course_key=cls.course_key, mode="audit")
|
||||
|
||||
def test_simple_outline(self):
|
||||
"""This outline is the same for everyone."""
|
||||
at_time = datetime(2020, 5, 21, tzinfo=timezone.utc)
|
||||
beta_tester_outline = get_user_course_outline(
|
||||
self.course_key, self.beta_tester, at_time
|
||||
)
|
||||
student_outline = get_user_course_outline(
|
||||
self.course_key, self.student, at_time
|
||||
)
|
||||
global_staff_outline = get_user_course_outline(
|
||||
self.course_key, self.global_staff, at_time
|
||||
)
|
||||
assert beta_tester_outline.sections == global_staff_outline.sections
|
||||
assert student_outline.sections == global_staff_outline.sections
|
||||
assert student_outline.at_time == at_time
|
||||
|
||||
beta_tester_outline_details = get_user_course_outline_details(
|
||||
self.course_key, self.beta_tester, at_time
|
||||
)
|
||||
assert beta_tester_outline_details.outline == beta_tester_outline
|
||||
|
||||
student_outline_details = get_user_course_outline_details(
|
||||
self.course_key, self.student, at_time
|
||||
)
|
||||
@@ -177,6 +196,7 @@ class ScheduleTestCase(CacheIsolationTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
course_key = CourseKey.from_string("course-v1:OpenEdX+Outline+T1")
|
||||
# Users...
|
||||
cls.global_staff = User.objects.create_user(
|
||||
'global_staff', email='gstaff@example.com', is_staff=True
|
||||
@@ -184,9 +204,10 @@ class ScheduleTestCase(CacheIsolationTestCase):
|
||||
cls.student = User.objects.create_user(
|
||||
'student', email='student@example.com', is_staff=False
|
||||
)
|
||||
cls.beta_tester = BetaTesterFactory(course_key=course_key)
|
||||
cls.anonymous_user = AnonymousUser()
|
||||
|
||||
cls.course_key = CourseKey.from_string("course-v1:OpenEdX+Outline+T1")
|
||||
cls.course_key = course_key
|
||||
|
||||
# The UsageKeys we're going to set up for date tests.
|
||||
cls.section_key = cls.course_key.make_usage_key('chapter', 'ch1')
|
||||
@@ -256,6 +277,7 @@ class ScheduleTestCase(CacheIsolationTestCase):
|
||||
title="User Outline Test Course!",
|
||||
published_at=datetime(2020, 5, 20, tzinfo=timezone.utc),
|
||||
published_version="5ebece4b69dd593d82fe2020",
|
||||
days_early_for_beta=None,
|
||||
course_visibility=CourseVisibility.PRIVATE,
|
||||
sections=[
|
||||
CourseSectionData(
|
||||
@@ -291,17 +313,22 @@ class ScheduleTestCase(CacheIsolationTestCase):
|
||||
]
|
||||
)
|
||||
],
|
||||
self_paced=False
|
||||
self_paced=False,
|
||||
)
|
||||
replace_course_outline(cls.outline)
|
||||
|
||||
# Enroll student in the course
|
||||
cls.student.courseenrollment_set.create(course_id=cls.course_key, is_active=True, mode="audit")
|
||||
# Enroll beta tester in the course
|
||||
cls.beta_tester.courseenrollment_set.create(course_id=cls.course_key, is_active=True, mode="audit")
|
||||
assert user_has_role(cls.beta_tester, CourseBetaTesterRole(cls.course_key))
|
||||
assert cls.outline.days_early_for_beta is None
|
||||
|
||||
def get_details(self, at_time):
|
||||
staff_details = get_user_course_outline_details(self.course_key, self.global_staff, at_time)
|
||||
student_details = get_user_course_outline_details(self.course_key, self.student, at_time)
|
||||
return staff_details, student_details
|
||||
beta_tester_details = get_user_course_outline_details(self.course_key, self.beta_tester, at_time)
|
||||
return staff_details, student_details, beta_tester_details
|
||||
|
||||
def get_sequence_keys(self, exclude=None):
|
||||
if exclude is None:
|
||||
@@ -311,20 +338,43 @@ class ScheduleTestCase(CacheIsolationTestCase):
|
||||
return [key for key in self.all_seq_keys if key not in exclude]
|
||||
|
||||
def test_before_course_starts(self):
|
||||
staff_details, student_details = self.get_details(
|
||||
staff_details, student_details, beta_tester_details = self.get_details(
|
||||
datetime(2020, 5, 9, tzinfo=timezone.utc)
|
||||
)
|
||||
# Staff can always access all sequences
|
||||
assert len(staff_details.outline.accessible_sequences) == 5
|
||||
# Student can access nothing
|
||||
assert len(student_details.outline.accessible_sequences) == 0
|
||||
# Beta tester can access nothing
|
||||
assert len(beta_tester_details.outline.accessible_sequences) == 0
|
||||
|
||||
# Everyone can see everything
|
||||
assert len(staff_details.outline.sequences) == 5
|
||||
assert len(student_details.outline.sequences) == 5
|
||||
assert len(beta_tester_details.outline.sequences) == 5
|
||||
|
||||
def test_course_beta_access(self):
|
||||
course_outline = attr.evolve(self.outline, days_early_for_beta=6)
|
||||
assert course_outline.days_early_for_beta is not None
|
||||
replace_course_outline(course_outline)
|
||||
|
||||
staff_details, student_details, beta_tester_details = self.get_details(
|
||||
datetime(2020, 5, 9, tzinfo=timezone.utc)
|
||||
)
|
||||
# Staff can always access all sequences
|
||||
assert len(staff_details.outline.accessible_sequences) == 5
|
||||
# Student can access nothing
|
||||
assert len(student_details.outline.accessible_sequences) == 0
|
||||
# Beta tester can access some
|
||||
assert len(beta_tester_details.outline.accessible_sequences) == 4
|
||||
|
||||
# Everyone can see everything
|
||||
assert len(staff_details.outline.sequences) == 5
|
||||
assert len(student_details.outline.sequences) == 5
|
||||
assert len(beta_tester_details.outline.sequences) == 5
|
||||
|
||||
def test_before_section_starts(self):
|
||||
staff_details, student_details = self.get_details(
|
||||
staff_details, student_details, beta_tester_details = self.get_details(
|
||||
datetime(2020, 5, 14, tzinfo=timezone.utc)
|
||||
)
|
||||
# Staff can always access all sequences
|
||||
@@ -338,8 +388,33 @@ class ScheduleTestCase(CacheIsolationTestCase):
|
||||
assert before_seq_sched_item_data.start == datetime(2020, 5, 14, tzinfo=timezone.utc)
|
||||
assert before_seq_sched_item_data.effective_start == datetime(2020, 5, 15, tzinfo=timezone.utc)
|
||||
|
||||
# Beta tester can access nothing
|
||||
assert len(beta_tester_details.outline.accessible_sequences) == 0
|
||||
|
||||
def test_section_beta_access(self):
|
||||
course_outline = attr.evolve(self.outline, days_early_for_beta=1)
|
||||
assert course_outline.days_early_for_beta is not None
|
||||
replace_course_outline(course_outline)
|
||||
|
||||
staff_details, student_details, beta_tester_details = self.get_details(
|
||||
datetime(2020, 5, 14, tzinfo=timezone.utc)
|
||||
)
|
||||
# Staff can always access all sequences
|
||||
assert len(staff_details.outline.accessible_sequences) == 5
|
||||
|
||||
# Student can access nothing -- even though one of the sequences is set
|
||||
# to start on 2020-05-14, it's not available because the section hasn't
|
||||
# started yet.
|
||||
assert len(student_details.outline.accessible_sequences) == 0
|
||||
before_seq_sched_item_data = student_details.schedule.sequences[self.seq_before_key]
|
||||
assert before_seq_sched_item_data.start == datetime(2020, 5, 14, tzinfo=timezone.utc)
|
||||
assert before_seq_sched_item_data.effective_start == datetime(2020, 5, 15, tzinfo=timezone.utc)
|
||||
|
||||
# Beta tester can access some
|
||||
assert len(beta_tester_details.outline.accessible_sequences) == 4
|
||||
|
||||
def test_at_section_start(self):
|
||||
staff_details, student_details = self.get_details(
|
||||
staff_details, student_details, beta_tester_details = self.get_details(
|
||||
datetime(2020, 5, 15, tzinfo=timezone.utc)
|
||||
)
|
||||
# Staff can always access all sequences
|
||||
@@ -352,8 +427,32 @@ class ScheduleTestCase(CacheIsolationTestCase):
|
||||
for key in self.get_sequence_keys(exclude=[self.seq_after_key]):
|
||||
assert key in student_details.outline.accessible_sequences
|
||||
|
||||
# Beta tester can access same as student
|
||||
assert len(beta_tester_details.outline.accessible_sequences) == 4
|
||||
|
||||
def test_at_beta_section_start(self):
|
||||
course_outline = attr.evolve(self.outline, days_early_for_beta=1)
|
||||
assert course_outline.days_early_for_beta is not None
|
||||
replace_course_outline(course_outline)
|
||||
|
||||
staff_details, student_details, beta_tester_details = self.get_details(
|
||||
datetime(2020, 5, 15, tzinfo=timezone.utc)
|
||||
)
|
||||
# Staff can always access all sequences
|
||||
assert len(staff_details.outline.accessible_sequences) == 5
|
||||
|
||||
# Student can access all sequences except the one that starts after this
|
||||
# datetime (self.seq_after_key)
|
||||
assert len(student_details.outline.accessible_sequences) == 4
|
||||
assert self.seq_after_key not in student_details.outline.accessible_sequences
|
||||
for key in self.get_sequence_keys(exclude=[self.seq_after_key]):
|
||||
assert key in student_details.outline.accessible_sequences
|
||||
|
||||
# Beta tester can access all
|
||||
assert len(beta_tester_details.outline.accessible_sequences) == 5
|
||||
|
||||
def test_is_due_and_before_due(self):
|
||||
staff_details, student_details = self.get_details(
|
||||
staff_details, student_details, beta_tester_details = self.get_details(
|
||||
datetime(2020, 5, 16, tzinfo=timezone.utc)
|
||||
)
|
||||
# Staff can always access all sequences
|
||||
@@ -367,8 +466,11 @@ class ScheduleTestCase(CacheIsolationTestCase):
|
||||
seq_due_sched_item_data = student_details.schedule.sequences[self.seq_due_key]
|
||||
assert seq_due_sched_item_data.due == datetime(2020, 5, 20, tzinfo=timezone.utc)
|
||||
|
||||
# Beta tester can access some
|
||||
assert len(beta_tester_details.outline.accessible_sequences) == 5
|
||||
|
||||
def test_is_due_and_after_due(self):
|
||||
staff_details, student_details = self.get_details(
|
||||
staff_details, student_details, beta_tester_details = self.get_details(
|
||||
datetime(2020, 5, 21, tzinfo=timezone.utc)
|
||||
)
|
||||
# Staff can always access all sequences
|
||||
@@ -382,6 +484,9 @@ class ScheduleTestCase(CacheIsolationTestCase):
|
||||
for key in self.get_sequence_keys(exclude=[self.seq_due_key]):
|
||||
assert key in student_details.outline.accessible_sequences
|
||||
|
||||
# Beta tester can access same as student
|
||||
assert len(beta_tester_details.outline.accessible_sequences) == 4
|
||||
|
||||
|
||||
class SelfPacedCourseOutlineTestCase(CacheIsolationTestCase):
|
||||
@classmethod
|
||||
@@ -439,6 +544,7 @@ class SelfPacedCourseOutlineTestCase(CacheIsolationTestCase):
|
||||
title="User Outline Test Course!",
|
||||
published_at=datetime(2020, 5, 20, tzinfo=timezone.utc),
|
||||
published_version="5ebece4b69dd593d82fe2020",
|
||||
days_early_for_beta=None,
|
||||
course_visibility=CourseVisibility.PRIVATE,
|
||||
sections=[
|
||||
CourseSectionData(
|
||||
@@ -528,6 +634,7 @@ class VisbilityTestCase(CacheIsolationTestCase):
|
||||
title="User Outline Test Course!",
|
||||
published_at=datetime(2020, 5, 20, tzinfo=timezone.utc),
|
||||
published_version="5ebece4b69dd593d82fe2020",
|
||||
days_early_for_beta=None,
|
||||
course_visibility=CourseVisibility.PRIVATE,
|
||||
sections=[
|
||||
CourseSectionData(
|
||||
@@ -610,6 +717,7 @@ class SequentialVisibilityTestCase(CacheIsolationTestCase):
|
||||
title="User Outline Test Course!",
|
||||
published_at=datetime(2020, 5, 20, tzinfo=timezone.utc),
|
||||
published_version="5ebece4b69dd593d82fe2020",
|
||||
days_early_for_beta=None,
|
||||
sections=generate_sections(cls.course_key, [2, 1, 3]),
|
||||
self_paced=False,
|
||||
course_visibility=CourseVisibility.PRIVATE
|
||||
|
||||
@@ -132,6 +132,10 @@ class CourseOutlineData:
|
||||
# course is modified.
|
||||
published_version = attr.ib(type=str)
|
||||
|
||||
# The time period (in days) before the official start of the course during which
|
||||
# beta testers have access to the course.
|
||||
days_early_for_beta = attr.ib(type=Optional[int])
|
||||
|
||||
sections = attr.ib(type=List[CourseSectionData])
|
||||
|
||||
# Defines if course self-paced or instructor-paced.
|
||||
@@ -199,6 +203,17 @@ class CourseOutlineData:
|
||||
]
|
||||
)
|
||||
|
||||
@days_early_for_beta.validator
|
||||
def validate_days_early_for_beta(self, attribute, value):
|
||||
"""
|
||||
Ensure that days_early_for_beta isn't negative.
|
||||
"""
|
||||
if value is not None and value < 0:
|
||||
raise ValueError(
|
||||
"Provided value {} for days_early_for_beta is invalid. The value must be positive or zero. "
|
||||
"A positive value will shift back the starting date for Beta users by that many days.".format(value)
|
||||
)
|
||||
|
||||
|
||||
@attr.s(frozen=True)
|
||||
class ScheduleItemData:
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.2.15 on 2020-09-01 07:04
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('learning_sequences', '0004_coursecontext_self_paced'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='coursecontext',
|
||||
name='days_early_for_beta',
|
||||
field=models.IntegerField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -80,6 +80,7 @@ class CourseContext(TimeStampedModel):
|
||||
course_visibility = models.CharField(
|
||||
max_length=32, choices=[(constant.value, constant.value) for constant in CourseVisibility]
|
||||
)
|
||||
days_early_for_beta = models.IntegerField(null=True, blank=True)
|
||||
self_paced = models.BooleanField(default=False)
|
||||
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ def get_outline_from_modulestore(course_key):
|
||||
title=course.display_name,
|
||||
published_at=course.subtree_edited_on,
|
||||
published_version=str(course.course_version), # .course_version is a BSON obj
|
||||
days_early_for_beta=course.days_early_for_beta,
|
||||
sections=sections_data,
|
||||
self_paced=course.self_paced,
|
||||
course_visibility=CourseVisibility(course.course_visibility),
|
||||
|
||||
@@ -44,6 +44,7 @@ class CourseOutlineViewTest(CacheIsolationTestCase, APITestCase):
|
||||
title="Views Test Course!",
|
||||
published_at=datetime(2020, 5, 20, tzinfo=timezone.utc),
|
||||
published_version="5ebece4b69dd593d82fe2020",
|
||||
days_early_for_beta=None,
|
||||
sections=generate_sections(cls.course_key, [2, 2]),
|
||||
self_paced=False,
|
||||
course_visibility=CourseVisibility.PUBLIC
|
||||
|
||||
@@ -70,6 +70,7 @@ class CourseOutlineView(APIView):
|
||||
"title": user_course_outline.title,
|
||||
"published_at": user_course_outline.published_at,
|
||||
"published_version": user_course_outline.published_version,
|
||||
"days_early_for_beta": user_course_outline.days_early_for_beta,
|
||||
"self_paced": user_course_outline.self_paced,
|
||||
|
||||
# Who and when this request was generated for (we can eventually
|
||||
|
||||
Reference in New Issue
Block a user