From 010905eb996bc4e2f6391bf115e74d4cbaf6a00a Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Thu, 3 Oct 2013 12:06:26 -0400 Subject: [PATCH] RESTful api for getting course listing and opening course in studio. Pattern for how to do refactoring from locations to locators and from old style urls to restful ones. --- cms/djangoapps/auth/authz.py | 29 ++-- cms/djangoapps/contentstore/views/access.py | 8 +- cms/djangoapps/contentstore/views/course.py | 80 ++++++++-- cms/djangoapps/contentstore/views/user.py | 12 +- cms/templates/widgets/header.html | 13 +- cms/urls.py | 30 ++-- .../xmodule/modulestore/loc_mapper_store.py | 2 +- .../xmodule/xmodule/modulestore/locator.py | 144 +++++++++++++++--- .../xmodule/xmodule/modulestore/parsers.py | 13 +- .../tests/test_split_modulestore.py | 18 +-- .../data/splitmongo_json/definitions.json | 50 +++--- .../test/data/splitmongo_json/structures.json | 24 +-- 12 files changed, 310 insertions(+), 113 deletions(-) diff --git a/cms/djangoapps/auth/authz.py b/cms/djangoapps/auth/authz.py index 4923851445..7ecddae274 100644 --- a/cms/djangoapps/auth/authz.py +++ b/cms/djangoapps/auth/authz.py @@ -1,13 +1,16 @@ +#======================================================================================================================= +# +# This code is somewhat duplicative of access.py in the LMS. We will unify the code as a separate story +# but this implementation should be data compatible with the LMS implementation +# +#======================================================================================================================= from django.contrib.auth.models import User, Group from django.core.exceptions import PermissionDenied from django.conf import settings from xmodule.modulestore import Location +from xmodule.modulestore.locator import CourseLocator, Locator -''' -This code is somewhat duplicative of access.py in the LMS. We will unify the code as a separate story -but this implementation should be data compatible with the LMS implementation -''' # define a couple of simple roles, we just need ADMIN and EDITOR now for our purposes INSTRUCTOR_ROLE_NAME = 'instructor' @@ -22,16 +25,22 @@ COURSE_CREATOR_GROUP_NAME = "course_creator_group" def get_course_groupname_for_role(location, role): - loc = Location(location) + location = Locator.to_locator_or_location(location) + # hack: check for existence of a group name in the legacy LMS format _ # if it exists, then use that one, otherwise use a _ which contains # more information - groupname = '{0}_{1}'.format(role, loc.course) + groupnames = [] + groupnames.append('{0}_{1}'.format(role, location.course_id)) + if isinstance(location, Location): + groupnames.append('{0}_{1}'.format(role, location.course)) + elif isinstance(location, CourseLocator): + groupnames.append('{0}_{1}'.format(role, location.as_old_location_course_id)) - if len(Group.objects.filter(name=groupname)) == 0: - groupname = '{0}_{1}'.format(role, loc.course_id) - - return groupname + for groupname in groupnames: + if Group.objects.filter(name=groupname).exists(): + return groupname + return groupnames[0] def get_users_in_course_group_by_role(location, role): diff --git a/cms/djangoapps/contentstore/views/access.py b/cms/djangoapps/contentstore/views/access.py index 5cb6b8c6f4..3c75215bd1 100644 --- a/cms/djangoapps/contentstore/views/access.py +++ b/cms/djangoapps/contentstore/views/access.py @@ -3,6 +3,7 @@ from auth.authz import is_user_in_course_group_role from django.core.exceptions import PermissionDenied from ..utils import get_course_location_for_item from xmodule.modulestore import Location +from xmodule.modulestore.locator import CourseLocator def get_location_and_verify_access(request, org, course, name): @@ -29,13 +30,14 @@ def has_access(user, location, role=STAFF_ROLE_NAME): will not be in both INSTRUCTOR and STAFF groups, so we have to cascade our queries here as INSTRUCTOR has all the rights that STAFF do ''' - course_location = get_course_location_for_item(location) - _has_access = is_user_in_course_group_role(user, course_location, role) + if not isinstance(location, CourseLocator): + location = get_course_location_for_item(location) + _has_access = is_user_in_course_group_role(user, location, role) # if we're not in STAFF, perhaps we're in INSTRUCTOR groups if not _has_access and role == STAFF_ROLE_NAME: _has_access = is_user_in_course_group_role( user, - course_location, + location, INSTRUCTOR_ROLE_NAME ) return _has_access diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index 772dfd2778..f043c09d73 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -12,11 +12,11 @@ from django.conf import settings from django.views.decorators.http import require_http_methods, require_POST from django.core.exceptions import PermissionDenied from django.core.urlresolvers import reverse -from django.http import HttpResponseBadRequest +from django.http import HttpResponseBadRequest, HttpResponseNotFound from util.json_request import JsonResponse from mitxmako.shortcuts import render_to_response -from xmodule.modulestore.django import modulestore +from xmodule.modulestore.django import modulestore, loc_mapper from xmodule.modulestore.inheritance import own_metadata from xmodule.contentstore.content import StaticContent @@ -48,7 +48,8 @@ from django_comment_common.utils import seed_permissions_roles from student.models import CourseEnrollment from xmodule.html_module import AboutDescriptor -__all__ = ['course_index', 'create_new_course', 'course_info', +from xmodule.modulestore.locator import BlockUsageLocator +__all__ = ['create_new_course', 'course_info', 'course_handler', 'course_info_updates', 'get_course_settings', 'course_config_graders_page', 'course_config_advanced_page', @@ -58,25 +59,84 @@ __all__ = ['course_index', 'create_new_course', 'course_info', 'create_textbook'] +@login_required +def course_handler(request, course_url): + """ + The restful handler for course specific requests. + It provides the course tree with the necessary information for identifying and labeling the parts. The root + will typically be a 'course' object but may not be especially as we support modules. + + GET + html: return html page overview for the given course + json: return json representing the course branch's index entry as well as dag w/ all of the children + replaced w/ json docs where each doc has {'_id': , 'display_name': , 'children': } + POST + json: create (or update?) this course or branch in this course for this user, return resulting json + descriptor (same as in GET course/...). Leaving off /branch/draft would imply create the course w/ default + branches. Cannot change the structure contents ('_id', 'display_name', 'children') but can change the + index entry. + PUT + json: update this course (index entry not xblock) such as repointing head, changing display name, org, + course_id, prettyid. Return same json as above. + DELETE + json: delete this branch from this course (leaving off /branch/draft would imply delete the course) + """ + if 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'): + if request.method == 'GET': + raise NotImplementedError('coming soon') + elif not has_access(request.user, BlockUsageLocator(course_url)): + raise PermissionDenied() + elif request.method == 'POST': + raise NotImplementedError() + elif request.method == 'PUT': + raise NotImplementedError() + elif request.method == 'DELETE': + raise NotImplementedError() + else: + return HttpResponseBadRequest() + elif request.method == 'GET': # assume html + return course_index(request, course_url) + else: + return HttpResponseNotFound() + + @login_required @ensure_csrf_cookie -def course_index(request, org, course, name): +def old_course_index_shim(request, org, course, name): + """ + A shim for any unconverted uses of course_index + """ + old_location = Location(['i4x', org, course, 'course', name]) + locator = loc_mapper().translate_location(old_location.course_id, old_location, False, True) + return course_index(request, locator) + + +@login_required +@ensure_csrf_cookie +def course_index(request, course_url): """ Display an editable course overview. org, course, name: Attributes of the Location for the item to edit """ - location = get_location_and_verify_access(request, org, course, name) + location = BlockUsageLocator(course_url) + # TODO: when converting to split backend, if location does not have a usage_id, + # we'll need to get the course's root block_id + if not has_access(request.user, location): + raise PermissionDenied() - lms_link = get_lms_link_for_item(location) + + old_location = loc_mapper().translate_locator_to_location(location) + + lms_link = get_lms_link_for_item(old_location) upload_asset_callback_url = reverse('upload_asset', kwargs={ - 'org': org, - 'course': course, - 'coursename': name + 'org': location.as_old_location_org, + 'course': location.as_old_location_course, + 'coursename': location.as_old_location_run }) - course = modulestore().get_item(location, depth=3) + course = modulestore().get_item(old_location, depth=3) sections = course.get_children() return render_to_response('overview.html', { diff --git a/cms/djangoapps/contentstore/views/user.py b/cms/djangoapps/contentstore/views/user.py index 9414bfb9e8..d307e1295e 100644 --- a/cms/djangoapps/contentstore/views/user.py +++ b/cms/djangoapps/contentstore/views/user.py @@ -11,7 +11,7 @@ from django_future.csrf import ensure_csrf_cookie from mitxmako.shortcuts import render_to_response from django.core.context_processors import csrf -from xmodule.modulestore.django import modulestore +from xmodule.modulestore.django import modulestore, loc_mapper from xmodule.modulestore import Location from xmodule.error_module import ErrorDescriptor from contentstore.utils import get_lms_link_for_item @@ -46,13 +46,13 @@ def index(request): courses = filter(course_filter, courses) def format_course_for_view(course): + # published = false b/c studio manipulates draft versions not b/c the course isn't pub'd + course_url = loc_mapper().translate_location( + course.location.course_id, course.location, published=False, add_entry_if_missing=True + ) return ( course.display_name, - reverse("course_index", kwargs={ - 'org': course.location.org, - 'course': course.location.course, - 'name': course.location.name, - }), + reverse("contentstore.views.course_handler", kwargs={'course_url': course_url}), get_lms_link_for_item( course.location ), diff --git a/cms/templates/widgets/header.html b/cms/templates/widgets/header.html index f5829e4e1f..3be0b3723a 100644 --- a/cms/templates/widgets/header.html +++ b/cms/templates/widgets/header.html @@ -2,6 +2,7 @@ <%! from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _ + from xmodule.modulestore.django import loc_mapper %>
@@ -12,10 +13,16 @@

edX Studio

% if context_course: - <% ctx_loc = context_course.location %> + <% + ctx_loc = context_course.location + index_url = reverse( + 'contentstore.views.course_handler', + kwargs={'course_url': loc_mapper().translate_location(ctx_loc.course_id, ctx_loc, False, True)} + ) + %>

${_("Current Course:")} - + ${context_course.display_org_with_default | h}${context_course.display_number_with_default | h} ${context_course.display_name_with_default} @@ -31,7 +38,7 @@