Discussion API
Background
The Mobile team is creating a new API to interact with discussion forums to support the addition of discussion features to the mobile apps. This API is specifically designed and scoped to support the mobile discussion implementation that we are undertaking this quarter, though our intention is for this API to be sufficiently general to support other use cases as well. In particular, we want the browser-based client to ultimately use this new API, though there is some functionality that it requires that are out of scope for this initial proposal. We plan to generally conform to the edX REST API Conventions.
Determining the course
In general, access control for discussions requires determining which course is being accessed. This can be done in three different ways: 1) the course_id as a path element (e.g. accessing a topic tree); 2) the course_id as a query parameter (e.g. accessing the thread list); or 3) in the content itself (e.g. accessing a specific thread or posting new content).
Topic definition
Unfortunately, the topic tree is not defined as a concept in itself but rather is constructed from settings of the discussion modules in the course. For the web front end, the hierarchy is formed from discussion modules by splitting the discussion_category setting ("Category" in Studio) on "/" for intermediate nodes in the tree and using the discussion_target setting ("Subcategory" in Studio) for leaf nodes. Non-courseware topics are also added alongside the topics from discussion modules; those are defined in the discussion_topics course setting ("Discussion Topic Mapping" in Studio Advanced Settings). In this API, the discussion_category setting is not split, so the hierarchy is presently limited to two levels. Also, courseware-specific topics (those defined by discussion modules) are returned separately from those defined in the discussion_topics setting. However, the data model is designed to accommodate an arbitrarily deep hierarchy to allow for future development of a more structured definition of such hierarchy.
Because the topic tree does not have a data model per se but is constructed in this way, its API is necessarily read-only. Another important consideration in the computation of the topic tree is that it respects content groups, release dates, and other access control on discussion modules.
Forum roles
At present, access control in the discussion feature is done via a role and permission framework that is designed to allow very granular granting of permissions. When a course is created in Studio or by the seed_permissions_roles django command, four roles are created: Adminstrator, Moderator, Community TA, and Student. Various appropriate permissions are assigned to those roles. There is no facility beyond manually changing database entries to modify the set of roles and permissions for a particular course. Thus, the designed granularity of the permission framework has, to our knowledge, never been needed or used. As a result, this proposal ignores permissions and grants access by role only. It should also be noted that the Moderator and Community TA roles differ only in name but have the same permissions. The Administrator role is assigned one additional permission called "manage_moderator", but forum role management is in done via the Instructor Dashboard, which is accessible to any course staff member, so the "manage_moderator" permission is never actually used. Thus, the Administrator, Moderator, and Community TA roles are interchangeable (for access control purposes) and referred to collectively below as "privileged roles."
Note that there is no direct representation of roles in this proposed API. Rather, more granular permissions are represented by listing editable fields for the thread and comment models. This is desirable because the client does not need to keep track of what each role means. (Yes, the author appreciates the irony of this statement given the above discussion of permissions on the server).
Groups
Groups are a feature that make certain threads only available to certain sets of users. Both users and threads can be assigned to groups. If a thread is assigned to a group, then only users who are also assigned to that group (and users with a privileged role) can view the thread. Users without a privileged role can only assign threads to their own groups, while users with a privileged role can assign a thread to any group.
Cohorts
Cohorts are an LMS feature that makes use of the discussion group functionality (and, currently, the only feature that makes use of this functionality). If cohorts are enabled for a course, then each user is assigned to exactly one cohort group. Each discussion topic can be cohorted or not. If a topic is cohorted, then new threads are assigned to the cohort of the author by default. A user with a privileged role can assign a new thread (or an existing thread) to any group or to be visible to everyone. If a topic is not cohorted, new threads are not assigned to a group.
Thread closure
A user with a privileged role can "close" a thread. When a thread is closed, only a user with a privileged role can modify the thread, modify any of the thread's comments, or add a new comment to the thread. Closing a thread does not affect its visibility.
Discussion blackouts
Course staff can configure blackout intervals for the course's discussions. During a blackout, all existing discussion content is accessible, but only a user with a privileged role can create or modify discussion content. This feature is typically used during an exam period to prevent students from discussing answers before the due date.
Disabling discussions
Some courses remove the discussion tab (or substitute another tab with the same name with a link to an external site). In courses where this has has been done, all endpoints in this API will be inaccessible (with all endpoints returning 404 errors). Given that it is possible for course authors to remove the discussion tab but still include discussion modules in the course, this will need to be revisited before moving the web front end to this API.
Resources
Course
Note: This representation of the course is read only.
Field | Description |
---|---|
id | String. The identifier of the course. |
blackouts | List of Objects. A list of intervals during which users without a privileged role may not post. Each interval is an object containing "start" and "end" fields, where each value is an ISO 8601 String. |
thread_list_url | String. The URL of the list of all threads in the course. |
following_thread_list_url | String. The URL of the list of the user's followed threads in the course |
topics_url | String. The URL of the topic listing for the course. |
cohorts | List of Objects. A list of cohorts in the course (if the course is cohorted). Each cohort is an object containing a "group_name" string field and a "group_id" integer field. |
Topic Tree
Note: This representation of topics is read only.
Field | Description |
---|---|
id | String. The identifier string of the topic (null if this topic is only an intermediate node that cannot contain its own threads). |
name | String. The display name of the topic. |
thread_list_url | String. The URL of the thread list for the topic. For nodes with children, this will be a query for all threads in any subtopic. |
children | List of Topic Trees. Contains the subtrees of this topic. Can vary by user. |
Course Topics
Note: This representation of topics is read only.
Field | Description |
---|---|
courseware_topics | List of Topic Trees. Contains the list of topic trees defined by discussion modules in the courseware. |
non_courseware_topics | List of Topic Trees. Contains the list of topic trees defined by course settings outside the courseware. |
Thread
Field | Description | Initializable by | Editable by |
---|---|---|---|
id | String. The identifier string of the thread. | None | None |
course_id | String. The identifier string of the thread's course. | Any role | None |
topic_id | String. The identifier of the thread's topic. | Any role | Author or privileged role |
group_id | Integer. The numeric identifier of the thread's group (i.e. cohort) or null if not in a group. | Privileged role | Privileged role |
group_name | String. The display name of the thread's group (i.e. cohort) or null if not in a group. | None | None |
author | String. The username of the thread's author. Null if and only if the thread is anonymous or the thread is anonymous to students and the user does not have a privileged role. Can vary by user. | None | None |
author_label | String. A string value to indicate that the author has a privileged role. Currently, this will be either "Staff" for Administrators and Moderators or "Community TA" for Community TAs. | None | None |
created_at | ISO 8601 String. The timestamp of the thread's creation. | None | None |
updated_at | ISO 8601 String. The timestamp of the thread's last modification. Note that this can include certain operations that are not visible to the present user, such as the thread being flagged by another user. | None | None |
type | String. The type of post (either "question" or "discussion"). | Any role | Author or privileged role |
title | String. The title of the thread. | Any role | Author or privileged role |
raw_body | String. The raw content of the thread. This may contain Markdown, HTML, and MathJax markup. | Any role | Author or privileged role |
rendered_body | String. The content of the thread, rendered in HTML (Markdown rendering and HTML stripping applied). | None | None |
has_endorsed | Boolean. Whether the thread has any endorsed comments. | None | None |
pinned | Boolean. Whether the thread is pinned (to appear at the beginning of a thread list regardless of sort). | Privileged role | Privileged role |
closed | Boolean. Whether new edits and responses are allowed. | Privileged role | Privileged role |
following | Boolean. Whether the user is following the thread. Can vary by user. | Any role | Any role |
abuse_flagged | Boolean. Whether the user has flagged the thread as abusive. Can vary by user. | None | Any role |
voted | Boolean. Whether the user has voted for the thread. Can vary by user. | None | Any role |
vote_count | Integer. The total number of votes for the thread. | None | None |
read | Boolean. Whether the user has read the thread. Can vary by user. | None | None |
comment_count | Integer. The total count of contributions (post + responses + comments) for the thread. | None | None |
unread_comment_count | Integer. The total count of unread contributions (post + responses + comments) for the thread. Can vary by user. | None | None |
comment_list_url | String. The URL of the comment list for the thread. In Q4 implementation, this will be null for question threads. | None | None |
endorsed_comment_list_url | String. The URL of the endorsed comment list for the thread. In Q4 implementation, this will be null for discussion threads. | None | None |
non_endorsed_comment_list_url | String. The URL of the non-endorsed comment list for the thread. In Q4 implementation, this will be null for discussion threads. | None | None |
editable_fields | List of Strings. The names of fields that can be edited by the user. | None | None |
response_count | Integer. The total number of direct responses for the thread. | None | None |
Comment
Field | Description | Initializable by | Editable by |
---|---|---|---|
id | String. The identifier string of the comment. | None | None |
parent_id | String. The identifier string of the comment's parent comment (null for first-level thread responses). | Any role | None |
thread_id | String. The identifier string of the comment's thread. | Any role | None |
author | String. The username of the thread's author. If and only if anonymous is true or anonymous_to_students is true and the user does not have a privileged role and is not the author, this will be null. Can vary by user. | None | None |
author_label | String. A string value to indicate that the author has a privileged role. Currently, this will be either "Staff" for Administrators and Moderators or "Community TA" for Community TAs. | None | None |
created_at | ISO 8601 String. The timestamp of the comment's creation. | None | None |
updated_at | ISO 8601 String. The timestamp of the comment's last modification. Note that this can include certain operations that are not visible to the present user, such as the thread being flagged by another user. | None | None |
raw_body | String. The raw content of the comment. This may contain Markdown, HTML, and MathJax markup. | Any role | Author or privileged role |
rendered_body | String. The content of the comment, rendered in HTML (Markdown rendering and HTML stripping applied). | None | None |
endorsed | Boolean. Whether the comment has been endorsed by a privileged user or accepted as a correct answer by the thread author or a privileged user. | Thread author (if thread is a question) or privileged role | Thread author (if thread is a question) or privileged role |
endorsed_by | String. The username of the user who endorsed the comment. Can be null if the information is unavailable or to avoid revealing the identity of the author of an anonymous thread (i.e. when a non-staff author of an anonymous thread endorses a response). | None | None |
endorsed_by_label | String. A string value to indicate that the endorser has a privileged role. Currently, this will be either "Staff" for Administrators and Moderators or "Community TA" for Community TAs. | None | None |
endorsed_at | ISO 8601 String. The timestamp of the endorsement or acceptance as a correct answer. Can be null if the information is unavailable. | None | None |
abuse_flagged | Boolean. Whether the user has flagged the comment as abusive. Can vary by user. | None | Any role |
voted | Boolean. Whether the user has voted for the comment. Can vary by user. | None | Any role |
vote_count | Integer. The total number of votes for the comment. | None | None |
children | List of Comments. The children of this comment. | None | None |
editable_fields | List of Strings. The names of fields that can be edited by the user. | None | None |
Endpoints
Note that all endpoints are accessible only if the user has been assigned a forum role for the course (which happens automatically upon enrollment in the course) and, except for the course endpoint, discussions have not been disabled for the course.
/api/discussion/v1/courses/{course_id}/
Method | Description | Access |
---|---|---|
GET | Retrieve the discussion information for a course (the Course resource above). No parameters. | Any role |
{ "id": "course-v1:TestX+TestCourse+TestRun", "discussions_enabled": true, "blackouts": [{"start": "2015-04-15T00:00:00Z", "end": "2015-04-22T00:00:00Z"}], "thread_list_url": "https://openedx.example.com/api/discussion/v1/threads/?course_id=course-v1:TestX+TestCourse+TestRun", "following_thread_list_url": "https://openedx.example.com/api/discussion/v1/threads/?course_id=course-v1:TestX+TestCourse+TestRun&following=1", "flagged_thread_list_url": "https://openedx.example.com/api/discussion/v1/threads/?course_id=course-v1:TestX+TestCourse+TestRun&abuse_flagged=1", "topics_url": "https://openedx.example.com/api/discussion/v1/course_topics/course-v1:TestX+TestCourse+TestRun/", "cohorts": [{"group_id": 1, "group_name": "Cohort One"}, {"group_id": 2, "group_name": "Cohort Two"}] }
/api/discussion/v1/course_topics/{course_id}/
Method | Description | Access |
---|---|---|
GET | Retrieve the topics for the course, respecting normal access control for the user. No parameters. | Any role |
{ "courseware_topics": [ { "name": "Week 1", "thread_list_url": "https://openedx.example.com/api/discussion/v1/threads/?course_id=course-v1:TestX+TestCourse+TestRun&topic_id=7bbd1b43d28b49a0bf1ae852d3e957f1&topic_id=bf960351e62e4d4aa42e851292defc5f", "children": [ { "id": "7bbd1b43d28b49a0bf1ae852d3e957f1", "name": "Lecture 1", "thread_list_url": "https://openedx.example.com/api/discussion/v1/threads/?course_id=course-v1:TestX+TestCourse+TestRun&topic_id=7bbd1b43d28b49a0bf1ae852d3e957f1", "children": [] }, { "id": "bf960351e62e4d4aa42e851292defc5f", "name": "Lecture 2", "thread_list_url": "https://openedx.example.com/api/discussion/v1/threads/?course_id=course-v1:TestX+TestCourse+TestRun&topic_id=bf960351e62e4d4aa42e851292defc5f", "children": [] } ] } ], "non_courseware_topics": [ { "id": "df3e1598d8544e04a34dfcfa22313caf", "name": "General", "thread_list_url": "https://openedx.example.com/api/discussion/v1/threads/?course_id=course-v1:TestX+TestCourse+TestRun&topic_id=df3e1598d8544e04a34dfcfa22313caf", "children": [] } ] }
/api/discussion/v1/threads/
Method | Description | Access |
---|---|---|
GET | Retrieve a list of threads. Query Parameters:
| Any role; flagged parameter is only respected for privileged users |
POST | Create a new thread. Parameters as per initializability noted in model. If the user does not have a privileged role, and the topic identified by topic_id is cohorted, the new thread's group_id will be automatically initialized to the id of the user's cohort. If ther user has a privileged role, the specified group_id (or lack thereof) will be respected. Required Body Parameters:
| Any role, subject to blackouts |
Example:
{ "text_search_rewrite": "example", "count": 26, "next": "https://openedx.example.com/api/discussion/v1/threads/?course_id=course-v1:TestX+TestCourse+TestRun&page=2", "previous": null, "results": [ { "id": "51dfd4e38d9f4f788a0e65c0e9311a55", "course_id": "course-v1:TestX+TestCourse+TestRun", "topic_id": "df3e1598d8544e04a34dfcfa22313caf", "group_id": null, "group_name": null, "author": "example-user", "created_at": "2015-04-09T17:31:56Z", "modified_at": "2015-04-09T17:31:56Z", "type": "discussion", "title": "Example Thread Title", "raw_body": "**Example Thread Body**", "rendered_body": "<b>Example Thread Body</b>", "has_endorsed": false, "pinned": false, "closed": false, "flagged": false, "voted": false, "vote_count": 42, "comment_count": 3, "unread_comment_count": 1, "read": true, "comment_list_url": "https://openedx.example.com/api/discussion/v1/comments/?thread_id=51dfd4e38d9f4f788a0e65c0e9311a55", "endorsed_comment_list_url": null, "non_endorsed_comment_list_url": null, "editable_fields": ["flagged", "voted"] }, // ... ] }
// Request body { "course_id": "course-v1:TestX+TestCourse+TestRun", "topic_id": "df3e1598d8544e04a34dfcfa22313caf", "type": "discussion", "title": "Example Thread Title", "raw_body": "**Example Thread Body**", } // Response body { "id": "51dfd4e38d9f4f788a0e65c0e9311a55", "course_id": "course-v1:TestX+TestCourse+TestRun", "topic_id": "df3e1598d8544e04a34dfcfa22313caf", "group_id": null, "group_name": null, "author": "example-user", "created_at": "2015-04-09T17:31:56Z", "modified_at": "2015-04-09T17:31:56Z", "type": "discussion", "title": "Example Thread Title", "raw_body": "**Example Thread Body**", "rendered_body": "<b>Example Thread Body</b>", "has_endorsed": false, "pinned": false, "closed": false, "flagged": false, "voted": false, "vote_count": 0, "comment_count": 0, "unread_comment_count": 0, "read": true, "comment_list_url": "https://openedx.example.com/api/discussion/v1/comments/?thread_id=51dfd4e38d9f4f788a0e65c0e9311a55", "endorsed_comment_list_url": null, "non_endorsed_comment_list_url": null, "editable_fields": ["type", "title", "raw_body", "flagged", "voted"], "response_count": 0 }
/api/discussion/v1/threads/{thread_id}/
Method | Description | Access |
---|---|---|
GET | Retrieve the thread. No parameters. | Any role, subject to group restriction |
PATCH | Modify the thread. Parameters as per editability noted in model | Author or privileged role, subject to group restriction, closure, and blackouts |
DELETE | Delete the thread. No parameters. | Author or privileged role |
Example:
{ "id": "51dfd4e38d9f4f788a0e65c0e9311a55", "course_id": "course-v1:TestX+TestCourse+TestRun", "topic_id": "df3e1598d8544e04a34dfcfa22313caf", "group_id": null, "group_name": null, "author": "example-user", "created_at": "2015-04-09T17:31:56Z", "modified_at": "2015-04-09T17:31:56Z", "type": "discussion", "title": "Example Thread Title", "raw_body": "**Example Thread Body**", "rendered_body": "<b>Example Thread Body</b>", "has_endorsed": false, "pinned": false, "closed": false, "flagged": false, "voted": false, "vote_count": 42, "comment_count": 3, "unread_comment_count": 1, "read": true, "comment_list_url": "https://openedx.example.com/api/discussion/v1/comments/?thread_id=51dfd4e38d9f4f788a0e65c0e9311a55", "endorsed_comment_list_url": null, "non_endorsed_comment_list_url": null, "editable_fields": ["flagged", "voted"], "response_count": 2 }
// Request body {"voted": true} // Response body { "id": "51dfd4e38d9f4f788a0e65c0e9311a55", "course_id": "course-v1:TestX+TestCourse+TestRun", "topic_id": "df3e1598d8544e04a34dfcfa22313caf", "group_id": null, "group_name": null, "author": "example-user", "created_at": "2015-04-09T17:31:56Z", "modified_at": "2015-04-09T17:31:56Z", "type": "discussion", "title": "Example Thread Title", "raw_body": "**Example Thread Body**", "rendered_body": "<b>Example Thread Body</b>", "has_endorsed": false, "pinned": false, "closed": false, "flagged": false, "voted": true, "vote_count": 43, "comment_count": 3, "unread_comment_count": 1, "read": true, "comment_list_url": "https://openedx.example.com/api/discussion/v1/comments/?thread_id=51dfd4e38d9f4f788a0e65c0e9311a55", "endorsed_comment_list_url": null, "non_endorsed_comment_list_url": null, "editable_fields": ["flagged", "voted"], "response_count": 2 }
/api/discussion/v1/comments/
Method | Description | Access |
---|---|---|
GET | Retrieve a list of comments, ordered by creation date ascending. Query Parameters:
| Any role; comments will only be returned if thread group restriction allows |
POST | Create a new comment. Parameters as per initializability noted in model. Required Body Parameters:
| Any role, subject to thread group restriction, thread closure, and blackouts |
Example:
{ "count": 31, "next": "https://openedx.example.com/api/discussion/v1/comments/?thread_id=51dfd4e38d9f4f788a0e65c0e9311a55&page=2", "previous": null, "results": [ { "id": "5fe452161cc049dfb3e124c41732beaf", "parent_id": null, "thread_id": "51dfd4e38d9f4f788a0e65c0e9311a55", "author": "example-user", "created_at": "2015-04-09T17:31:56Z", "modified_at": "2015-04-10T14:24:43Z", "raw_body": "**Example Comment Body**", "rendered_body": "<b>Example Comment Body</b>", "endorsed": true, "endorsed_by": "example-moderator", "endorsed_at": "2015-04-10T14:24:43Z", "flagged": false, "voted": false, "vote_count": 18, "children": [], "editable_fields": ["flagged", "voted"] }, // ... ] }
// Request body { "thread_id": "51dfd4e38d9f4f788a0e65c0e9311a55", "raw_body": "**Example Comment Body**", } // Response body { "id": "5fe452161cc049dfb3e124c41732beaf", "parent_id": null, "thread_id": "51dfd4e38d9f4f788a0e65c0e9311a55", "author": "example-user", "created_at": "2015-04-09T17:31:56Z", "modified_at": "2015-04-09T17:31:56Z", "raw_body": "**Example Comment Body**", "rendered_body": "<b>Example Comment Body</b>", "endorsed": false, "endorsed_by": null, "endorsed_at": null, "flagged": false, "voted": false, "vote_count": 0, "children": [], "editable_fields": ["raw_body", "flagged", "voted"] }
/api/discussion/v1/comments/{comment_id}/
Method | Description | Access |
---|---|---|
GET | Retrieve a list of child comments for a response Query Parameters:
| Any role; comments will only be returned if thread group restriction allows |
PATCH | Modify the comment. Parameters as per editability noted in model. | Author or privileged role, subject to thread group restriction, thread closure, and blackouts |
DELETE | Delete the comment. No parameters. | Author or privileged role |
Example:
[ { "id": "5fe452161cc049dfb3e124c41732beaf", "parent_id": "6w3452161cc049dfb3e124c41732bead", "thread_id": "51dfd4e38d9f4f788a0e65c0e9311a55", "author": "example-user", "created_at": "2015-04-09T17:31:56Z", "modified_at": "2015-04-10T14:24:43Z", "raw_body": "**Example Comment Body**", "rendered_body": "<b>Example Comment Body</b>", "endorsed": false, "endorsed_by": null, "endorsed_at": null, "flagged": false, "voted": false, "vote_count": 0, "children": [], "editable_fields": ["flagged", "voted"] }, { "id": "1w33erf67yh8kf3sf45km11k0987hj2n", "parent_id": "6w3452161cc049dfb3e124c41732bead", "thread_id": "51dfd4e38d9f4f788a0e65c0e9311a55", "author": "example-user", "created_at": "2015-04-09T17:31:56Z", "modified_at": "2015-04-10T14:24:43Z", "raw_body": "**Example Another Comment Body**", "rendered_body": "<b>Example Another Comment Body</b>", "endorsed": false, "endorsed_by": null, "endorsed_at": null, "flagged": false, "voted": false, "vote_count": 0, "children": [], "editable_fields": ["flagged", "voted"] } ]
// Request body {"voted": true} // Response body { "id": "5fe452161cc049dfb3e124c41732beaf", "parent_id": null, "thread_id": "51dfd4e38d9f4f788a0e65c0e9311a55", "author": "example-user", "created_at": "2015-04-09T17:31:56Z", "modified_at": "2015-04-10T14:24:43Z", "raw_body": "**Example Comment Body**", "rendered_body": "<b>Example Comment Body</b>", "endorsed": true, "endorsed_by": "example-moderator", "endorsed_at": "2015-04-10T14:24:43Z", "flagged": false, "voted": true, "vote_count": 19, "children": [], "editable_fields": ["flagged", "voted"] }