library.py 9.12 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11
"""
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
12
from contentstore.utils import reverse_library_url, add_instructor
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
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 .component import get_component_templates, CONTAINER_TEMPATES
E. Kolpakov committed
29 30 31
from student.auth import (
    STUDIO_VIEW_USERS, STUDIO_EDIT_ROLES, get_user_permissions, has_studio_read_access, has_studio_write_access
)
32
from student.roles import CourseCreatorRole, CourseInstructorRole, CourseStaffRole, LibraryUserRole
33 34 35
from student import auth
from util.json_request import expect_json, JsonResponse, JsonResponseBadRequest

36
__all__ = ['library_handler', 'manage_library_users']
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74

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
75
    if not has_studio_read_access(request.user, library_key):
E. Kolpakov committed
76 77 78 79
        log.exception(
            u"User %s tried to access library %s without permission",
            request.user.username, unicode(library_key)
        )
80 81 82 83 84 85 86 87
        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'
E. Kolpakov committed
88 89 90 91
    if (
            request.REQUEST.get('format', 'html') == 'json' or
            'application/json' in request.META.get('HTTP_ACCEPT', 'text/html')
    ):
92 93
        response_format = 'json'

94
    return library_blocks_view(library, request.user, response_format)
95 96 97 98 99 100 101 102 103 104 105 106


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()
107
        if has_studio_read_access(request.user, lib.location.library_key)
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
    ]
    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},
            )
135 136
        # Give the user admin ("Instructor") role for this library:
        add_instructor(new_lib.location.library_key, request.user, request.user)
137 138 139
    except KeyError as error:
        log.exception("Unable to create library - missing required JSON key.")
        return JsonResponseBadRequest({
140
            "ErrMsg": _("Unable to create library - missing required field '{field}'").format(field=error.message)
141 142 143 144
        })
    except InvalidKeyError as error:
        log.exception("Unable to create library - invalid key.")
        return JsonResponseBadRequest({
E. Kolpakov committed
145 146
            "ErrMsg": _("Unable to create library '{name}'.\n\n{err}").format(name=display_name, err=error.message)
        })
147 148 149 150 151 152
    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 '
153
                'change your library code so that it is unique within your organization.'
154 155 156 157 158 159 160 161 162 163
            )
        })

    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,
    })


164
def library_blocks_view(library, user, response_format):
165 166 167 168 169 170
    """
    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.
171 172

    Assumes that read permissions have been checked before calling this.
173 174 175 176 177 178 179 180 181 182
    """
    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,
183
            "library_id": unicode(library.location.library_key),
184 185 186 187 188
            "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],
        })

189 190
    can_edit = has_studio_write_access(user, library.location.library_key)

191
    xblock_info = create_xblock_info(library, include_ancestor_info=False, graders=[])
192
    component_templates = get_component_templates(library, library=True) if can_edit else []
193 194

    return render_to_response('library.html', {
195
        'can_edit': can_edit,
196 197 198
        'context_library': library,
        'component_templates': json.dumps(component_templates),
        'xblock_info': xblock_info,
199 200
        'templates': CONTAINER_TEMPATES,
        'lib_users_url': reverse_library_url('manage_library_users', unicode(library.location.library_key)),
201
    })
202 203 204 205 206 207 208 209 210 211 212 213


def manage_library_users(request, library_key_string):
    """
    Studio UI for editing the users within a library.

    Uses the /course_team/:library_key/:user_email/ REST API to make changes.
    """
    library_key = CourseKey.from_string(library_key_string)
    if not isinstance(library_key, LibraryLocator):
        raise Http404  # This is not a library
    user_perms = get_user_permissions(request.user, library_key)
E. Kolpakov committed
214
    if not user_perms & STUDIO_VIEW_USERS:
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
        raise PermissionDenied()
    library = modulestore().get_library(library_key)
    if library is None:
        raise Http404

    # Segment all the users explicitly associated with this library, ensuring each user only has one role listed:
    instructors = set(CourseInstructorRole(library_key).users_with_role())
    staff = set(CourseStaffRole(library_key).users_with_role()) - instructors
    users = set(LibraryUserRole(library_key).users_with_role()) - instructors - staff
    all_users = instructors | staff | users

    return render_to_response('manage_users_lib.html', {
        'context_library': library,
        'staff': staff,
        'instructors': instructors,
        'users': users,
        'all_users': all_users,
        'allow_actions': bool(user_perms & STUDIO_EDIT_ROLES),
        'library_key': unicode(library_key),
        'lib_users_url': reverse_library_url('manage_library_users', library_key_string),
    })