Skip to content

Commit

Permalink
Merge pull request #6046 from open-craft/content_libraries/2-studio-l…
Browse files Browse the repository at this point in the history
…ib-support

Studio Support for Content Libraries (SOL-1, SOL-2, & SOL-3)
  • Loading branch information
antoviaque committed Dec 4, 2014
2 parents b0662e7 + 4cc7b75 commit b7c340b
Show file tree
Hide file tree
Showing 43 changed files with 1,944 additions and 282 deletions.
7 changes: 7 additions & 0 deletions cms/djangoapps/contentstore/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,13 @@ def reverse_course_url(handler_name, course_key, kwargs=None):
return reverse_url(handler_name, 'course_key_string', course_key, kwargs)


def reverse_library_url(handler_name, library_key, kwargs=None):
"""
Creates the URL for handlers that use library_keys as URL parameters.
"""
return reverse_url(handler_name, 'library_key_string', library_key, kwargs)


def reverse_usage_url(handler_name, usage_key, kwargs=None):
"""
Creates the URL for handlers that use usage_keys as URL parameters.
Expand Down
1 change: 1 addition & 0 deletions cms/djangoapps/contentstore/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from .helpers import *
from .item import *
from .import_export import *
from .library import *
from .preview import *
from .public import *
from .export_git import *
Expand Down
10 changes: 10 additions & 0 deletions cms/djangoapps/contentstore/views/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@
ADVANCED_PROBLEM_TYPES = settings.ADVANCED_PROBLEM_TYPES


CONTAINER_TEMPATES = [
"basic-modal", "modal-button", "edit-xblock-modal",
"editor-mode-button", "upload-dialog", "image-modal",
"add-xblock-component", "add-xblock-component-button", "add-xblock-component-menu",
"add-xblock-component-menu-problem", "xblock-string-field-editor", "publish-xblock", "publish-history",
"unit-outline", "container-message"
]


def _advanced_component_types():
"""
Return advanced component types which can be created.
Expand Down Expand Up @@ -216,6 +225,7 @@ def container_handler(request, usage_key_string):
'xblock_info': xblock_info,
'draft_preview_link': preview_lms_link,
'published_preview_link': lms_link,
'templates': CONTAINER_TEMPATES
})
else:
return HttpResponseBadRequest("Only supports HTML requests")
Expand Down
26 changes: 26 additions & 0 deletions cms/djangoapps/contentstore/views/course.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
add_extra_panel_tab,
remove_extra_panel_tab,
reverse_course_url,
reverse_library_url,
reverse_usage_url,
reverse_url,
remove_all_instructors,
Expand All @@ -56,6 +57,7 @@
ADVANCED_COMPONENT_TYPES,
)
from contentstore.tasks import rerun_course
from .library import LIBRARIES_ENABLED
from .item import create_xblock_info
from course_creators.views import get_course_creator_status, add_user_with_status_unrequested
from contentstore import utils
Expand Down Expand Up @@ -341,6 +343,14 @@ def _accessible_courses_list_from_groups(request):
return courses_list.values(), in_process_course_actions


def _accessible_libraries_list(user):
"""
List all libraries available to the logged in user by iterating through all libraries
"""
# No need to worry about ErrorDescriptors - split's get_libraries() never returns them.
return [lib for lib in modulestore().get_libraries() if has_course_access(user, lib.location)]


@login_required
@ensure_csrf_cookie
def course_listing(request):
Expand All @@ -360,6 +370,8 @@ def course_listing(request):
# so fallback to iterating through all courses
courses, in_process_course_actions = _accessible_courses_list(request)

libraries = _accessible_libraries_list(request.user) if LIBRARIES_ENABLED else []

def format_course_for_view(course):
"""
Return a dict of the data which the view requires for each course
Expand Down Expand Up @@ -396,6 +408,18 @@ def format_in_process_course_view(uca):
) if uca.state == CourseRerunUIStateManager.State.FAILED else ''
}

def format_library_for_view(library):
"""
Return a dict of the data which the view requires for each library
"""
return {
'display_name': library.display_name,
'library_key': unicode(library.location.library_key),
'url': reverse_library_url('library_handler', unicode(library.location.library_key)),
'org': library.display_org_with_default,
'number': library.display_number_with_default,
}

# remove any courses in courses that are also in the in_process_course_actions list
in_process_action_course_keys = [uca.course_key for uca in in_process_course_actions]
courses = [
Expand All @@ -409,6 +433,8 @@ def format_in_process_course_view(uca):
return render_to_response('index.html', {
'courses': courses,
'in_process_course_actions': in_process_course_actions,
'libraries_enabled': LIBRARIES_ENABLED,
'libraries': [format_library_for_view(lib) for lib in libraries],
'user': request.user,
'request_course_creator_url': reverse('contentstore.views.request_course_creator'),
'course_creator_status': _get_course_creator_status(request.user),
Expand Down
5 changes: 4 additions & 1 deletion cms/djangoapps/contentstore/views/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from edxmako.shortcuts import render_to_string, render_to_response
from xblock.core import XBlock
from xmodule.modulestore.django import modulestore
from contentstore.utils import reverse_course_url, reverse_usage_url
from contentstore.utils import reverse_course_url, reverse_library_url, reverse_usage_url

__all__ = ['edge', 'event', 'landing']

Expand Down Expand Up @@ -106,6 +106,9 @@ def xblock_studio_url(xblock, parent_xblock=None):
url=reverse_course_url('course_handler', xblock.location.course_key),
usage_key=urllib.quote(unicode(xblock.location))
)
elif category == 'library':
library_key = xblock.location.course_key
return reverse_library_url('library_handler', library_key)
else:
return reverse_usage_url('container_handler', xblock.location)

Expand Down
19 changes: 13 additions & 6 deletions cms/djangoapps/contentstore/views/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
from models.settings.course_grading import CourseGradingModel
from cms.lib.xblock.runtime import handler_url, local_resource_url
from opaque_keys.edx.keys import UsageKey, CourseKey
from opaque_keys.edx.locator import LibraryUsageLocator

__all__ = ['orphan_handler', 'xblock_handler', 'xblock_view_handler', 'xblock_outline_handler']

Expand Down Expand Up @@ -649,7 +650,9 @@ def _get_module_info(xblock, rewrite_static_links=True):
)

# Pre-cache has changes for the entire course because we'll need it for the ancestor info
modulestore().has_changes(modulestore().get_course(xblock.location.course_key, depth=None))
# Except library blocks which don't [yet] use draft/publish
if not isinstance(xblock.location, LibraryUsageLocator):
modulestore().has_changes(modulestore().get_course(xblock.location.course_key, depth=None))

# Note that children aren't being returned until we have a use case.
return create_xblock_info(xblock, data=data, metadata=own_metadata(xblock), include_ancestor_info=True)
Expand Down Expand Up @@ -690,12 +693,16 @@ def safe_get_username(user_id):

return None

is_library_block = isinstance(xblock.location, LibraryUsageLocator)
is_xblock_unit = is_unit(xblock, parent_xblock)
# this should not be calculated for Sections and Subsections on Unit page
has_changes = modulestore().has_changes(xblock) if (is_xblock_unit or course_outline) else None
# this should not be calculated for Sections and Subsections on Unit page or for library blocks
has_changes = modulestore().has_changes(xblock) if (is_xblock_unit or course_outline) and not is_library_block else None

if graders is None:
graders = CourseGradingModel.fetch(xblock.location.course_key).graders
if not is_library_block:
graders = CourseGradingModel.fetch(xblock.location.course_key).graders
else:
graders = []

# Compute the child info first so it can be included in aggregate information for the parent
should_visit_children = include_child_info and (course_outline and not is_xblock_unit or not course_outline)
Expand All @@ -715,15 +722,15 @@ def safe_get_username(user_id):
visibility_state = _compute_visibility_state(xblock, child_info, is_xblock_unit and has_changes)
else:
visibility_state = None
published = modulestore().has_published_version(xblock)
published = modulestore().has_published_version(xblock) if not is_library_block else None

xblock_info = {
"id": unicode(xblock.location),
"display_name": xblock.display_name_with_default,
"category": xblock.category,
"edited_on": get_default_time_display(xblock.subtree_edited_on) if xblock.subtree_edited_on else None,
"published": published,
"published_on": get_default_time_display(xblock.published_on) if xblock.published_on else None,
"published_on": get_default_time_display(xblock.published_on) if published and xblock.published_on else None,
"studio_url": xblock_studio_url(xblock, parent_xblock),
"released_to_students": datetime.now(UTC) > xblock.start,
"release_date": release_date,
Expand Down
185 changes: 185 additions & 0 deletions cms/djangoapps/contentstore/views/library.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
"""
Views related to content libraries.
A content library is a structure containing XBlocks which can be re-used in the
multiple courses.
"""
from __future__ import absolute_import

import json
import logging

from contentstore.views.item import create_xblock_info
from contentstore.utils import reverse_library_url
from django.http import HttpResponseNotAllowed, Http404
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django.conf import settings
from django.utils.translation import ugettext as _
from django.views.decorators.http import require_http_methods
from django_future.csrf import ensure_csrf_cookie
from edxmako.shortcuts import render_to_response
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locator import LibraryLocator, LibraryUsageLocator
from xmodule.modulestore.exceptions import DuplicateCourseError
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore

from .access import has_course_access
from .component import get_component_templates, CONTAINER_TEMPATES
from student.roles import CourseCreatorRole
from student import auth
from util.json_request import expect_json, JsonResponse, JsonResponseBadRequest

__all__ = ['library_handler']

log = logging.getLogger(__name__)

LIBRARIES_ENABLED = settings.FEATURES.get('ENABLE_CONTENT_LIBRARIES', False)


@login_required
@ensure_csrf_cookie
@require_http_methods(('GET', 'POST'))
def library_handler(request, library_key_string=None):
"""
RESTful interface to most content library related functionality.
"""
if not LIBRARIES_ENABLED:
log.exception("Attempted to use the content library API when the libraries feature is disabled.")
raise Http404 # Should never happen because we test the feature in urls.py also

if library_key_string is not None and request.method == 'POST':
return HttpResponseNotAllowed(("POST",))

if request.method == 'POST':
return _create_library(request)

# request method is get, since only GET and POST are allowed by @require_http_methods(('GET', 'POST'))
if library_key_string:
return _display_library(library_key_string, request)

return _list_libraries(request)


def _display_library(library_key_string, request):
"""
Displays single library
"""
library_key = CourseKey.from_string(library_key_string)
if not isinstance(library_key, LibraryLocator):
log.exception("Non-library key passed to content libraries API.") # Should never happen due to url regex
raise Http404 # This is not a library
if not has_course_access(request.user, library_key):
log.exception(u"User %s tried to access library %s without permission", request.user.username, unicode(library_key))
raise PermissionDenied()

library = modulestore().get_library(library_key)
if library is None:
log.exception(u"Library %s not found", unicode(library_key))
raise Http404

response_format = 'html'
if request.REQUEST.get('format', 'html') == 'json' or 'application/json' in request.META.get('HTTP_ACCEPT', 'text/html'):
response_format = 'json'

return library_blocks_view(library, response_format)


def _list_libraries(request):
"""
List all accessible libraries
"""
lib_info = [
{
"display_name": lib.display_name,
"library_key": unicode(lib.location.library_key),
}
for lib in modulestore().get_libraries()
if has_course_access(request.user, lib.location.library_key)
]
return JsonResponse(lib_info)


@expect_json
def _create_library(request):
"""
Helper method for creating a new library.
"""
if not auth.has_access(request.user, CourseCreatorRole()):
log.exception(u"User %s tried to create a library without permission", request.user.username)
raise PermissionDenied()
display_name = None
try:
display_name = request.json['display_name']
org = request.json['org']
library = request.json.get('number', None)
if library is None:
library = request.json['library']
store = modulestore()
with store.default_store(ModuleStoreEnum.Type.split):
new_lib = store.create_library(
org=org,
library=library,
user_id=request.user.id,
fields={"display_name": display_name},
)
except KeyError as error:
log.exception("Unable to create library - missing required JSON key.")
return JsonResponseBadRequest({
"ErrMsg": _("Unable to create library - missing required field '{field}'".format(field=error.message))
})
except InvalidKeyError as error:
log.exception("Unable to create library - invalid key.")
return JsonResponseBadRequest({
"ErrMsg": _("Unable to create library '{name}'.\n\n{err}").format(name=display_name, err=error.message)}
)
except DuplicateCourseError:
log.exception("Unable to create library - one already exists with the same key.")
return JsonResponseBadRequest({
'ErrMsg': _(
'There is already a library defined with the same '
'organization and library code. Please '
'change either organization or library code to be unique.'
)
})

lib_key_str = unicode(new_lib.location.library_key)
return JsonResponse({
'url': reverse_library_url('library_handler', lib_key_str),
'library_key': lib_key_str,
})


def library_blocks_view(library, response_format):
"""
The main view of a course's content library.
Shows all the XBlocks in the library, and allows adding/editing/deleting
them.
Can be called with response_format="json" to get a JSON-formatted list of
the XBlocks in the library along with library metadata.
"""
assert isinstance(library.location.library_key, LibraryLocator)
assert isinstance(library.location, LibraryUsageLocator)

children = library.children
if response_format == "json":
# The JSON response for this request is short and sweet:
prev_version = library.runtime.course_entry.structure['previous_version']
return JsonResponse({
"display_name": library.display_name,
"library_id": unicode(library.course_id),
"version": unicode(library.runtime.course_entry.course_key.version),
"previous_version": unicode(prev_version) if prev_version else None,
"blocks": [unicode(x) for x in children],
})

xblock_info = create_xblock_info(library, include_ancestor_info=False, graders=[])
component_templates = get_component_templates(library)

return render_to_response('library.html', {
'context_library': library,
'component_templates': json.dumps(component_templates),
'xblock_info': xblock_info,
'templates': CONTAINER_TEMPATES
})
Loading

0 comments on commit b7c340b

Please sign in to comment.