Merge pull request #24396 from open-craft/patrick/BB-2672-tnl-7260-course-outline-api-hide-after-due
[BD-29] TNL-7260 LearningSequences Course Outline makes sequences with hide_after_due inaccessible after due date
This commit is contained in:
@@ -76,6 +76,8 @@ class CourseLearningSequenceData:
|
||||
title = attr.ib(type=str)
|
||||
visibility = attr.ib(type=VisibilityData)
|
||||
|
||||
inaccessible_after_due = attr.ib(type=bool, default=True)
|
||||
|
||||
|
||||
@attr.s(frozen=True)
|
||||
class CourseSectionData:
|
||||
|
||||
@@ -75,6 +75,7 @@ def get_course_outline(course_key: CourseKey) -> CourseOutlineData:
|
||||
sequence_data = CourseLearningSequenceData(
|
||||
usage_key=sequence_model.usage_key,
|
||||
title=sequence_model.title,
|
||||
inaccessible_after_due=sec_seq_model.inaccessible_after_due,
|
||||
visibility=VisibilityData(
|
||||
hide_from_toc=sec_seq_model.hide_from_toc,
|
||||
visible_to_staff_only=sec_seq_model.visible_to_staff_only,
|
||||
@@ -315,6 +316,7 @@ def _update_course_section_sequences(course_outline: CourseOutlineData, learning
|
||||
sequence=sequence_models[sequence_data.usage_key],
|
||||
defaults={
|
||||
'ordering': ordering,
|
||||
'inaccessible_after_due': sequence_data.inaccessible_after_due,
|
||||
'hide_from_toc': sequence_data.visibility.hide_from_toc,
|
||||
'visible_to_staff_only': sequence_data.visibility.visible_to_staff_only,
|
||||
},
|
||||
|
||||
@@ -75,6 +75,13 @@ class ScheduleOutlineProcessor(OutlineProcessor):
|
||||
seq_start = self.keys_to_schedule_fields[seq.usage_key].get('start')
|
||||
if seq_start and self.at_time < seq_start:
|
||||
inaccessible.add(seq.usage_key)
|
||||
continue
|
||||
|
||||
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)
|
||||
continue
|
||||
|
||||
return inaccessible
|
||||
|
||||
|
||||
@@ -21,7 +21,10 @@ class TestCourseOutlineData(TestCase):
|
||||
test as needed.
|
||||
"""
|
||||
super().setUpClass()
|
||||
normal_visibility = VisibilityData(hide_from_toc=False, visible_to_staff_only=False)
|
||||
normal_visibility = VisibilityData(
|
||||
hide_from_toc=False,
|
||||
visible_to_staff_only=False
|
||||
)
|
||||
cls.course_key = CourseKey.from_string("course-v1:OpenEdX+Learning+TestRun")
|
||||
cls.course_outline = CourseOutlineData(
|
||||
course_key=cls.course_key,
|
||||
@@ -110,7 +113,10 @@ def generate_sections(course_key, num_sequences):
|
||||
|
||||
All sections and sequences have normal visibility.
|
||||
"""
|
||||
normal_visibility = VisibilityData(hide_from_toc=False, visible_to_staff_only=False)
|
||||
normal_visibility = VisibilityData(
|
||||
hide_from_toc=False,
|
||||
visible_to_staff_only=False
|
||||
)
|
||||
sections = []
|
||||
for sec_num, seq_count in enumerate(num_sequences, 1):
|
||||
sections.append(
|
||||
|
||||
@@ -30,7 +30,8 @@ class CourseOutlineTestCase(CacheIsolationTestCase):
|
||||
def setUpTestData(cls):
|
||||
cls.course_key = CourseKey.from_string("course-v1:OpenEdX+Learn+Roundtrip")
|
||||
normal_visibility = VisibilityData(
|
||||
hide_from_toc=False, visible_to_staff_only=False
|
||||
hide_from_toc=False,
|
||||
visible_to_staff_only=False
|
||||
)
|
||||
cls.course_outline = CourseOutlineData(
|
||||
course_key=cls.course_key,
|
||||
@@ -122,7 +123,8 @@ class UserCourseOutlineTestCase(CacheIsolationTestCase):
|
||||
# Seed with data
|
||||
cls.course_key = CourseKey.from_string("course-v1:OpenEdX+Outline+T1")
|
||||
normal_visibility = VisibilityData(
|
||||
hide_from_toc=False, visible_to_staff_only=False
|
||||
hide_from_toc=False,
|
||||
visible_to_staff_only=False
|
||||
)
|
||||
cls.simple_outline = CourseOutlineData(
|
||||
course_key=cls.course_key,
|
||||
@@ -185,6 +187,15 @@ class ScheduleTestCase(CacheIsolationTestCase):
|
||||
cls.seq_same_key = cls.course_key.make_usage_key('sequential', 'seq_same')
|
||||
cls.seq_after_key = cls.course_key.make_usage_key('sequential', 'seq_after')
|
||||
cls.seq_inherit_key = cls.course_key.make_usage_key('sequential', 'seq_inherit')
|
||||
cls.seq_due_key = cls.course_key.make_usage_key('sequential', 'seq_due')
|
||||
|
||||
cls.all_seq_keys = [
|
||||
cls.seq_before_key,
|
||||
cls.seq_same_key,
|
||||
cls.seq_after_key,
|
||||
cls.seq_inherit_key,
|
||||
cls.seq_due_key,
|
||||
]
|
||||
|
||||
# Set scheduling information into edx-when for a single Section with
|
||||
# sequences starting at various times.
|
||||
@@ -219,9 +230,20 @@ class ScheduleTestCase(CacheIsolationTestCase):
|
||||
cls.seq_inherit_key,
|
||||
{'start': None}
|
||||
),
|
||||
# Sequence should inherit start information from Section, but has a due date set.
|
||||
(
|
||||
cls.seq_due_key,
|
||||
{
|
||||
'start': None,
|
||||
'due': datetime(2020, 5, 20, tzinfo=timezone.utc)
|
||||
}
|
||||
),
|
||||
]
|
||||
)
|
||||
visibility = VisibilityData(hide_from_toc=False, visible_to_staff_only=False)
|
||||
visibility = VisibilityData(
|
||||
hide_from_toc=False,
|
||||
visible_to_staff_only=False
|
||||
)
|
||||
cls.outline = CourseOutlineData(
|
||||
course_key=cls.course_key,
|
||||
title="User Outline Test Course!",
|
||||
@@ -234,16 +256,29 @@ class ScheduleTestCase(CacheIsolationTestCase):
|
||||
visibility=visibility,
|
||||
sequences=[
|
||||
CourseLearningSequenceData(
|
||||
usage_key=cls.seq_before_key, title='Before', visibility=visibility
|
||||
usage_key=cls.seq_before_key,
|
||||
title='Before',
|
||||
visibility=visibility
|
||||
),
|
||||
CourseLearningSequenceData(
|
||||
usage_key=cls.seq_same_key, title='Same', visibility=visibility
|
||||
usage_key=cls.seq_same_key,
|
||||
title='Same', visibility=visibility
|
||||
),
|
||||
CourseLearningSequenceData(
|
||||
usage_key=cls.seq_after_key, title='After', visibility=visibility
|
||||
usage_key=cls.seq_after_key,
|
||||
title='After',
|
||||
visibility=visibility
|
||||
),
|
||||
CourseLearningSequenceData(
|
||||
usage_key=cls.seq_inherit_key, title='Inherit', visibility=visibility
|
||||
usage_key=cls.seq_inherit_key,
|
||||
title='Inherit',
|
||||
visibility=visibility
|
||||
),
|
||||
CourseLearningSequenceData(
|
||||
usage_key=cls.seq_due_key,
|
||||
title='Due',
|
||||
visibility=visibility,
|
||||
inaccessible_after_due=True
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -256,25 +291,32 @@ class ScheduleTestCase(CacheIsolationTestCase):
|
||||
student_details = get_user_course_outline_details(self.course_key, self.student, at_time)
|
||||
return staff_details, student_details
|
||||
|
||||
def get_sequence_keys(self, exclude=None):
|
||||
if exclude is None:
|
||||
exclude = []
|
||||
if not isinstance(exclude, list):
|
||||
raise TypeError("`exclude` must be a list of keys to be excluded")
|
||||
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(
|
||||
datetime(2020, 5, 9, tzinfo=timezone.utc)
|
||||
)
|
||||
# Staff can always access all sequences
|
||||
assert len(staff_details.outline.accessible_sequences) == 4
|
||||
assert len(staff_details.outline.accessible_sequences) == 5
|
||||
# Student can access nothing
|
||||
assert len(student_details.outline.accessible_sequences) == 0
|
||||
|
||||
# Everyone can see everything
|
||||
assert len(staff_details.outline.sequences) == 4
|
||||
assert len(student_details.outline.sequences) == 4
|
||||
assert len(staff_details.outline.sequences) == 5
|
||||
assert len(student_details.outline.sequences) == 5
|
||||
|
||||
def test_before_section_starts(self):
|
||||
staff_details, student_details = self.get_details(
|
||||
datetime(2020, 5, 14, tzinfo=timezone.utc)
|
||||
)
|
||||
# Staff can always access all sequences
|
||||
assert len(staff_details.outline.accessible_sequences) == 4
|
||||
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
|
||||
@@ -289,14 +331,44 @@ class ScheduleTestCase(CacheIsolationTestCase):
|
||||
datetime(2020, 5, 15, tzinfo=timezone.utc)
|
||||
)
|
||||
# Staff can always access all sequences
|
||||
assert len(staff_details.outline.accessible_sequences) == 4
|
||||
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) == 3
|
||||
assert self.seq_before_key in student_details.outline.accessible_sequences
|
||||
assert self.seq_same_key in student_details.outline.accessible_sequences
|
||||
assert self.seq_inherit_key in student_details.outline.accessible_sequences
|
||||
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
|
||||
|
||||
def test_is_due_and_before_due(self):
|
||||
staff_details, student_details = self.get_details(
|
||||
datetime(2020, 5, 16, tzinfo=timezone.utc)
|
||||
)
|
||||
# Staff can always access all sequences
|
||||
assert len(staff_details.outline.accessible_sequences) == 5
|
||||
|
||||
# Student can access all sequences including the one that is due in
|
||||
# the future (self.seq_due_key)
|
||||
assert len(student_details.outline.accessible_sequences) == 5
|
||||
assert self.seq_due_key in student_details.outline.accessible_sequences
|
||||
|
||||
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)
|
||||
|
||||
def test_is_due_and_after_due(self):
|
||||
staff_details, student_details = self.get_details(
|
||||
datetime(2020, 5, 21, 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 is due before this
|
||||
# datetime (self.seq_due_key)
|
||||
assert len(student_details.outline.accessible_sequences) == 4
|
||||
assert self.seq_due_key not in student_details.outline.accessible_sequences
|
||||
assert self.seq_due_key in student_details.outline.sequences
|
||||
for key in self.get_sequence_keys(exclude=[self.seq_due_key]):
|
||||
assert key in student_details.outline.accessible_sequences
|
||||
|
||||
|
||||
class VisbilityTestCase(CacheIsolationTestCase):
|
||||
@@ -322,12 +394,22 @@ class VisbilityTestCase(CacheIsolationTestCase):
|
||||
|
||||
cls.staff_in_normal_key = cls.course_key.make_usage_key('sequential', 'staff_in_normal')
|
||||
cls.hide_in_normal_key = cls.course_key.make_usage_key('sequential', 'hide_in_normal')
|
||||
cls.due_in_normal_key = cls.course_key.make_usage_key('sequential', 'due_in_normal')
|
||||
cls.normal_in_normal_key = cls.course_key.make_usage_key('sequential', 'normal_in_normal')
|
||||
cls.normal_in_staff_key = cls.course_key.make_usage_key('sequential', 'normal_in_staff')
|
||||
|
||||
v_normal = VisibilityData(hide_from_toc=False, visible_to_staff_only=False)
|
||||
v_hide_from_toc = VisibilityData(hide_from_toc=True, visible_to_staff_only=False)
|
||||
v_staff_only = VisibilityData(hide_from_toc=False, visible_to_staff_only=True)
|
||||
v_normal = VisibilityData(
|
||||
hide_from_toc=False,
|
||||
visible_to_staff_only=False
|
||||
)
|
||||
v_hide_from_toc = VisibilityData(
|
||||
hide_from_toc=True,
|
||||
visible_to_staff_only=False
|
||||
)
|
||||
v_staff_only = VisibilityData(
|
||||
hide_from_toc=False,
|
||||
visible_to_staff_only=True
|
||||
)
|
||||
|
||||
cls.outline = CourseOutlineData(
|
||||
course_key=cls.course_key,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.2.14 on 2020-07-09 06:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('learning_sequences', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='coursesectionsequence',
|
||||
name='inaccessible_after_due',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -168,6 +168,9 @@ class CourseSectionSequence(CourseContentVisibilityMixin, TimeStampedModel):
|
||||
section = models.ForeignKey(CourseSection, on_delete=models.CASCADE)
|
||||
sequence = models.ForeignKey(LearningSequence, on_delete=models.CASCADE)
|
||||
|
||||
# Make the sequence inaccessible from the outline after the due date has passed
|
||||
inaccessible_after_due = models.BooleanField(null=False, default=False)
|
||||
|
||||
# Ordering, starts with 0, but global for the course. So if you had 200
|
||||
# sequences across 20 sections, the numbering here would be 0-199.
|
||||
ordering = models.PositiveIntegerField(null=False)
|
||||
|
||||
@@ -29,6 +29,7 @@ def update_from_modulestore(course_key):
|
||||
CourseLearningSequenceData(
|
||||
usage_key=sequence.location,
|
||||
title=sequence.display_name,
|
||||
inaccessible_after_due=sequence.hide_after_due,
|
||||
visibility=VisibilityData(
|
||||
hide_from_toc=sequence.hide_from_toc,
|
||||
visible_to_staff_only=sequence.visible_to_staff_only
|
||||
|
||||
@@ -112,6 +112,7 @@ class CourseOutlineView(APIView):
|
||||
"id": str(sequence.usage_key),
|
||||
"title": sequence.title,
|
||||
"accessible": sequence.usage_key in accessible_sequences,
|
||||
"inaccessible_after_due": sequence.inaccessible_after_due,
|
||||
**schedule_item_dict,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user