preview.py 8.3 KB
Newer Older
1 2
from __future__ import absolute_import

Steve Strassmann committed
3
import logging
Steve Strassmann committed
4 5
from functools import partial

6
from django.conf import settings
Steve Strassmann committed
7
from django.core.urlresolvers import reverse
8
from django.http import Http404, HttpResponseBadRequest
9
from django.contrib.auth.decorators import login_required
10
from edxmako.shortcuts import render_to_string
Steve Strassmann committed
11

12
from xmodule_modifiers import replace_static_urls, wrap_xblock, wrap_fragment
13
from xmodule.x_module import PREVIEW_VIEWS, STUDENT_VIEW, AUTHOR_VIEW
Steve Strassmann committed
14 15
from xmodule.error_module import ErrorDescriptor
from xmodule.exceptions import NotFoundError, ProcessingError
16
from xmodule.modulestore.django import modulestore, ModuleI18nService
17
from opaque_keys.edx.keys import UsageKey
Steve Strassmann committed
18
from xmodule.x_module import ModuleSystem
19
from xblock.runtime import KvsFieldData
20 21
from xblock.django.request import webob_to_django_response, django_to_webob_request
from xblock.exceptions import NoSuchHandlerError
22
from xblock.fragment import Fragment
Steve Strassmann committed
23

24
from lms.lib.xblock.field_data import LmsFieldData
25
from cms.lib.xblock.field_data import CmsFieldData
26
from cms.lib.xblock.runtime import local_resource_url
Calen Pennington committed
27

28 29
from util.sandboxing import can_execute_unsafe_code

30
import static_replace
Steve Strassmann committed
31
from .session_kv_store import SessionKeyValueStore
32
from .helpers import render_from_lms
Steve Strassmann committed
33

34 35
from contentstore.views.access import get_user_role

36
__all__ = ['preview_handler']
Steve Strassmann committed
37

Steve Strassmann committed
38
log = logging.getLogger(__name__)
39

Steve Strassmann committed
40

41
@login_required
42
def preview_handler(request, usage_key_string, handler, suffix=''):
43
    """
44 45
    Dispatch an AJAX action to an xblock

46
    usage_key_string: The usage_key_string-id of the block to dispatch to, passed through `quote_slashes`
47
    handler: The handler to execute
48
    suffix: The remainder of the url to be passed to the handler
49
    """
50
    usage_key = UsageKey.from_string(usage_key_string)
51

52
    descriptor = modulestore().get_item(usage_key)
53
    instance = _load_preview_module(request, descriptor)
54
    # Let the module handle the AJAX
55
    req = django_to_webob_request(request)
56
    try:
57 58 59 60 61
        resp = instance.handle(handler, req, suffix)

    except NoSuchHandlerError:
        log.exception("XBlock %s attempted to access missing handler %r", instance, handler)
        raise Http404
62 63 64 65 66 67 68 69 70 71

    except NotFoundError:
        log.exception("Module indicating to user that request doesn't exist")
        raise Http404

    except ProcessingError:
        log.warning("Module raised an error while processing AJAX request",
                    exc_info=True)
        return HttpResponseBadRequest()

72
    except Exception:
73 74 75
        log.exception("error processing ajax call")
        raise

76
    return webob_to_django_response(resp)
77

Steve Strassmann committed
78

79 80 81 82
class PreviewModuleSystem(ModuleSystem):  # pylint: disable=abstract-method
    """
    An XModule ModuleSystem for use in Studio previews
    """
83 84 85 86
    # xmodules can check for this attribute during rendering to determine if
    # they are being rendered for preview (i.e. in Studio)
    is_author_mode = True

87
    def handler_url(self, block, handler_name, suffix='', query='', thirdparty=False):
88
        return reverse('preview_handler', kwargs={
89
            'usage_key_string': unicode(block.location),
90 91 92
            'handler': handler_name,
            'suffix': suffix,
        }) + '?' + query
93

94 95 96
    def local_resource_url(self, block, uri):
        return local_resource_url(block, uri)

97

98 99 100 101 102 103 104 105 106 107 108 109 110 111
class StudioUserService(object):
    """
    Provides a Studio implementation of the XBlock user service.
    """

    def __init__(self, request):
        super(StudioUserService, self).__init__()
        self._request = request

    @property
    def user_id(self):
        return self._request.user.id


112
def _preview_module_system(request, descriptor):
113 114 115 116 117 118 119 120
    """
    Returns a ModuleSystem for the specified descriptor that is specialized for
    rendering module previews.

    request: The active django request
    descriptor: An XModuleDescriptor
    """

121
    course_id = descriptor.location.course_key
122 123 124 125
    display_name_only = (descriptor.category == 'static_tab')

    wrappers = [
        # This wrapper wraps the module in the template specified above
126
        partial(wrap_xblock, 'PreviewRuntime', display_name_only=display_name_only, usage_id_serializer=unicode),
127 128 129 130 131 132

        # This wrapper replaces urls in the output that start with /static
        # with the correct course-specific url for the static content
        partial(replace_static_urls, None, course_id=course_id),
        _studio_wrap_xblock,
    ]
133

134 135
    descriptor.runtime._services['user'] = StudioUserService(request)  # pylint: disable=protected-access

136
    return PreviewModuleSystem(
137
        static_url=settings.STATIC_URL,
138
        # TODO (cpennington): Do we want to track how instructors are using the preview problems?
139
        track_function=lambda event_type, event: None,
140
        filestore=descriptor.runtime.resources_fs,
141
        get_module=partial(_load_preview_module, request),
142 143
        render_template=render_from_lms,
        debug=True,
Chris Dodge committed
144
        replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_id=course_id),
145
        user=request.user,
146
        can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)),
147
        mixins=settings.XBLOCK_MIXINS,
148
        course_id=course_id,
149 150 151
        anonymous_student_id='student',

        # Set up functions to modify the fragment produced by student_view
152
        wrappers=wrappers,
153
        error_descriptor_class=ErrorDescriptor,
154
        get_user_role=lambda: get_user_role(request.user, course_id),
155
        descriptor_runtime=descriptor.runtime,
156 157 158
        services={
            "i18n": ModuleI18nService(),
        },
159 160
    )

Steve Strassmann committed
161

162
def _load_preview_module(request, descriptor):
163
    """
164 165
    Return a preview XModule instantiated from the supplied descriptor. Will use mutable fields
    if XModule supports an author_view. Otherwise, will use immutable fields and student_view.
166 167 168 169

    request: The active django request
    descriptor: An XModuleDescriptor
    """
170
    student_data = KvsFieldData(SessionKeyValueStore(request))
171 172 173 174
    if _has_author_view(descriptor):
        field_data = CmsFieldData(descriptor._field_data, student_data)  # pylint: disable=protected-access
    else:
        field_data = LmsFieldData(descriptor._field_data, student_data)  # pylint: disable=protected-access
175
    descriptor.bind_for_student(
176
        _preview_module_system(request, descriptor),
177
        field_data
178 179
    )
    return descriptor
180

Steve Strassmann committed
181

182 183 184 185 186 187 188
def _is_xblock_reorderable(xblock, context):
    """
    Returns true if the specified xblock is in the set of reorderable xblocks.
    """
    return xblock.location in context['reorderable_items']


189 190 191 192 193 194
# pylint: disable=unused-argument
def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False):
    """
    Wraps the results of rendering an XBlock view in a div which adds a header and Studio action buttons.
    """
    # Only add the Studio wrapper when on the container page. The unit page will remain as is for now.
195
    if context.get('container_view', None) and view in PREVIEW_VIEWS:
196 197 198
        root_xblock = context.get('root_xblock')
        is_root = root_xblock and xblock.location == root_xblock.location
        is_reorderable = _is_xblock_reorderable(xblock, context)
199 200 201 202
        template_context = {
            'xblock_context': context,
            'xblock': xblock,
            'content': frag.content,
203 204
            'is_root': is_root,
            'is_reorderable': is_reorderable,
205
        }
206
        html = render_to_string('studio_xblock_wrapper.html', template_context)
207 208 209 210 211
        frag = wrap_fragment(frag, html)
    return frag


def get_preview_fragment(request, descriptor, context):
212
    """
213
    Returns the HTML returned by the XModule's student_view or author_view (if available),
David Baumgold committed
214
    specified by the descriptor and idx.
215
    """
216
    module = _load_preview_module(request, descriptor)
217

218
    preview_view = AUTHOR_VIEW if _has_author_view(module) else STUDENT_VIEW
219

220
    try:
221
        fragment = module.render(preview_view, context)
Julian Arni committed
222
    except Exception as exc:                          # pylint: disable=W0703
223
        log.warning("Unable to render %s for %r", preview_view, module, exc_info=True)
224 225
        fragment = Fragment(render_to_string('html_error.html', {'message': str(exc)}))
    return fragment
226 227 228 229 230 231 232 233 234


def _has_author_view(descriptor):
    """
    Returns True if the xmodule linked to the descriptor supports "author_view".

    If False, "student_view" and LmsFieldData should be used.
    """
    return getattr(descriptor, 'has_author_view', False)