Skip to content
Snippets Groups Projects
Unverified Commit bf306ec4 authored by Demid's avatar Demid Committed by GitHub
Browse files

Support self-paced courses in learning_sequences app

The main difference between how instructor-paced and self-paced courses
work with respect to the outline on the backend is how we treat the
hide_after_due attribute on subsections (sequences). Namely, self-paced
courses ignore due dates even if they are specified on the sequence
(for example, by OLX import). If hide_after_due is True in a self-paced
course sequence, we only make it inaccessible after the entire course
ends.

This was tracked as BD-29 and TNL-7262.
parent 71191dd3
No related branches found
Tags release-2020-08-27-14.14
No related merge requests found
Showing
with 155 additions and 13 deletions
......@@ -104,6 +104,7 @@ def get_course_outline(course_key: CourseKey) -> CourseOutlineData:
published_at=course_context.learning_context.published_at,
published_version=course_context.learning_context.published_version,
sections=sections_data,
self_paced=course_context.self_paced,
course_visibility=CourseVisibility(course_context.course_visibility),
)
TieredCache.set_all_tiers(cache_key, outline_data, 300)
......@@ -218,7 +219,10 @@ def _get_user_course_outline_and_processors(course_key: CourseKey,
accessible_sequences=accessible_sequences,
**{
name: getattr(trimmed_course_outline, name)
for name in ['course_key', 'title', 'published_at', 'published_version', 'sections', 'course_visibility']
for name in [
'course_key', 'title', 'published_at', 'published_version',
'sections', 'self_paced', 'course_visibility'
]
}
)
......@@ -266,6 +270,7 @@ def _update_course_context(course_outline: CourseOutlineData):
learning_context=learning_context,
defaults={
'course_visibility': course_outline.course_visibility.value,
'self_paced': course_outline.self_paced,
}
)
if created:
......
......@@ -27,7 +27,6 @@ class ScheduleOutlineProcessor(OutlineProcessor):
Things we don't handle yet:
* Beta test users
* Self-paced courses
* Things that are made inaccessible after they're due.
"""
......@@ -62,6 +61,8 @@ class ScheduleOutlineProcessor(OutlineProcessor):
if self._course_start is None or self.at_time < self._course_start:
return set(full_course_outline.sequences)
self_paced = full_course_outline.self_paced
inaccessible = set()
for section in full_course_outline.sections:
section_start = self.keys_to_schedule_fields[section.usage_key].get('start')
......@@ -77,7 +78,13 @@ class ScheduleOutlineProcessor(OutlineProcessor):
inaccessible.add(seq.usage_key)
continue
seq_due = self.keys_to_schedule_fields[seq.usage_key].get('due')
# if course is self-paced, all sequences with enabled hide after due
# must have due date equal to course's end date
if self_paced:
seq_due = self._course_end
else:
seq_due = self.keys_to_schedule_fields[seq.usage_key].get('due')
if seq.inaccessible_after_due:
if seq_due and self.at_time > seq_due:
inaccessible.add(seq.usage_key)
......
......@@ -32,6 +32,7 @@ class TestCourseOutlineData(TestCase):
published_at=datetime(2020, 5, 19, tzinfo=timezone.utc),
published_version="5ebece4b69dd593d82fe2014",
sections=generate_sections(cls.course_key, [3, 2]),
self_paced=False,
course_visibility=CourseVisibility.PRIVATE
)
......
......@@ -43,6 +43,7 @@ class CourseOutlineTestCase(CacheIsolationTestCase):
published_at=datetime(2020, 5, 20, tzinfo=timezone.utc),
published_version="5ebece4b69dd593d82fe2015",
sections=generate_sections(cls.course_key, [2, 2]),
self_paced=False,
course_visibility=CourseVisibility.PRIVATE
)
......@@ -133,6 +134,7 @@ class UserCourseOutlineTestCase(CacheIsolationTestCase):
published_at=datetime(2020, 5, 20, tzinfo=timezone.utc),
published_version="5ebece4b69dd593d82fe2020",
sections=generate_sections(cls.course_key, [2, 1, 3]),
self_paced=False,
course_visibility=CourseVisibility.PRIVATE
)
replace_course_outline(cls.simple_outline)
......@@ -288,7 +290,8 @@ class ScheduleTestCase(CacheIsolationTestCase):
),
]
)
]
],
self_paced=False
)
replace_course_outline(cls.outline)
......@@ -380,6 +383,105 @@ class ScheduleTestCase(CacheIsolationTestCase):
assert key in student_details.outline.accessible_sequences
class SelfPacedCourseOutlineTestCase(CacheIsolationTestCase):
@classmethod
def setUpTestData(cls):
# Users...
cls.global_staff = User.objects.create_user(
'global_staff', email='gstaff@example.com', is_staff=True
)
cls.student = User.objects.create_user(
'student', email='student@example.com', is_staff=False
)
cls.course_key = CourseKey.from_string("course-v1:OpenEdX+Outline+T1")
# The UsageKeys we're going to set up for date tests.
cls.section_key = cls.course_key.make_usage_key('chapter', 'ch1')
# Sequence with due date
cls.seq_due_key = cls.course_key.make_usage_key('sequential', 'seq')
# Sequence with due date and "inaccessible after due" enabled
cls.seq_hide_after_due_key = cls.course_key.make_usage_key('sequential', 'seq_hide_after_due_key')
# Set scheduling information into edx-when for a single Section with
# two sequences with due date
set_dates_for_course(
cls.course_key,
[
(
cls.course_key.make_usage_key('course', 'course'),
{
'start': datetime(2020, 5, 10, tzinfo=timezone.utc),
}
),
(
cls.section_key,
{'start': datetime(2020, 5, 15, tzinfo=timezone.utc)}
),
(
cls.seq_due_key,
{'due': datetime(2020, 5, 21, tzinfo=timezone.utc)}
),
(
cls.seq_hide_after_due_key,
{'due': datetime(2020, 5, 21, tzinfo=timezone.utc)}
),
]
)
visibility = VisibilityData(
hide_from_toc=False,
visible_to_staff_only=False
)
cls.outline = CourseOutlineData(
course_key=cls.course_key,
title="User Outline Test Course!",
published_at=datetime(2020, 5, 20, tzinfo=timezone.utc),
published_version="5ebece4b69dd593d82fe2020",
course_visibility=CourseVisibility.PRIVATE,
sections=[
CourseSectionData(
usage_key=cls.section_key,
title="Section",
visibility=visibility,
sequences=[
CourseLearningSequenceData(
usage_key=cls.seq_due_key,
title='Due',
visibility=visibility
),
CourseLearningSequenceData(
usage_key=cls.seq_hide_after_due_key,
title='Inaccessible after due',
visibility=visibility,
inaccessible_after_due=True
),
],
),
],
self_paced=True,
)
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")
def test_sequences_accessible_after_due(self):
at_time = datetime(2020, 5, 22, tzinfo=timezone.utc)
staff_outline = get_user_course_outline_details(self.course_key, self.global_staff, at_time).outline
student_outline = get_user_course_outline_details(self.course_key, self.student, at_time).outline
# Staff can always access all sequences
assert len(staff_outline.accessible_sequences) == 2
# In self-paced course, due date of sequences equals to due date of
# course, so here student should see all sequences, even if their
# due dates explicitly were set before end of course
assert len(student_outline.accessible_sequences) == 2
class VisbilityTestCase(CacheIsolationTestCase):
"""
Visibility-related tests.
......@@ -455,7 +557,8 @@ class VisbilityTestCase(CacheIsolationTestCase):
]
)
]
],
self_paced=False
)
replace_course_outline(cls.outline)
......@@ -464,17 +567,17 @@ class VisbilityTestCase(CacheIsolationTestCase):
def test_visibility(self):
at_time = datetime(2020, 5, 21, tzinfo=timezone.utc) # Exact value doesn't matter
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)
staff_outline = get_user_course_outline_details(self.course_key, self.global_staff, at_time).outline
student_outline = get_user_course_outline_details(self.course_key, self.student, at_time).outline
# Sections visible
assert len(staff_details.outline.sections) == 2
assert len(student_details.outline.sections) == 1
assert len(staff_outline.sections) == 2
assert len(student_outline.sections) == 1
# Sequences visible
assert len(staff_details.outline.sequences) == 4
assert len(student_details.outline.sequences) == 1
assert self.normal_in_normal_key in student_details.outline.sequences
assert len(staff_outline.sequences) == 4
assert len(student_outline.sequences) == 1
assert self.normal_in_normal_key in student_outline.sequences
class SequentialVisibilityTestCase(CacheIsolationTestCase):
......@@ -508,6 +611,7 @@ class SequentialVisibilityTestCase(CacheIsolationTestCase):
published_at=datetime(2020, 5, 20, tzinfo=timezone.utc),
published_version="5ebece4b69dd593d82fe2020",
sections=generate_sections(cls.course_key, [2, 1, 3]),
self_paced=False,
course_visibility=CourseVisibility.PRIVATE
)
replace_course_outline(cls.course_outline)
......
......@@ -134,6 +134,9 @@ class CourseOutlineData:
sections = attr.ib(type=List[CourseSectionData])
# Defines if course self-paced or instructor-paced.
self_paced = attr.ib(type=bool)
# To make sure that our data structure is consistent, this field is
# derived from what you pass into `sections`. Do not set this directly.
sequences = attr.ib(type=Dict[UsageKey, CourseLearningSequenceData], init=False)
......
# Generated by Django 2.2.14 on 2020-08-02 23:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('learning_sequences', '0003_create_course_context_for_course_specific_models'),
]
operations = [
migrations.AddField(
model_name='coursecontext',
name='self_paced',
field=models.BooleanField(default=False),
),
]
......@@ -80,6 +80,7 @@ class CourseContext(TimeStampedModel):
course_visibility = models.CharField(
max_length=32, choices=[(constant.value, constant.value) for constant in CourseVisibility]
)
self_paced = models.BooleanField(default=False)
class LearningSequence(TimeStampedModel):
......
......@@ -72,6 +72,7 @@ def get_outline_from_modulestore(course_key):
published_at=course.subtree_edited_on,
published_version=str(course.course_version), # .course_version is a BSON obj
sections=sections_data,
self_paced=course.self_paced,
course_visibility=CourseVisibility(course.course_visibility),
)
return course_outline_data
......@@ -45,6 +45,7 @@ class CourseOutlineViewTest(CacheIsolationTestCase, APITestCase):
published_at=datetime(2020, 5, 20, tzinfo=timezone.utc),
published_version="5ebece4b69dd593d82fe2020",
sections=generate_sections(cls.course_key, [2, 2]),
self_paced=False,
course_visibility=CourseVisibility.PUBLIC
)
replace_course_outline(cls.outline)
......
......@@ -39,7 +39,7 @@ class CourseOutlineView(APIView):
This serializer was purposefully declared inline with the
CourseOutlineView to discourage reuse/magic. Our goal is to make it
extremely obvious how things are being serialized, and not have suprise
extremely obvious how things are being serialized, and not have surprise
regressions because a shared serializer in another module was modified
to fix an issue in one of its three use cases.
......@@ -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,
"self_paced": user_course_outline.self_paced,
# Who and when this request was generated for (we can eventually
# support arbitrary times).
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment