WBS for Course Content Outline and Course Unit pages
Low-level WBS for Course Content Outline and Course Unit pages
High-level analysis of the page Course Content Outline
From legacy to MFE + API (w/o functional changes).
Following the example we’ll use frontend-driven-development here.
- 1 Low-level WBS for Course Content Outline and Course Unit pages
- 2 High-level analysis of the page Course Content Outline
- 2.1 Steps
- 2.2 STEP 1: Feature Set
- 2.2.1 Page decomposition
- 2.2.1.1 Page route
- 2.2.1.2 Page layout
- 2.2.2 Functions
- 2.2.3 Features implementation description
- 2.2.1 Page decomposition
- 2.3 STEP 2: Configuration
- 2.4 STEP 3: MFE extension
- 2.5 STEP 4: API Contract
- 2.6 STEP 5: API (Backend for Frontend)
- 2.7 STEP 6: Documentation
Steps
Understand (list) the feature set.
extract features - split legacy UI to functional pieces
discover optional implicit functions
Define additional configuration
waffle course flag(s) for feature(s) - rollout period
Extend MFE
feature/sub-features placement (codebase hierarchy, naming, etc.)
routing (we want to stick to legacy paths as close as possible - to keep user experience not affected: bookmarks, etc.)
state management (shape, UI, data, etc.)
data required (shape, potential API endpoints, etc.) - API contract
main UI building blocks (basic presentational components)
unit tests
Compose final API contract
endpoints list with shaped data set
Implement API (BFF)
create backend DRF wrappers around legacy utils to satisfy requested API contract
unit tests
Internatioanalization (I18N)
Update documentation
MFE README
CMS (Platform) documentation
STEP 1: Feature Set
Legacy context:
Feature context:
course
Navigation:
CMS > Content > Outline
Page path:
<STUDIO-HOST>/course/course-v1:edX+DemoX+Demo_Course
Page decomposition
Page route
Dedicated route is already present.
Page layout
top bar (full width)
heading (left)
tool bar - page actions (right)
action-button1
… …
action-buttonN
content (full width)
main section (left)
status bar
attributes
attr1
… …
attrN
highlights
outline
empty course
section1
…
… …
sectionN
aside section (right)
info-item1
… …
info-itemN
Functions
Additional functions:
Heading (A)
Tool Bar (B)
New section creation (“New Section” button) - duplicated action (see:
2.a.i.1
)Course reindex initiation (“Reindex“ button)
Expand/collapse all sections (corresponding wording button)
Learning MFE initial unit link (“View Live“ button)
Status Bar (C)
Course Schedule link (“Start Date“ clickable value)
Pacing Type (passive course type)
Checklists (out of scope?)
Course Highlight Emails
Activation “Enable Now“ button
External documentation link (“Learn more“ link) - configurable?
Side Bar (D)
“Creating your course organisation” static section
“Reorganising your course” static section
External documentation link (“Learn more about the course outline“ button) - configurable?
“Setting release dates and grading policies” static section
External documentation link (“Learn more about grading policy settings”) - configurable?
“Changing the content learners see” static section
External documentation link (“Learn more about content visibility settings“) - configurable?
Feedback Messages
“Sav“
Course structure manipulation:
Outline (E)
List sections
Empty course (initial state)
New Section - creation
Sections list
Loader
Section actions (red)
New Section - creation (j)
Delete Section - removal (f)
Duplicate Section - cloning (e)
Configure Section - configuration (d)
Reorder Section - movement (g)
Expand/Collapse Section - appearance (a)
Rename Section - edit (b, c)
Section Highlights - description (i)
Publish Section (h)
Subsection actions (yellow)
New Subsection - creation (h)
Delete Subsection - removal (f)
Duplicate Subsection - cloning (e)
Configure Subsection - configuration (d)
Reorder Subsection - movement (g)
Expand/Collapse Subsection - appearance (a)
Rename Subsection - edit (b, c)
Publish Subsection (h)
Unit actions (green)
New Unit - creation (h)
Delete Unit - removal (e)
Duplicate Unit - cloning (d)
Configure Unit - configuration (c)
Reorder Unit - movement (f)
Rename Unit - edit (a, b)
Publish Unit (g)
Feedback (F)
“Saving…“ | “Adding…“ | … (during any action processing)
“Error“ (API errors, etc.)
Page alerts (example:
Course has been successfully reindexed.
)…
Implicit functions
…
Features implementation description
Feature | Responsibility | State | Data | Presentational components |
---|---|---|---|---|
OUTLINE (top page level) |
| State: // "courseOutline" state slice
// courseOutlineReducer "subroot" reducer
... Actions: Selectors: | Template context (legacy) - cms/djangoapps/contentstore/views/course.py#course_handler
cms/djangoapps/contentstore/views/course.py#course_index
---
{
'language_code': request.LANGUAGE_CODE,
'context_course': course_module,
'lms_link': lms_link,
'sections': sections,
'course_structure': course_structure,
'initial_state': course_outline_initial_state(locator_to_show, course_structure) if locator_to_show else None,
'rerun_notification_id': current_action.id if current_action else None,
'course_release_date': course_release_date,
'settings_url': settings_url,
'reindex_link': reindex_link,
'deprecated_blocks_info': deprecated_blocks_info,
'notification_dismiss_url': reverse_course_url(
'course_notifications_handler',
current_action.course_key,
kwargs={
'action_state_id': current_action.id,
},
) if current_action else None,
'frontend_app_publisher_url': frontend_app_publisher_url,
'mfe_proctored_exam_settings_url': get_proctored_exam_settings_url(course_module.id),
'advance_settings_url': reverse_course_url('advanced_settings_handler', course_module.id),
'proctoring_errors': proctoring_errors,
} New API Is there any global non-feature-specific data? |
|
course-details |
| Shape: {
"start_date": <course-start-date>,
"pacing_type": <course-pacing-type>,
} Actions: |
|
|
course-checklists | out-of-scope? | Shape: Actions: |
|
|
course-highlights |
See OeX documentation. | Shape: {
"editor_is_open": false,
"documentation_link": <web-link>,
"emails_activated": false,
"highlights": {
"section_id": [
{
"text": <highlight1-content>
},
{
"text": <highlight2-content>
}
],
"section_id": [],
}
} Actions: course-highlights.editor.opened {
"section": <section_id>
}
course-highlights.editor.closed {}
course-highlights.emails.activated {}
course-highlights.save.started {
"section": <section_id>,
"highlights": [
{
"text": <highlight1-content>
},
{
"text": <highlight2-content>
}
]
} Selectors: TBD | Enable “Course Highlight Emails“ (legacy) POST <CMS>/xblock/block-v1:RG+02+101+type@course+block@course
{
publish: "republish",
metadata: {
highlights_enabled_for_messaging: true
}
}
---
JSON {
"id": "block-v1:RG+02+101+type@course+block@course",
...
"data": null,
"metadata": {
...
"highlights_enabled_for_messaging": true,
...
}
} Fetch block outline data (legacy) GET <CMS>/xblock/outline/<course-locator>
---
JSON {
"id": "block-v1:RG+02+101+type@course+block@course",
"display_name": "CMS MFE",
"category": "course",
"has_children": true,
...
"highlights_enabled_for_messaging": true,
"highlights_enabled": true,
"highlights_preview_only": false,
"highlights_doc_url": "http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-olive.master/developing_course/course_sections.html#set-section-highlights-for-weekly-course-highlight-messages",
...
} New API GET <CMS-API>/highlights/<course_id>
---
JSON {
"highlights_enabled_for_messaging": <bool>,
"highlights_enabled": <bool>,
"highlights_preview_only": <bool>,
"highlights_doc_url": <URI>,
...
}
POST <CMS-API>/highlights/ {
"course_id": <course_id>,
"section_id": <section_id>,
"highlights": [
{
"text": <highlight1-content>
},
{
"text": <highlight2-content>
}
]
}
---
JSON {
...
} |
|
section-create |
| Shape: {
in_progress: false,
} Actions: // notation is abstract:
course.section.create {
"after": "section_id", (optional)
}
course.section.creation.started {}
course.section.creation.succeed {}
course.section.creation.failed {}
dispatch: alert.show {
"type": "error"
"message": <text>
}
dispatch: feedback.show {
"text": "Adding",
}
Selectors: TBD | Create new block (legacy) POST <CMS>/xblock/ {
"parent_locator":"block-v1:RG+02+101+type@course+block@course",
"category":"chapter",
"display_name":"Section"
}
---
JSON {
"locator": "block-v1:RG+02+101+type@chapter+block@9cf00a10306e45e48e3bcce89bccd818",
"courseKey": "course-v1:RG+02+101"
} Fetch course outline data (legacy) GET <CMS>/xblock/outline/block-v1:RG+02+101+type@course+block@course
---
JSON {
"id": "block-v1:RG+02+101+type@course+block@course",
"display_name": "CMS MFE",
"category": "course",
"has_children": true,
...
"child_info": {
"category": "chapter",
"display_name": "Section",
"children": [
{
"id": "block-v1:RG+02+101+type@chapter+block@9cf00a10306e45e48e3bcce89bccd818",
"display_name": "Section",
"category": "chapter",
"has_children": true,
"edited_on": "Jun 29, 2023 at 10:19 UTC",
... other data
}
]
},
...
} New API NOTE: even if there is already a new API for course parts creation
we implement a dedicated Outline BFF API (which should reuse available funcs)
POST <CMS-API>/course/section/create {
"parent_id": <course_id>,
"include_after_id": <section_id>, (optional, default: last)
"display_name":<section-name"> (optional, default: "Section")
}
---
JSON {
"locator": "block-v1:RG+02+101+type@chapter+block@9cf00a10306e45e48e3bcce89bccd818",
"display_name": "Section",
}
|
|
course-index |
| Shape: {
indexing: false,
enabled: false,
} Actions: // abstract notation:
<course-indexing-started> {
"course": <course-id>,
}
<course-indexing-finished> {
"course": <course-id>,
}
[on <course-indexing-finished>]
<display-alert-success> {
"icon": "fa-bullhorn",
"title": "Course Index",
"message": <user_message>
} | Recreate course search index (legacy) GET <CMS>/course/course-v1:RG+02+101/search_reindex
---
JSON {
"user_message": "Course has been successfully reindexed."
} New API // Initial state:
GET <CMS-API>/search-index/
---
JSON {
"enabled": true,
}
POST <CMS-API>/search-index/reindex {
"course_id": <course_id>,
}
---
JSON {
"error": <bool>,
"feedback": "Course has been successfully reindexed.",
...
} |
|
learning-link (in progress) |
| Shape: {
...
} Actions: learning.link.navigated {
"block_id": <block_id>,
"target": <lms-jump-to-url>,
"title": <title-text>,
}
possibly, tracking events | Template context (legacy) # target URL example:
<LMS>/courses/course-v1:RG+02+101/jump_to/block-v1:RG+02+101+type@course+block@course New API // Fetch LMS course live view (new)
GET <CMS-API>/learning-link/?course_id=<course_id>
---
JSON {
<block_id>: {
"target": <lms-jump-to-url>,
"title": <title-text>,
}
} |
|
TOOLBAR |
| Shape: Actions: Selectors: | Template context (legacy) - cms/djangoapps/contentstore/views/course.py#course_index
---
{
'language_code': request.LANGUAGE_CODE,
'context_course': course_module,
'lms_link': lms_link,
'sections': sections,
'course_structure': course_structure,
'initial_state': course_outline_initial_state(locator_to_show, course_structure) if locator_to_show else None,
'rerun_notification_id': current_action.id if current_action else None,
'course_release_date': course_release_date,
'settings_url': settings_url,
'reindex_link': reindex_link,
'deprecated_blocks_info': deprecated_blocks_info,
'notification_dismiss_url': reverse_course_url(
'course_notifications_handler',
current_action.course_key,
kwargs={
'action_state_id': current_action.id,
},
) if current_action else None,
'frontend_app_publisher_url': frontend_app_publisher_url,
'mfe_proctored_exam_settings_url': get_proctored_exam_settings_url(course_module.id),
'advance_settings_url': reverse_course_url('advanced_settings_handler', course_module.id),
'proctoring_errors': proctoring_errors,
} New API Is there any global non-feature-specific data?
|
|
STATUSBAR |
| Shape: Actions: | Template context (legacy)
Fetch course attributes reuse course data (selectors) |
|
ASIDE |
| Shape: Actions: |
|
|
FEEDBACK |
| Shape: {
"message": <Saving...>
} Actions: <outline.feedback.show> {
"message": <Saving...>
}
<outline.feedback.hide> {} | No API data. |
|
CONTENT |
| Shape: {
"sections": {
<section_id>: {
"display_name": <string>,
...
},
}
} Actions: Selectors: | Template context (legacy) - cms/djangoapps/contentstore/views/course.py#course_index
---
{
...
'context_course': course_module,
...
'sections': sections,
'course_structure': course_structure,
'initial_state': course_outline_initial_state(locator_to_show, course_structure) if locator_to_show else None,
...
} What is the initial data set? Fetch Quality data (legacy) ? - see what is it about GET <CMS>/api/courses/v1/quality/course-v1:RG+02+101/?exclude_graded=true&all=true Fetch Validation data (legacy) ? - see what is it about GET <CMS>/api/courses/v1/validation/course-v1:RG+02+101/?graded_only=true&all=true New API ? |
|
collapse-all-toggle |
| Shape: {
"collapse_index": 1 | 0 | -1,
// 1 - fully collapsed,
// 0 - partially collapsed,
// -1 - fully expanded,
// keeps track on each collapsible items
// sections/subsections?
} Actions: outline.collapse.all {}
outline.expand.all {}
// listens to each collapsible item | No API data. Possibly, tracking events. |
|
empty-outline |
| Shape: {} Actions: - | No API data. |
|
section-list |
| Shape: {
order: [
<section_id>,
<section_id>,
<section_id>,
...
]
} Actions: - | Template context (legacy) - cms/djangoapps/contentstore/views/course.py#course_index
---
{
...
'sections': sections,
'course_structure': course_structure,
'initial_state': course_outline_initial_state(locator_to_show, course_structure) if locator_to_show else None,
...
} New API // Fetch initial course outline data
// (should sections list data include all outline stuff in it??)
GET <CMS-API>/course/outline?course_id=<course_id>
---
JSON {
// the same data we had in template context?
} |
|
section-mode (expand/collapse) |
| Shape: {
<section_id>: "collpsed" | "expanded",
<section_id>: "collpsed" | "expanded",
...
} Actions: outline.section.expanded {
"section_id": <section_id>
}
outline.section.collapsed {
"section_id": <section_id>
} | No API data. Possibly, tracking events. |
|
section-edit |
| Shape: // dedicated "editing" state:
// on save section-feature state is updated.
{
<section_id>: {
"editing": <bool>,
"display_name": <edited_display_name>,
},
<section_id>: {
"editing": <bool>,
"display_name": <edited_display_name>,
}
} Actions: outline.section.editing.initiated {
"section_id": <section_id>
}
outline.section.editing.updated {
"section_id": <section_id>,
"display_name": <value>,
}
outline.section.editing.finished {
"section_id": <section_id>
} | Delete section API (legacy) // Update course section
POST <CMS>/xblock/block-v1:RG+02+101+type@chapter+block@e852b385ad5a4e45ba6226ba2bd5d6af {
"metadata":{"display_name":"Section1"}
} Side effects:
New API // Update course section
POST <CMS-API>/course/section/update {
"section_id": <section_id>,
"metadata":{
"display_name":"Section1",
}
}
---
JSON {
"error": <bool>,
"feedback": <string>,
} |
|
section-settings |
| Shape: {
<section_id>: {
"configuring": <bool>,
"active_tab": basic | visibility,
"values": {
"release_date": <value>,
"release_time": <value>,
"hide_from_learners": <bool>,
}
},
} Actions: outline.section.settings.initiated {
"section_id": <section_id>,
}
outline.section.settings.tab {
"section_id": <section_id>,
"tab_id": basic | visibility,
}
outline.section.settings.updated {
"section_id": <section_id>,
<field_id>: <value>,
}
outline.section.settings.canceled {
"section_id": <section_id>,
}
outline.section.settings.saved {
"section_id": <section_id>,
} | Initial state API (legacy) Template context? Section settings saving API (legacy) POST <CMS>/xblock/block-v1:RG+02+101+type@chapter+block@eb6f08a917074cd496ca96c00c858767 {
{
"metadata":{
"start":"2030-01-23T01:00:00.000Z",
"visible_to_staff_only":true
},
"publish":"republish"
}
}
---
JSON {
"id": "block-v1:RG+02+101+type@chapter+block@eb6f08a917074cd496ca96c00c858767",
"data": null,
"metadata": {
"display_name": "Section12",
"start": "2030-01-23T01:00:00Z",
"visible_to_staff_only": true
}
} Side effects:
New API // Update course section settings
POST <CMS-API>/course/section/settings {
"section_id": <section_id>,
"data":{
"start": "2030-01-23T01:00:00Z",
"visible_to_staff_only": true
}
}
---
JSON {
"error": <bool>,
"feedback": <string>,
} |
|
section-clone |
| Shape: {
"in_progress": <bool>,
} Actions: outline.section.cloning.initiated {
"section_id": <section_id>,
}
outline.section.cloning.finished {
"section_id": <section_id>,
"new_section": {
...
}
} | Duplicate section API (legacy) POST <CMS>/xblock/ {
"duplicate_source_locator":"block-v1:RG+02+101+type@chapter+block@eb6f08a917074cd496ca96c00c858767",
"parent_locator":"block-v1:RG+02+101+type@course+block@course"
}
---
JSON {
"locator": "block-v1:RG+02+101+type@chapter+block@ccab4052c1a34f1db4b4ebd4c69bb8c6",
"courseKey": "course-v1:RG+02+101"
} Side effects:
New API // Clone course section
POST <CMS-API>/course/section/clone {
"section_id": <section_block_id>,
"parent_id": <course_block_id>
}
---
JSON {
"error": <bool>,
"feedback": <string>,
"locator": <section_block_id>,
"course_id": <course_block_id>
} |
|
section-delete |
| Shape: {
in_progress: <bool>,
} Actions: course.section.deletion.initiated {
"section_id": <section_id>,
}
course.section.deletion.canceled {
"section_id": <section_id>,
}
course.section.deletion.confirmed {
"section_id": <section_id>,
}
| Delete section API (legacy) // Remove course section
DELETE <CMS>/xblock/block-v1:RG+02+101+type@chapter+block@e852b385ad5a4e45ba6226ba2bd5d6af Side effects:
New API // Remove course section
POST <CMS-API>/course/section/delete {
"section_id": <section_id>,
}
---
JSON {
"error": <bool>,
"feedback": <string>,
} |
|
section-move |
| Shape: {
"in_progress": <bool>,
"section_id": <section_id>,
"source_index": <int>,
"target_index": <int>,
} Actions: course.section.movement.initiated {
"section_id": <section_id>,
"source_index": <int>,
}
course.section.movement.started {
"section_id": <section_id>,
}
course.section.movement.finished {
"section_id": <section_id>,
"target_index": <int>,
} |