Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend the require_http_methods decorator. For example--

@require_http_methods(("DELETE", "GET", "PUT", "POST", "PATCH"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would still like to see this.

"""
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