preview.py 11.4 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 13 14
from openedx.core.lib.xblock_utils import (
    replace_static_urls, wrap_xblock, wrap_fragment, wrap_xblock_aside, request_token, xblock_local_resource_url,
)
15
from xmodule.x_module import PREVIEW_VIEWS, STUDENT_VIEW, AUTHOR_VIEW
16
from xmodule.contentstore.django import contentstore
Steve Strassmann committed
17 18
from xmodule.error_module import ErrorDescriptor
from xmodule.exceptions import NotFoundError, ProcessingError
19
from xmodule.studio_editable import has_author_view
20
from xmodule.services import SettingsService
21
from xmodule.modulestore.django import modulestore, ModuleI18nService
22
from xmodule.mixin import wrap_with_license
23
from opaque_keys.edx.keys import UsageKey
24
from opaque_keys.edx.asides import AsideUsageKeyV1, AsideUsageKeyV2
Steve Strassmann committed
25
from xmodule.x_module import ModuleSystem
26
from xblock.runtime import KvsFieldData
27 28
from xblock.django.request import webob_to_django_response, django_to_webob_request
from xblock.exceptions import NoSuchHandlerError
29
from xblock.fragment import Fragment
30
from xblock_django.user_service import DjangoXBlockUserService
Steve Strassmann committed
31

32
from lms.djangoapps.lms_xblock.field_data import LmsFieldData
33
from cms.lib.xblock.field_data import CmsFieldData
Calen Pennington committed
34

35
from util.sandboxing import can_execute_unsafe_code, get_python_lib_zip
36

37
import static_replace
Steve Strassmann committed
38
from .session_kv_store import SessionKeyValueStore
39
from .helpers import render_from_lms
Steve Strassmann committed
40

41
from contentstore.views.access import get_user_role
42
from xblock_config.models import StudioConfig
43

44
__all__ = ['preview_handler']
Steve Strassmann committed
45

Steve Strassmann committed
46
log = logging.getLogger(__name__)
47

Steve Strassmann committed
48

49
@login_required
50
def preview_handler(request, usage_key_string, handler, suffix=''):
51
    """
52 53
    Dispatch an AJAX action to an xblock

54
    usage_key_string: The usage_key_string-id of the block to dispatch to, passed through `quote_slashes`
55
    handler: The handler to execute
56
    suffix: The remainder of the url to be passed to the handler
57
    """
58
    usage_key = UsageKey.from_string(usage_key_string)
59

60
    if isinstance(usage_key, (AsideUsageKeyV1, AsideUsageKeyV2)):
61 62 63 64 65 66 67 68 69 70 71
        descriptor = modulestore().get_item(usage_key.usage_key)
        for aside in descriptor.runtime.get_asides(descriptor):
            if aside.scope_ids.block_type == usage_key.aside_type:
                asides = [aside]
                instance = aside
                break
    else:
        descriptor = modulestore().get_item(usage_key)
        instance = _load_preview_module(request, descriptor)
        asides = []

72
    # Let the module handle the AJAX
73
    req = django_to_webob_request(request)
74
    try:
75 76 77 78 79
        resp = instance.handle(handler, req, suffix)

    except NoSuchHandlerError:
        log.exception("XBlock %s attempted to access missing handler %r", instance, handler)
        raise Http404
80 81 82 83 84 85 86 87 88 89

    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()

90
    except Exception:
91 92 93
        log.exception("error processing ajax call")
        raise

94
    modulestore().update_item(descriptor, request.user.id, asides=asides)
95
    return webob_to_django_response(resp)
96

Steve Strassmann committed
97

98 99 100 101
class PreviewModuleSystem(ModuleSystem):  # pylint: disable=abstract-method
    """
    An XModule ModuleSystem for use in Studio previews
    """
102 103 104 105
    # 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

106 107 108
    def __init__(self, **kwargs):
        super(PreviewModuleSystem, self).__init__(**kwargs)

109
    def handler_url(self, block, handler_name, suffix='', query='', thirdparty=False):
110
        return reverse('preview_handler', kwargs={
111
            'usage_key_string': unicode(block.scope_ids.usage_id),
112 113 114
            'handler': handler_name,
            'suffix': suffix,
        }) + '?' + query
115

116
    def local_resource_url(self, block, uri):
117
        return xblock_local_resource_url(block, uri)
118

119
    def applicable_aside_types(self, block):
120
        """
121
        Remove acid_aside and honor the config record
122
        """
123 124
        if not StudioConfig.asides_enabled(block.scope_ids.block_type):
            return []
125 126 127

        # TODO: aside_type != 'acid_aside' check should be removed once AcidBlock is only installed during tests
        # (see https://openedx.atlassian.net/browse/TE-811)
128 129 130 131 132
        return [
            aside_type
            for aside_type in super(PreviewModuleSystem, self).applicable_aside_types(block)
            if aside_type != 'acid_aside'
        ]
133

134 135 136 137 138 139
    def render_child_placeholder(self, block, view_name, context):
        """
        Renders a placeholder XBlock.
        """
        return self.wrap_xblock(block, view_name, Fragment(), context)

140 141 142 143 144 145
    def layout_asides(self, block, context, frag, view_name, aside_frag_fns):
        position_for_asides = '<!-- footer for xblock_aside -->'
        result = Fragment()
        result.add_frag_resources(frag)

        for aside, aside_fn in aside_frag_fns:
146 147 148 149 150 151 152
            aside_frag = aside_fn(block, context)
            if aside_frag.content != u'':
                aside_frag_wrapped = self.wrap_aside(block, aside, view_name, aside_frag, context)
                aside.save()
                result.add_frag_resources(aside_frag_wrapped)
                replacement = position_for_asides + aside_frag_wrapped.content
                frag.content = frag.content.replace(position_for_asides, replacement)
153 154 155 156

        result.add_content(frag.content)
        return result

157

158
def _preview_module_system(request, descriptor, field_data):
159 160 161 162 163 164 165 166
    """
    Returns a ModuleSystem for the specified descriptor that is specialized for
    rendering module previews.

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

167
    course_id = descriptor.location.course_key
168 169 170 171
    display_name_only = (descriptor.category == 'static_tab')

    wrappers = [
        # This wrapper wraps the module in the template specified above
172 173 174 175 176 177 178
        partial(
            wrap_xblock,
            'PreviewRuntime',
            display_name_only=display_name_only,
            usage_id_serializer=unicode,
            request_token=request_token(request)
        ),
179 180 181 182 183 184

        # 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,
    ]
185

186 187 188 189 190 191 192 193 194
    wrappers_asides = [
        partial(
            wrap_xblock_aside,
            'PreviewRuntime',
            usage_id_serializer=unicode,
            request_token=request_token(request)
        )
    ]

195 196 197 198
    if settings.FEATURES.get("LICENSING", False):
        # stick the license wrapper in front
        wrappers.insert(0, wrap_with_license)

199
    return PreviewModuleSystem(
200
        static_url=settings.STATIC_URL,
201
        # TODO (cpennington): Do we want to track how instructors are using the preview problems?
202
        track_function=lambda event_type, event: None,
203
        filestore=descriptor.runtime.resources_fs,
204
        get_module=partial(_load_preview_module, request),
205 206
        render_template=render_from_lms,
        debug=True,
Chris Dodge committed
207
        replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_id=course_id),
208
        user=request.user,
209
        can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)),
210
        get_python_lib_zip=(lambda: get_python_lib_zip(contentstore, course_id)),
211
        mixins=settings.XBLOCK_MIXINS,
212
        course_id=course_id,
213 214 215
        anonymous_student_id='student',

        # Set up functions to modify the fragment produced by student_view
216
        wrappers=wrappers,
217
        wrappers_asides=wrappers_asides,
218
        error_descriptor_class=ErrorDescriptor,
219
        get_user_role=lambda: get_user_role(request.user, course_id),
220 221
        # Get the raw DescriptorSystem, not the CombinedSystem
        descriptor_runtime=descriptor._runtime,  # pylint: disable=protected-access
222
        services={
223
            "field-data": field_data,
224
            "i18n": ModuleI18nService,
225
            "settings": SettingsService(),
226
            "user": DjangoXBlockUserService(request.user),
227
        },
228 229
    )

Steve Strassmann committed
230

231
def _load_preview_module(request, descriptor):
232
    """
233 234
    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.
235 236 237 238

    request: The active django request
    descriptor: An XModuleDescriptor
    """
239
    student_data = KvsFieldData(SessionKeyValueStore(request))
240
    if has_author_view(descriptor):
241
        wrapper = partial(CmsFieldData, student_data=student_data)
242
    else:
243 244 245 246 247 248
        wrapper = partial(LmsFieldData, student_data=student_data)

    # wrap the _field_data upfront to pass to _preview_module_system
    wrapped_field_data = wrapper(descriptor._field_data)  # pylint: disable=protected-access
    preview_runtime = _preview_module_system(request, descriptor, wrapped_field_data)

249
    descriptor.bind_for_student(
250 251 252
        preview_runtime,
        request.user.id,
        [wrapper]
253 254
    )
    return descriptor
255

Steve Strassmann committed
256

257 258
def _is_xblock_reorderable(xblock, context):
    """
259 260
    Returns true if the specified xblock is in the set of reorderable xblocks
    otherwise returns false.
261
    """
262 263 264 265
    try:
        return xblock.location in context['reorderable_items']
    except KeyError:
        return False
266 267


268 269 270 271 272
# 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.
    """
273 274
    # Only add the Studio wrapper when on the container page. The "Pages" page will remain as is for now.
    if not context.get('is_pages_view', None) and view in PREVIEW_VIEWS:
275 276 277
        root_xblock = context.get('root_xblock')
        is_root = root_xblock and xblock.location == root_xblock.location
        is_reorderable = _is_xblock_reorderable(xblock, context)
278 279 280
        template_context = {
            'xblock_context': context,
            'xblock': xblock,
281
            'show_preview': context.get('show_preview', True),
282
            'content': frag.content,
283 284
            'is_root': is_root,
            'is_reorderable': is_reorderable,
285
            'can_edit': context.get('can_edit', True),
286 287
            'can_edit_visibility': context.get('can_edit_visibility', True),
            'can_add': context.get('can_add', True),
288
        }
289
        html = render_to_string('studio_xblock_wrapper.html', template_context)
290 291 292 293 294
        frag = wrap_fragment(frag, html)
    return frag


def get_preview_fragment(request, descriptor, context):
295
    """
296
    Returns the HTML returned by the XModule's student_view or author_view (if available),
David Baumgold committed
297
    specified by the descriptor and idx.
298
    """
299
    module = _load_preview_module(request, descriptor)
300

301
    preview_view = AUTHOR_VIEW if has_author_view(module) else STUDENT_VIEW
302

303
    try:
304
        fragment = module.render(preview_view, context)
305
    except Exception as exc:                          # pylint: disable=broad-except
306
        log.warning("Unable to render %s for %r", preview_view, module, exc_info=True)
307 308
        fragment = Fragment(render_to_string('html_error.html', {'message': str(exc)}))
    return fragment