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.
This commit is contained in:
@@ -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 <role>_<course>
|
||||
# if it exists, then use that one, otherwise use a <role>_<course_id> 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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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
|
||||
),
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<%!
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
from xmodule.modulestore.django import loc_mapper
|
||||
%>
|
||||
|
||||
<div class="wrapper-header wrapper" id="view-top">
|
||||
@@ -12,10 +13,16 @@
|
||||
<h1 class="branding"><a href="/"><img src="${static.url("img/logo-edx-studio.png")}" alt="edX Studio" /></a></h1>
|
||||
|
||||
% 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)}
|
||||
)
|
||||
%>
|
||||
<h2 class="info-course">
|
||||
<span class="sr">${_("Current Course:")}</span>
|
||||
<a class="course-link" href="${reverse('course_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">
|
||||
<a class="course-link" href="${index_url}">
|
||||
<span class="course-org">${context_course.display_org_with_default | h}</span><span class="course-number">${context_course.display_number_with_default | h}</span>
|
||||
<span class="course-title" title="${context_course.display_name_with_default}">${context_course.display_name_with_default}</span>
|
||||
</a>
|
||||
@@ -31,7 +38,7 @@
|
||||
<div class="nav-sub">
|
||||
<ul>
|
||||
<li class="nav-item nav-course-courseware-outline">
|
||||
<a href="${reverse('course_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">${_("Outline")}</a>
|
||||
<a href="${index_url}">${_("Outline")}</a>
|
||||
</li>
|
||||
<li class="nav-item nav-course-courseware-updates">
|
||||
<a href="${reverse('course_info', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">${_("Updates")}</a>
|
||||
|
||||
30
cms/urls.py
30
cms/urls.py
@@ -9,7 +9,7 @@ startup.run()
|
||||
from ratelimitbackend import admin
|
||||
admin.autodiscover()
|
||||
|
||||
urlpatterns = ('', # nopep8
|
||||
urlpatterns = patterns('', # nopep8
|
||||
url(r'^$', 'contentstore.views.howitworks', name='homepage'),
|
||||
url(r'^listing', 'contentstore.views.index', name='index'),
|
||||
url(r'^request_course_creator$', 'contentstore.views.request_course_creator', name='request_course_creator'),
|
||||
@@ -25,8 +25,6 @@ urlpatterns = ('', # nopep8
|
||||
url(r'^create_new_course', 'contentstore.views.create_new_course', name='create_new_course'),
|
||||
url(r'^reorder_static_tabs', 'contentstore.views.reorder_static_tabs', name='reorder_static_tabs'),
|
||||
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)$',
|
||||
'contentstore.views.course_index', name='course_index'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/import/(?P<name>[^/]+)$',
|
||||
'contentstore.views.import_course', name='import_course'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/import_status/(?P<name>[^/]+)$',
|
||||
@@ -106,7 +104,8 @@ urlpatterns = ('', # nopep8
|
||||
)
|
||||
|
||||
# User creation and updating views
|
||||
urlpatterns += (
|
||||
urlpatterns += patterns(
|
||||
'',
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/checklists/(?P<name>[^/]+)$', 'contentstore.views.get_checklists', name='checklists'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/checklists/(?P<name>[^/]+)/update(/)?(?P<checklist_index>.+)?.*$',
|
||||
'contentstore.views.update_checklist', name='checklists_updates'),
|
||||
@@ -125,22 +124,37 @@ urlpatterns += (
|
||||
url(r'^logout$', 'student.views.logout_user', name='logout'),
|
||||
)
|
||||
|
||||
# restful api
|
||||
urlpatterns += patterns(
|
||||
'contentstore.views',
|
||||
# index page, course outline page, and course structure json access
|
||||
# replaces url(r'^listing', 'contentstore.views.index', name='index'),
|
||||
# ? url(r'^create_new_course', 'contentstore.views.create_new_course', name='create_new_course')
|
||||
# TODO remove shim and this pattern once import_export and test_contentstore no longer use
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)$',
|
||||
'course.old_course_index_shim', name='course_index'
|
||||
),
|
||||
|
||||
url(r'^course$', 'index'),
|
||||
url(r'^course/(?P<course_url>.*)$', 'course_handler'),
|
||||
)
|
||||
|
||||
js_info_dict = {
|
||||
'domain': 'djangojs',
|
||||
'packages': ('cms',),
|
||||
}
|
||||
|
||||
urlpatterns += (
|
||||
urlpatterns += patterns('',
|
||||
# Serve catalog of localized strings to be rendered by Javascript
|
||||
url(r'^i18n.js$', 'django.views.i18n.javascript_catalog', js_info_dict),
|
||||
)
|
||||
|
||||
if settings.MITX_FEATURES.get('ENABLE_SERVICE_STATUS'):
|
||||
urlpatterns += (
|
||||
urlpatterns += patterns('',
|
||||
url(r'^status/', include('service_status.urls')),
|
||||
)
|
||||
|
||||
urlpatterns += (url(r'^admin/', include(admin.site.urls)),)
|
||||
urlpatterns += patterns('', url(r'^admin/', include(admin.site.urls)),)
|
||||
|
||||
# enable automatic login
|
||||
if settings.MITX_FEATURES.get('AUTOMATIC_AUTH_FOR_TESTING'):
|
||||
@@ -155,8 +169,6 @@ if settings.DEBUG:
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
urlpatterns = patterns(*urlpatterns)
|
||||
|
||||
# Custom error pages
|
||||
#pylint: disable=C0103
|
||||
handler404 = 'contentstore.views.render_404'
|
||||
|
||||
Reference in New Issue
Block a user