library.py 9.08 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
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
19
from django.views.decorators.csrf import ensure_csrf_cookie
20 21 22 23 24 25 26
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
27
from .user import user_with_role
28

29
from .component import get_component_templates, CONTAINER_TEMPLATES
E. Kolpakov committed
30 31 32
from student.auth import (
    STUDIO_VIEW_USERS, STUDIO_EDIT_ROLES, get_user_permissions, has_studio_read_access, has_studio_write_access
)
33
from student.roles import CourseInstructorRole, CourseStaffRole, LibraryUserRole
34 35 36
from student import auth
from util.json_request import expect_json, JsonResponse, JsonResponseBadRequest

37
__all__ = ['library_handler', 'manage_library_users']
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 75

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

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


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()
108
        if has_studio_read_access(request.user, lib.location.library_key)
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132
    ]
    return JsonResponse(lib_info)


@expect_json
def _create_library(request):
    """
    Helper method for creating a new library.
    """
    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},
            )
133 134
        # Give the user admin ("Instructor") role for this library:
        add_instructor(new_lib.location.library_key, request.user, request.user)
135 136 137
    except KeyError as error:
        log.exception("Unable to create library - missing required JSON key.")
        return JsonResponseBadRequest({
138
            "ErrMsg": _("Unable to create library - missing required field '{field}'").format(field=error.message)
139 140 141 142
        })
    except InvalidKeyError as error:
        log.exception("Unable to create library - invalid key.")
        return JsonResponseBadRequest({
E. Kolpakov committed
143 144
            "ErrMsg": _("Unable to create library '{name}'.\n\n{err}").format(name=display_name, err=error.message)
        })
145 146 147 148 149 150
    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 '
151
                'change your library code so that it is unique within your organization.'
152 153 154 155 156 157 158 159 160 161
            )
        })

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


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

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

187 188
    can_edit = has_studio_write_access(user, library.location.library_key)

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

    return render_to_response('library.html', {
193
        'can_edit': can_edit,
194 195 196
        'context_library': library,
        'component_templates': json.dumps(component_templates),
        'xblock_info': xblock_info,
197
        'templates': CONTAINER_TEMPLATES,
198
    })
199 200 201 202 203 204 205 206 207 208 209 210


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
211
    if not user_perms & STUDIO_VIEW_USERS:
212 213 214 215 216 217 218 219 220
        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
221 222 223 224 225 226 227 228

    formatted_users = []
    for user in instructors:
        formatted_users.append(user_with_role(user, 'instructor'))
    for user in staff:
        formatted_users.append(user_with_role(user, 'staff'))
    for user in users:
        formatted_users.append(user_with_role(user, 'library_user'))
229 230 231

    return render_to_response('manage_users_lib.html', {
        'context_library': library,
232
        'users': formatted_users,
233 234 235
        '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),
236
        'show_children_previews': library.show_children_previews
237
    })