diff --git a/cms/djangoapps/contentstore/signals/handlers.py b/cms/djangoapps/contentstore/signals/handlers.py index efbe888e28..e7e26cd96b 100644 --- a/cms/djangoapps/contentstore/signals/handlers.py +++ b/cms/djangoapps/contentstore/signals/handlers.py @@ -67,6 +67,7 @@ def listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable= # import here, because signal is registered at startup, but items in tasks are not yet able to be loaded from cms.djangoapps.contentstore.tasks import update_outline_from_modulestore_task, update_search_index if key_supports_outlines(course_key): + # Push the course outline to learning_sequences asynchronously. update_outline_from_modulestore_task.delay(str(course_key)) # Finally call into the course search subsystem diff --git a/openedx/core/djangoapps/content/learning_sequences/README.rst b/openedx/core/djangoapps/content/learning_sequences/README.rst index 0f03a16367..de0f093181 100644 --- a/openedx/core/djangoapps/content/learning_sequences/README.rst +++ b/openedx/core/djangoapps/content/learning_sequences/README.rst @@ -9,17 +9,12 @@ users through the LMS, though it is also available to Studio for pushing data into the system. The first API this app implements is computing the Course Outline. +.. important:: This package should _not_ depend on the modulestore directly. ---------------- -Direction: Keep ---------------- - -This package is being actively developed, but in a very early state. We're not -going to start feeding data into it (by triggering off of publish) until it's a -little more complete, so that it's easier to make drastic changes to the data -model if necessary. During development, you can seed data into it by using the -``update_course_outline`` management command. +The primary path which feeds course outline data into learning sequence models +is a signal handler upon a Studio course publish. However, you can also seed +data into it by using the ``update_course_outline`` management command. ----- Usage @@ -49,7 +44,7 @@ rules will have to be reimplmented–as we've already seen with mobile APIs? a single sequence. 3. Block Transformers are extremely powerful, but also complex and slow. Optimizing them to suit our use case would be a lot of work and result in - even more complexity. Sequence and outline related metadata is much smaller, + even more complexity. Sequence- and outline-related metadata is much smaller, and we can make simplifying assumptions like treating the outline as a tree and not a DAG (i.e. each Sequence being in only one Section). @@ -59,7 +54,8 @@ How to Extend? This app is experimenting with some new conventions, so please read the decision docs (``docs/decisions``). Many of the modules also have a long top-level -docstring explaining what they should be used for–please read these. Many of the +docstring explaining for what they should be used – please read these docstrings. +Many of the conventions are there to promote predictable behavior and are dramatically less effective if broken even in little ways. @@ -71,7 +67,7 @@ The public data types are in ``api/data.py``. Database persistence is in dumb. All real business logic should happen in a module within the ``api`` package. Currently, all the existing API logic is handled in ``api/outlines.py`` and re-exported at the top level in ``api/__init__.py``. If your new -functionality is outlines related, please follow this convention. Otherwise, you +functionality is outline-related, please follow this convention. Otherwise, you can create a new module in ``api/`` (e.g. ``api/sequences.py``) to hold your logic, and re-export that via the top level ``api/__init__.py``. @@ -79,8 +75,8 @@ I want to add a new rule affecting how sequences show up in the outline. ======================================================================== You probably want to create or modify an OutlineProcessor (``api/processors``). -This is not yet a pluggable interface, though it was designed to make it easy to -turn into one. Please see the docstrings in ``api/processors/base.py`` for +This interface is not yet pluggable, though it was designed to make it easy to +become pluggable. Please see the docstrings in ``api/processors/base.py`` for details on how to write one. I want to pull data from ModuleStore or Block Structures. diff --git a/openedx/core/djangoapps/content/learning_sequences/data.py b/openedx/core/djangoapps/content/learning_sequences/data.py index 5e39b4fc71..3dd4eda231 100644 --- a/openedx/core/djangoapps/content/learning_sequences/data.py +++ b/openedx/core/djangoapps/content/learning_sequences/data.py @@ -18,9 +18,6 @@ Guidelines: or use API functions from other apps. They should not trigger expensive computation. -Note: we're using old-style syntax for attrs because we need to support Python -3.5, but we can move to the PEP-526 style once we move to Python 3.6+. - TODO: Validate all datetimes to be UTC. """ import logging @@ -50,7 +47,7 @@ class ObjectDoesNotExist(Exception): pass # lint-amnesty, pylint: disable=unnecessary-pass -@attr.s(frozen=True) +@attr.s(frozen=True, auto_attribs=True) class ContentErrorData: """ A human-readable description of something wrong with the content, to ease @@ -62,11 +59,11 @@ class ContentErrorData: when things don't show up where we expect then to be and we omit them from the outline (unknown tag types, sequences where we expect sections, etc.) """ - message = attr.ib(type=str) - usage_key = attr.ib(type=Optional[UsageKey], default=None) + message: str + usage_key: Optional[UsageKey] = None -@attr.s(frozen=True) +@attr.s(frozen=True, auto_attribs=True) class VisibilityData: """ XBlock attributes that help determine item visibility. @@ -75,29 +72,29 @@ class VisibilityData: # lets you define a Sequence that is reachable by direct URL but not shown # in Course navigation. It was used for things like supplementary tutorials # that were not considered a part of the normal course path. - hide_from_toc = attr.ib(type=bool, default=False) + hide_from_toc: bool = False # Restrict visibility to course staff, regardless of start date. This is # often used to hide content that either still being built out, or is a # scratch space of content that will eventually be copied over to other # sequences. - visible_to_staff_only = attr.ib(type=bool, default=False) + visible_to_staff_only: bool = False -@attr.s(frozen=True) +@attr.s(frozen=True, auto_attribs=True) class ExamData: """ XBlock attributes that describe exams """ - is_practice_exam = attr.ib(type=bool, default=False) - is_proctored_enabled = attr.ib(type=bool, default=False) - is_time_limited = attr.ib(type=bool, default=False) + is_practice_exam: bool = False + is_proctored_enabled: bool = False + is_time_limited: bool = False def __bool__(self): return self.is_practice_exam or self.is_proctored_enabled or self.is_time_limited -@attr.s(frozen=True) +@attr.s(frozen=True, auto_attribs=True) class CourseLearningSequenceData: """ A Learning Sequence (a.k.a. subsection) from a Course. @@ -107,22 +104,22 @@ class CourseLearningSequenceData: learning sequences in Courses vs. Pathways vs. Libraries. Such an object would likely not have `visibility` as that holds course-specific concepts. """ - usage_key = attr.ib(type=UsageKey) - title = attr.ib(type=str) - visibility = attr.ib(type=VisibilityData, default=VisibilityData()) - exam = attr.ib(type=ExamData, default=ExamData()) - inaccessible_after_due = attr.ib(type=bool, default=False) + usage_key: UsageKey + title: str + visibility: VisibilityData = VisibilityData() + exam: ExamData = ExamData() + inaccessible_after_due: bool = False -@attr.s(frozen=True) +@attr.s(frozen=True, auto_attribs=True) class CourseSectionData: """ A Section in a Course (sometimes called a Chapter). """ - usage_key = attr.ib(type=UsageKey) - title = attr.ib(type=str) - visibility = attr.ib(type=VisibilityData) - sequences = attr.ib(type=List[CourseLearningSequenceData]) + usage_key: UsageKey + title: str + visibility: VisibilityData + sequences: List[CourseLearningSequenceData] @attr.s(frozen=True) @@ -247,41 +244,41 @@ class CourseOutlineData: ) -@attr.s(frozen=True) +@attr.s(frozen=True, auto_attribs=True) class ScheduleItemData: """ Scheduling specific data (start/end/due dates) for a single item. """ - usage_key = attr.ib(type=UsageKey) + usage_key: UsageKey # Start date that is specified for this item - start = attr.ib(type=Optional[datetime]) + start: Optional[datetime] # Effective release date that it's available (may be affected by parents) - effective_start = attr.ib(type=Optional[datetime]) - due = attr.ib(type=Optional[datetime]) + effective_start: Optional[datetime] + due: Optional[datetime] -@attr.s(frozen=True) +@attr.s(frozen=True, auto_attribs=True) class ScheduleData: """ Overall course schedule data. """ - course_start = attr.ib(type=Optional[datetime]) - course_end = attr.ib(type=Optional[datetime]) - sections = attr.ib(type=Dict[UsageKey, ScheduleItemData]) - sequences = attr.ib(type=Dict[UsageKey, ScheduleItemData]) + course_start: Optional[datetime] + course_end: Optional[datetime] + sections: Dict[UsageKey, ScheduleItemData] + sequences: Dict[UsageKey, ScheduleItemData] -@attr.s(frozen=True) +@attr.s(frozen=True, auto_attribs=True) class SpecialExamAttemptData: """ Overall special exam attempt data. """ - sequences = attr.ib(type=Dict[UsageKey, Dict]) + sequences: Dict[UsageKey, Dict] -@attr.s(frozen=True) +@attr.s(frozen=True, auto_attribs=True) class UserCourseOutlineData(CourseOutlineData): """ A course outline that has been customized for a specific user and time. @@ -298,16 +295,16 @@ class UserCourseOutlineData(CourseOutlineData): # to reach up into parts of a Course that the user is not normally allowed # to know the existence of (e.g. Sequences marked `visible_to_staff_only`), # we can use this attribute. - base_outline = attr.ib(type=CourseOutlineData) + base_outline: CourseOutlineData # Django User representing who we've customized this outline for. This may # be the AnonymousUser. - user = attr.ib(type=User) + user: User # UTC TZ time representing the time for which this user course outline was # created. It is possible to create UserCourseOutlineData for a time in the # future (i.e. "What will this user be able to see next week?") - at_time = attr.ib(type=datetime) + at_time: datetime # What Sequences is this `user` allowed to access? Anything in the `outline` # is something that the `user` is allowed to know exists, but they might not @@ -317,15 +314,15 @@ class UserCourseOutlineData(CourseOutlineData): # * If anonymous course access is enabled in "public_outline" mode, # unauthenticated users (AnonymousUser) will see the course outline but # not be able to access anything inside. - accessible_sequences = attr.ib(type=Set[UsageKey]) + accessible_sequences: Set[UsageKey] -@attr.s(frozen=True) +@attr.s(frozen=True, auto_attribs=True) class UserCourseOutlineDetailsData: """ Class that has a user's course outline plus useful details (like schedules). Will eventually expand to include other systems like Completion. """ - outline = attr.ib(type=UserCourseOutlineData) - schedule = attr.ib(type=ScheduleData) - special_exam_attempts = attr.ib(type=SpecialExamAttemptData) + outline: UserCourseOutlineData + schedule: ScheduleData + special_exam_attempts: SpecialExamAttemptData