From 17e69d7881b4c19503a64d08b36836c2a5990dc1 Mon Sep 17 00:00:00 2001 From: Bill Currie Date: Tue, 7 Jul 2020 15:31:46 +0900 Subject: [PATCH] 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. --- .../learning_sequences/api/outlines.py | 12 +- .../api/processors/schedule.py | 29 +++- .../learning_sequences/api/tests/test_data.py | 22 +++ .../api/tests/test_outlines.py | 128 ++++++++++++++++-- .../content/learning_sequences/data.py | 15 ++ .../0005_coursecontext_days_early_for_beta.py | 18 +++ .../content/learning_sequences/models.py | 1 + .../content/learning_sequences/tasks.py | 1 + .../learning_sequences/tests/test_views.py | 1 + .../content/learning_sequences/views.py | 1 + 10 files changed, 214 insertions(+), 14 deletions(-) create mode 100644 openedx/core/djangoapps/content/learning_sequences/migrations/0005_coursecontext_days_early_for_beta.py diff --git a/openedx/core/djangoapps/content/learning_sequences/api/outlines.py b/openedx/core/djangoapps/content/learning_sequences/api/outlines.py index b8c18271bc..f31f57fc06 100644 --- a/openedx/core/djangoapps/content/learning_sequences/api/outlines.py +++ b/openedx/core/djangoapps/content/learning_sequences/api/outlines.py @@ -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, } ) diff --git a/openedx/core/djangoapps/content/learning_sequences/api/processors/schedule.py b/openedx/core/djangoapps/content/learning_sequences/api/processors/schedule.py index a0429e8253..306b2423d6 100644 --- a/openedx/core/djangoapps/content/learning_sequences/api/processors/schedule.py +++ b/openedx/core/djangoapps/content/learning_sequences/api/processors/schedule.py @@ -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, diff --git a/openedx/core/djangoapps/content/learning_sequences/api/tests/test_data.py b/openedx/core/djangoapps/content/learning_sequences/api/tests/test_data.py index 0c678c605e..13a83f6ffa 100644 --- a/openedx/core/djangoapps/content/learning_sequences/api/tests/test_data.py +++ b/openedx/core/djangoapps/content/learning_sequences/api/tests/test_data.py @@ -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): """ diff --git a/openedx/core/djangoapps/content/learning_sequences/api/tests/test_outlines.py b/openedx/core/djangoapps/content/learning_sequences/api/tests/test_outlines.py index d90efc9d7c..814d15fd2f 100644 --- a/openedx/core/djangoapps/content/learning_sequences/api/tests/test_outlines.py +++ b/openedx/core/djangoapps/content/learning_sequences/api/tests/test_outlines.py @@ -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 diff --git a/openedx/core/djangoapps/content/learning_sequences/data.py b/openedx/core/djangoapps/content/learning_sequences/data.py index 10325133cd..a12071a2c5 100644 --- a/openedx/core/djangoapps/content/learning_sequences/data.py +++ b/openedx/core/djangoapps/content/learning_sequences/data.py @@ -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: diff --git a/openedx/core/djangoapps/content/learning_sequences/migrations/0005_coursecontext_days_early_for_beta.py b/openedx/core/djangoapps/content/learning_sequences/migrations/0005_coursecontext_days_early_for_beta.py new file mode 100644 index 0000000000..0272c98515 --- /dev/null +++ b/openedx/core/djangoapps/content/learning_sequences/migrations/0005_coursecontext_days_early_for_beta.py @@ -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), + ), + ] diff --git a/openedx/core/djangoapps/content/learning_sequences/models.py b/openedx/core/djangoapps/content/learning_sequences/models.py index 8ef5cdf20c..2fa574d2bb 100644 --- a/openedx/core/djangoapps/content/learning_sequences/models.py +++ b/openedx/core/djangoapps/content/learning_sequences/models.py @@ -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) diff --git a/openedx/core/djangoapps/content/learning_sequences/tasks.py b/openedx/core/djangoapps/content/learning_sequences/tasks.py index 743441c5ac..ef03a17bf4 100644 --- a/openedx/core/djangoapps/content/learning_sequences/tasks.py +++ b/openedx/core/djangoapps/content/learning_sequences/tasks.py @@ -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), diff --git a/openedx/core/djangoapps/content/learning_sequences/tests/test_views.py b/openedx/core/djangoapps/content/learning_sequences/tests/test_views.py index 098764ab27..7bfb5dc1f2 100644 --- a/openedx/core/djangoapps/content/learning_sequences/tests/test_views.py +++ b/openedx/core/djangoapps/content/learning_sequences/tests/test_views.py @@ -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 diff --git a/openedx/core/djangoapps/content/learning_sequences/views.py b/openedx/core/djangoapps/content/learning_sequences/views.py index dd9753be78..69b33ec786 100644 --- a/openedx/core/djangoapps/content/learning_sequences/views.py +++ b/openedx/core/djangoapps/content/learning_sequences/views.py @@ -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