Context

Programs are a structured way to group edX course offerings together, to serve learning objectives or instructional goals that span multiple courses.

Programs will be used as the technical foundation to deliver XSeries, an edx.org-specific product offering planned for early 2016. It is our intention to implement Programs as a generic and reusable capability in the edX Platform, so it can be leveraged in support of related use cases both by edx.org and the wider Open edX community. However, our main priority for Q1-Q2 2016 is to develop the functionality necessary to deliver the XSeries MVP.

This document focuses on a key new piece of infrastructure encapsulating the data, logic, and API upon which the new feature will be built, and how it will integrate with key services in the platform to support basic use cases.      

Architecture Overview

Program-related functionality will be implemented in a self-contained, independently-deployable web application. Related support will be implemented in the form of smaller changes to the LMS, Studio, and other participating platform applications.

Modularity is emphasized as a general principle; dependencies in related systems will be kept to a minimum and enabled/disabled via feature flags.  This is intended to maximize operational flexibility when choosing to introduce the Programs functionality within an existing installation.

The new application will serve as the "source of truth" in edx-platform for the Programs themselves, as well as Learners' associations with those Programs (aka Registrations). These data will be exposed as resources via RESTful HTTP APIs. In addition, the application will host client-side JavaScript applications as user-facing frontends to these APIs.

We plan to implement these features using Django / DRF / SQL backend and Backbone.js frontend stacks, keeping consistent with other platform applications.

Data Model


Entities

Program

The Program represents the collection of multiple Course Codes in an ordered sequence.  Each Course Code in turn collects Course Run Modes; learners must complete one course run mode for each course code in order to satisfy the completion requirement of the program. 

Attributes:

Organization

This entity is a reference to an organization as known to the platform.  It maps to the first component in an LMS course_key, and exists in this model in order to enforce relational integrity at the physical (database) level.

Attributes:

CourseCode

This entity represents the notion of a certain course offering independent of run.  This concept does not exist in edx-platform, but can generally be inferred by comparing the first two components (org and course) in course keys; for example two course runs with course keys "course-v1:ANUx+ASTRO2X+2B3T2015" and "course-v1:ANUx+ASTRO2X+2B4T2015" would be associated with a single Course Code whose key is "ASTRO2X" and is associated with an Organization whose key is "ANUx".

ProgramOrganization

This represents the partner Organization that is publishing a given Program.

For the XSeries MVP, only one Organization per Program will be supported; this entity is modeled as many-to-many for future extensibility.

ProgramCourseCode

This represents an association between a Program and a CourseCode. The ProgramCourse has a 'position' attribute which is used to present CourseCodes within a Program in a user-defined order.

For the XSeries MVP, a CourseCode may only be associated with a single Program.  This is in order to simplify the learner dashboard implementation planned for MVP.  In a future iteration, a CourseCode and its runs / modes may appear in multiple Programs.

ProgramCourseRunMode

This represents a specific run of a course, and a "mode" of that course, linked to a Program (via ProgramCourseCode).  Runs and modes cannot be associated with Programs until they have been published to the participating LMS. (To avoid consistency problems, logic may be added to modify, remove, or invalidate Programs when a related run / mode stops being available in the LMS, though this is not part of XSeries MVP.)

ProgramUser

This represents a User's association with / registration into a Program, and captures any state relevant to that association. For the XSeries MVP, this state consists of a required 'status' enum attribute which will be used to track whether the User is "active", "completed", or "deleted" in that Program.  "Deleted" will be an internal-only status to support soft-deletion; API consumers will not see registrations with this status.

For the XSeries MVP, we are eschewing an explicit user interaction to make this association.  Instead, learners will be automatically and lazily associated with Programs:

Program Lifecycle

The following sequential lifecycle is envisioned for Programs, based on known/anticipated use cases:

  1. unpublished - an initial status during which the Program is being constructed but should not yet be made available to Learners.  Unpublished Programs are invisible to non-admins and learners cannot be associated with them.
  2. active – this status is set when the Author/Admin has finished building the Program and wants to make it available to Learners in the participating LMS.
    1. When a Program has been set to active, Course Runs cannot be removed from the Program.
    2. A Program can only be reverted from active to unpublished if no Users have yet been associated with the Program.
  3. retired – this status is set when the Program should no longer be offered to new Learners. It may still be displayed as a historical item if that makes sense in a given context (e.g. viewing a Learner's past achievements).
  4. deleted – used to support soft-deletion of Programs where needed. Programs with this status will not be visible to API consumers.

Population of Organizations

Organizations are pointers to related data for which the authoritative source is presently Studio (as part of the Certificate administration feature).  The existence, key and display name of each Organization has to be copied from Studio.  This can be done using a combination of strategies:

Population of CourseCodes

CourseCodes have no equivalent entity in edx-platform.  They are instead derived from the set of course runs that are published into the LMS using Studio.  To generate this data in the Programs service, its API will expose and endpoint that accepts POST (or DELETE) requests containing information about a course run, and will generate CourseCode (and if needed, Organization) records.  

This endpoint will be used in a way similar to the way Organizations are synced:

Internally, the API endpoint for course runs will parse the course_key into its component parts (org, course, run).  A get-or-create operation will ensure that records exist corresponding to the org (in Organization) and course (in CourseCode).  If the CourseCode is being created for the first time, the course run's title should be used for the display_name of the CourseCode.  If the Organization is being created for the first time (which should happen rarely, if ever, assuming Organizations are synced first), the org part will be used for both the key and the display_name.

 

Selection of Course Run Modes

Course runs themselves are not duplicated in the service.  Administrative tools used to build Programs will use our enrollment API to fetch listings of available runs and modes, filtered by an organization and course code selected using Admin interfaces.  (Support for these filters needs to be added to the enrollment API.)  When selections are made, the selected course key, mode (slug), and SKU (if used) will be used to create ProgramCourseRunMode records in the service. 

API

General notes

Unless otherwise specified in endpoint details,

Roles and Permissions

MVP Permissions:

MVP Roles (and their permissions):

Role Mapping:

Future Expansion (speculative):

Programs

GET /programs/
List all Programs.

Optional query filters:

NOTE: querying / searching by Program name or description is out of scope for this MVP.

This endpoint handles access differently depending on whether it is accessed by a learner or staff:

Example Success Response:

HTTP/1.1 200 OK
Content-Type: application/json

[
  {
    "id": 1,	
    "name": "Astrophysics",
    "description": "A great astrophysics series",
    "category": "XSeries",
    "status": "unpublished",
    "organizations": [
      {
        "id": "ANUx",
        "display_name": "Australian National University"
      }
    ],
    "courses": [
      {
        "id": "ANUx/ANU-ASTRO2x",
        "organization": {
          "id": "ANUx",
          "display_name": "Australian National University"
        },
        "display_name": "Astrophysics: Exploring Exoplanets",
        "runs": [
          {
            "course_key": "ANUx/ANU-ASTRO2x/2B3T2015",
            "display_name": "Australian National University"
          },
          {
            "course_key": "ANUx/ANU-ASTRO2x/2B4T2015",
            "display_name": "Astrophysics: Exploring Exoplanets"
          }
        ]
      }
    ]
  }
]
POST /programs/

Create a Program.

Response codes:

Example request body:

{
  "name": "Astrophysics",
  "description": "A great astrophysics series",
  "category": "XSeries",
  "organizations": [
    {
      "id": "ANUx"
    }
  ],
  "courses": [
    {
      "id": "ANUx/ANU-ASTRO2x",
      "organization": {
        "id": "ANUx",
      },
      "runs": []
    }
  ]
}

 

Example Response (success):

HTTP/1.1 201 Created
Content-Type: application/json

{
  "id": 1,	
  "name": "Astrophysics",
  "description": "A great astrophysics series",
  "category": "XSeries",
  "status": "unpublished",
  "organizations": [
    {
      "id": "ANUx",
      "display_name": "Australian National University"
    }
  ],
  "courses": [
    {
      "id": "ANUx/ANU-ASTRO2x",
      "organization": {
        "id": "ANUx",
        "display_name": "Australian National University"
      },
      "display_name": "Astrophysics: Exploring Exoplanets",
      "runs": []
    }
  ]
}
GET /programs/:program_id/

View full details of a Program, including Orgs, Courses, and Runs.

Example response:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": 1,	
  "name": "Astrophysics",
  "description": "A great astrophysics series",
  "category": "XSeries",
  "status": "unpublished",
  "organizations": [
    {
      "id": "ANUx",
      "display_name": "Australian National University"
    }
  ],
  "courses": [
    {
      "id": "ANUx/ANU-ASTRO2x",
      "organization": {
        "id": "ANUx",
        "display_name": "Australian National University"
      },
      "display_name": " Astrophysics: Exploring Exoplanets",
      "runs": [
        {
          "course_key": "ANUx/ANU-ASTRO2x/2B3T2015",
          "display_name": "Astrophysics: Exploring Exoplanets"
        },
        {
          "course_key": "ANUx/ANU-ASTRO2x/2B4T2015"
          "display_name": "Astrophysics: Exploring Exoplanets"
        }
      ]
    }
  ]
}
PATCH /programs/:program_id/

Partially update a Program.

Any of the following keys may be included, and will overwrite existing values: "name", "description", "status", "orgs", "courses". Values for any keys not included in the request will not be modified.

This is a merge-patch implementation, and callers must specify content-type "application/merge-patch+json".

Response codes:

After a successful patch, the entire (updated) resource is returned, same as for the GET response.

Example request body:

{
  "name": "Astrophysics",
  "description": "Learn contemporary astrophysics from the best",
  "status": "active"
}


Example response (success):

HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": 1,	
  "name": "Astrophysics",
  "description": "Learn contemporary astrophysics from the best",
  "category": "XSeries",
  "status": "active",
  "organizations": [
    {
      "id": "ANUx",
      "display_name": "Australian National University"
    }
  ],
  "courses": [
    {
      "id": "ANUx/ANU-ASTRO2x",
      "organization": {
        "id": "ANUx",
        "display_name": "Australian National University"
      },
      "display_name": " Astrophysics: Exploring Exoplanets",
      "runs": [
        {
          "course_key": "ANUx/ANU-ASTRO2x/2B3T2015",
          "display_name": " Astrophysics: Exploring Exoplanets"
        },
        {
          "course_key": "ANUx/ANU-ASTRO2x/2B4T2015"
          "display_name": " Astrophysics: Exploring Exoplanets"
        }
      ]
    }
  ]
}

 

Course Runs

PUT /runs/:course_key/

Create or update a Course Run.

This will also automatically create an Org and a Course in the system derived from components of the course_key, if they do not already exist. When the Course is being created for the first time, the display_name attribute will be copied from the name of the Course Run. The display_name attribute of a newly-created Org will be initially empty.

Display names for the Org, Course, and Course Run may be subsequently customized via API calls.

The course_key part of the URL should be urlencoded.

DELETE /runs/:course_key/

Remove a Course Run. This will not remove any orphaned Org or Course records that may be left behind.

The course_key part of the URL should be urlencoded.

if any active Program would be affected by removing the Course Run, a 403 / Forbidden response will be returned.

Organizations

GET /organizations/

List all Organizations.

Example response:

HTTP/1.1 200 OK
Content-Type: application/json

[
  {
    "id": "ANUx",
    "display_name": "Australian National University"
  }
]
PUT /organizations/:organization_id/

Update the display_name of an Organization.  The entire (updated) resource is returned.

Example Request:

{
  "display_name": "The Australian National University"
}

Example Response:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": "ANUx",
  "display_name": "The Australian National University"
}

Courses

GET /courses/

List all Courses, with Course Runs inline.

Optional query filters:


Example response:

HTTP/1.1 200 OK
Content-Type: application/json

[
  {
    "id": "ANUx/ANU-ASTRO2x",
    "organization": {
      "id": "ANUx",
      "display_name": "Australian National University"
    },
    "display_name": " Astrophysics: Exploring Exoplanets",
    "runs": [
      {
        "course_key": "ANUx/ANU-ASTRO2x/2B3T2015",
        "display_name": " Astrophysics: Exploring Exoplanets"
      },
      {
        "course_key": "ANUx/ANU-ASTRO2x/2B4T2015",
        "display_name": " Astrophysics: Exploring Exoplanets"
      }
    ]
  }
]
GET /courses/:course_id/

View a single Course, with Course Runs inline. 

The course_id part of the URL should be urlencoded.

Example response:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": "ANUx/ANU-ASTRO2x",
  "organization": {
    "id": "ANUx",
    "display_name": "Australian National University"
  },
  "display_name": "Astrophysics: Exploring Exoplanets",
  "runs": [
    {
      "course_key": "ANUx/ANU-ASTRO2x/2B3T2015",
      "display_name": " Astrophysics: Exploring Exoplanets"
    },
    {
      "course_key": "ANUx/ANU-ASTRO2x/2B4T2015",
      "display_name": " Astrophysics: Exploring Exoplanets"
    }
  ]
}
PUT /courses/:course_id/

Update the display_name of a Course.  The entire (updated) resource is returned.

Example Request:

{
  "display_name": "Astrophysics: Exploring Exciting Exoplanets"
}

Example Response: (see GET response)

XSeries MVP Administration Flow (Otto / CAT)

The Program Admin views will be implemented as a JavaScript single-page application (SPA) which will deployed with the Programs service, and included within the Otto UX via script tags. This client application will use the Program HTTP APIs to perform CRUD on Programs and list related entities (Orgs, Courses, Runs).

edx-platform Integration

In MVP, learner-facing interfaces related to Programs will be exposed as links from the LMS dashboard.  Post-MVP, we may expose authoring views using JavaScript code embedded in Studio.

A new feature flag, ENABLE_PROGRAMS, will be introduced, which will toggle the availability of program-related functionality.

LMS Integration: Learner Registration / Dashboard

The ENABLE_PROGRAMS flag will toggle the inclusion of logic on the LMS dashboard that will call to the Programs API to determine which of their currently-enrolled course runs are associated with programs, and draw a link to that program inline with each.  The link will take the learner to a detailed view of that Program, including courses, course runs, and their completion status within each.

The program detail views will be implemented in JavaScript code which will be deployed with the Programs service, and included within the LMS UX via script tags.  This application will use the Program HTTP APIs and the Enrollment API together to indicate in which of the Program's Course Runs the Learner is presently enrolled.

Enrollment Integration

Under the scope of this proposal, the LMS remains the source of truth for Course [Run] Enrollments.  Program-related APIs do not require direct access to enrollment data per se; however, displaying enrollment-related information on a per-Learner basis will be relevant for the dashboard and similar use cases.  In the proposed implementation, it should be sufficient to retrieve enrollment information from the existing API and pass that data to the Program-specific views.

In order to provide controls for the Learner to enroll in a new Course Run from Program-related views, we will pass data/configuration to the views that is capable of redirecting the user to the appropriate endpoint when requesting new enrollments - either the ecommerce system (when participating/configured) or directly to the LMS's enrollment handlers. 

Progress, Completion, and Certification

For XSeries we need to implement behavior that will allow Learners to:

In addition, administrators will need to be able to create and customize certificate templates for Programs.

In order to support these requirements we will (probably) need to enhance or create LMS functionality around tracking and communicating course completion, and both the LMS and the Programs service will need to collaborate in order to fulfill certificates.  We will address this work in detail in a follow-on discovery / design effort.  

Commerce Integration (tentative)

Integration with Otto (the external commerce service) will work along similar lines to the present integration for creating and fulfilling products based on seats in Course [Runs].

This functionality / integration has been removed from the MVP and will very likely be revised in a successive design effort.

Mobile Integration

Learners' courseware experience on the Mobile app is not impacted by work being done for the XSeries MVP.  Purchasing / registering in XSeries is specifically out of scope within the MVP.  We do intend for all new APIs and UX's to be compatible with the Mobile implementations, to support future integrations post-MVP.

Analytics Integration

A bulk data export facility for consumption by downstream analytics will be developed, but this is not currently planned as part of the MVP.  edX also has specific reporting requirements around sales and registrations which will be driven in large part by data that will reside in our commerce service. 

XBlock Integration

It is not within the scope of the XSeries MVP to interact directly with Programs from XBlocks; however, implementing a runtime service client for the HTTP APIs or referencing the Javascript applications from HTML views should be straightforward and self-explanatory.