Commit ebff94af by Greg Price

Add discussion topic API

This is the first stage of the new Discussion API.

JIRA: MA-604
parent 50e9b435
"""
Discussion API internal interface
"""
from collections import defaultdict
from django_comment_client.utils import get_accessible_discussion_modules
def get_course_topics(course, user):
"""
Return the course topic listing for the given course and user.
Parameters:
course: The course to get topics for
user: The requesting user, for access control
Returns:
A course topic listing dictionary; see discussion_api.views.CourseTopicViews
for more detail.
"""
def get_module_sort_key(module):
"""
Get the sort key for the module (falling back to the discussion_target
setting if absent)
"""
return module.sort_key or module.discussion_target
discussion_modules = get_accessible_discussion_modules(course, user)
modules_by_category = defaultdict(list)
for module in discussion_modules:
modules_by_category[module.discussion_category].append(module)
courseware_topics = [
{
"id": None,
"name": category,
"children": [
{
"id": module.discussion_id,
"name": module.discussion_target,
"children": [],
}
for module in sorted(modules_by_category[category], key=get_module_sort_key)
],
}
for category in sorted(modules_by_category.keys())
]
non_courseware_topics = [
{
"id": entry["id"],
"name": name,
"children": [],
}
for name, entry in sorted(
course.discussion_topics.items(),
key=lambda item: item[1].get("sort_key", item[0])
)
]
return {
"courseware_topics": courseware_topics,
"non_courseware_topics": non_courseware_topics,
}
"""
A models.py is required to make this an app (until we move to Django 1.7)
"""
"""
Tests for Discussion API views
"""
from datetime import datetime
import json
import mock
from pytz import UTC
from django.core.urlresolvers import reverse
from student.tests.factories import CourseEnrollmentFactory, UserFactory
from util.testing import UrlResetMixin
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.tabs import DiscussionTab
class CourseTopicsViewTest(UrlResetMixin, ModuleStoreTestCase):
"""Tests for CourseTopicsView"""
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
def setUp(self):
super(CourseTopicsViewTest, self).setUp()
self.maxDiff = None # pylint: disable=invalid-name
self.course = CourseFactory.create(
org="x",
course="y",
run="z",
start=datetime.now(UTC),
discussion_topics={"Test Topic": {"id": "test_topic"}}
)
self.password = "password"
self.user = UserFactory.create(password=self.password)
CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id)
self.url = reverse("course_topics", kwargs={"course_id": unicode(self.course.id)})
self.client.login(username=self.user.username, password=self.password)
def assert_response_correct(self, response, expected_status, expected_content):
"""
Assert that the response has the given status code and parsed content
"""
self.assertEqual(response.status_code, expected_status)
parsed_content = json.loads(response.content)
self.assertEqual(parsed_content, expected_content)
def test_not_authenticated(self):
self.client.logout()
response = self.client.get(self.url)
self.assert_response_correct(
response,
401,
{"developer_message": "Authentication credentials were not provided."}
)
def test_non_existent_course(self):
response = self.client.get(
reverse("course_topics", kwargs={"course_id": "non/existent/course"})
)
self.assert_response_correct(
response,
404,
{"developer_message": "Not found."}
)
def test_not_enrolled(self):
unenrolled_user = UserFactory.create(password=self.password)
self.client.login(username=unenrolled_user.username, password=self.password)
response = self.client.get(self.url)
self.assert_response_correct(
response,
404,
{"developer_message": "Not found."}
)
def test_discussions_disabled(self):
self.course.tabs = [tab for tab in self.course.tabs if not isinstance(tab, DiscussionTab)]
modulestore().update_item(self.course, self.user.id)
response = self.client.get(self.url)
self.assert_response_correct(
response,
404,
{"developer_message": "Not found."}
)
def test_get(self):
response = self.client.get(self.url)
self.assert_response_correct(
response,
200,
{
"courseware_topics": [],
"non_courseware_topics": [{
"id": "test_topic",
"name": "Test Topic",
"children": []
}],
}
)
"""
Discussion API URLs
"""
from django.conf import settings
from django.conf.urls import patterns, url
from discussion_api.views import CourseTopicsView
urlpatterns = patterns(
"discussion_api",
url(
r"^v1/course_topics/{}".format(settings.COURSE_ID_PATTERN),
CourseTopicsView.as_view(),
name="course_topics"
),
)
"""
Discussion API views
"""
from django.http import Http404
from rest_framework.authentication import OAuth2Authentication, SessionAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from opaque_keys.edx.locator import CourseLocator
from courseware.courses import get_course_with_access
from discussion_api.api import get_course_topics
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin
from xmodule.tabs import DiscussionTab
class CourseTopicsView(DeveloperErrorViewMixin, APIView):
"""
**Use Cases**
Retrieve the topic listing for a course. Only topics accessible to the
authenticated user are included.
**Example Requests**:
GET /api/discussion/v1/course_topics/{course_id}
**Response Values**:
* courseware_topics: The list of topic trees for courseware-linked
topics. Each item in the list includes:
* id: The id of the discussion topic (null for a topic that only
has children but cannot contain threads itself).
* name: The display name of the topic.
* children: A list of child subtrees of the same format.
* non_courseware_topics: The list of topic trees that are not linked to
courseware. Items are of the same format as in courseware_topics.
"""
authentication_classes = (OAuth2Authentication, SessionAuthentication)
permission_classes = (IsAuthenticated,)
def get(self, request, course_id):
"""Implements the GET method as described in the class docstring."""
course_key = CourseLocator.from_string(course_id)
course = get_course_with_access(request.user, 'load_forum', course_key)
if not any([isinstance(tab, DiscussionTab) for tab in course.tabs]):
raise Http404
return Response(get_course_topics(course, request.user))
...@@ -1657,6 +1657,7 @@ INSTALLED_APPS = ( ...@@ -1657,6 +1657,7 @@ INSTALLED_APPS = (
# Discussion forums # Discussion forums
'django_comment_client', 'django_comment_client',
'django_comment_common', 'django_comment_common',
'discussion_api',
'notes', 'notes',
'edxnotes', 'edxnotes',
......
...@@ -443,6 +443,7 @@ if settings.COURSEWARE_ENABLED: ...@@ -443,6 +443,7 @@ if settings.COURSEWARE_ENABLED:
# discussion forums live within courseware, so courseware must be enabled first # discussion forums live within courseware, so courseware must be enabled first
if settings.FEATURES.get('ENABLE_DISCUSSION_SERVICE'): if settings.FEATURES.get('ENABLE_DISCUSSION_SERVICE'):
urlpatterns += ( urlpatterns += (
url(r'^api/discussion/', include('discussion_api.urls')),
url(r'^courses/{}/discussion/'.format(settings.COURSE_ID_PATTERN), url(r'^courses/{}/discussion/'.format(settings.COURSE_ID_PATTERN),
include('django_comment_client.urls')), include('django_comment_client.urls')),
url(r'^notification_prefs/enable/', 'notification_prefs.views.ajax_enable'), url(r'^notification_prefs/enable/', 'notification_prefs.views.ajax_enable'),
......
"""
Utilities related to API views
"""
from django.http import Http404
from rest_framework.exceptions import APIException
from rest_framework.response import Response
class DeveloperErrorViewMixin(object):
"""
A view mixin to handle common error cases other than validation failure
(auth failure, method not allowed, etc.) by generating an error response
conforming to our API conventions with a developer message.
"""
def make_error_response(self, status_code, developer_message):
"""
Build an error response with the given status code and developer_message
"""
return Response({"developer_message": developer_message}, status=status_code)
def handle_exception(self, exc):
if isinstance(exc, APIException):
return self.make_error_response(exc.status_code, exc.detail)
elif isinstance(exc, Http404):
return self.make_error_response(404, "Not found.")
else:
raise
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment