Commit 2ba49579 by Xavier Antoviaque

xblock-external-ui: Add XBlock API call to render XBlock views

xblock-external-ui: Include CSRF token in the API answer
xblock-external-ui: Include full path when building local_url
xblock-external-ui: Fix TestHandleXBlockCallback & bok_choy, add tests
xblock-external-ui: Only return `instance` in `_invoke_xblock_handler()`
xblock-external-ui: Group resources by hash tag to avoid duplicate loads
xblock-external-ui: PEP8
xblock-external-ui: Fail early if the XBlock view is called anonymously

    We used to serve anonymous requests, but most XBlocks assume that the
    user is logged in, which can generate a lot of errors when the user is
    accessed or when an XBlock ajax callback is queried. Fail early to only
    get one error per page load, and prevent displaying the XBlock
    altogether when the LMS doesn't find an active user session.

xblock-external-ui: Add request params in view render context
xblock-external-ui: HTTP error status when file is too large for handler
xblock-external-ui: Fix unicode encodings in XBlock rendering
xblock-external-ui: Feature flag for API call ENABLE_XBLOCK_VIEW_ENDPOINT
parent 7aea582a
"""
Module rendering
"""
import hashlib
import json
import logging
import mimetypes
......@@ -5,6 +10,7 @@ import mimetypes
import static_replace
import xblock.reference.plugins
from collections import OrderedDict
from functools import partial
from requests.auth import HTTPBasicAuth
import dogstats_wrapper as dog_stats_api
......@@ -13,6 +19,8 @@ from opaque_keys import InvalidKeyError
from django.conf import settings
from django.contrib.auth.models import User
from django.core.cache import cache
from django.core.context_processors import csrf
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse
from django.http import Http404, HttpResponse
from django.views.decorators.csrf import csrf_exempt
......@@ -32,7 +40,7 @@ from student.models import anonymous_id_for_user, user_by_anonymous_id
from xblock.core import XBlock
from xblock.fields import Scope
from xblock.runtime import KvsFieldData, KeyValueStore
from xblock.exceptions import NoSuchHandlerError
from xblock.exceptions import NoSuchHandlerError, NoSuchViewError
from xblock.django.request import django_to_webob_request, webob_to_django_response
from xmodule.error_module import ErrorDescriptor, NonStaffErrorDescriptor
from xmodule.exceptions import NotFoundError, ProcessingError
......@@ -781,7 +789,7 @@ def handle_xblock_callback_noauth(request, course_id, usage_id, handler, suffix=
"""
request.user.known = False
return _invoke_xblock_handler(request, course_id, usage_id, handler, suffix, request.user)
return _invoke_xblock_handler(request, course_id, usage_id, handler, suffix)
def handle_xblock_callback(request, course_id, usage_id, handler, suffix=None):
......@@ -802,7 +810,7 @@ def handle_xblock_callback(request, course_id, usage_id, handler, suffix=None):
if not request.user.is_authenticated():
return HttpResponse('Unauthenticated', status=403)
return _invoke_xblock_handler(request, course_id, usage_id, handler, suffix, request.user)
return _invoke_xblock_handler(request, course_id, usage_id, handler, suffix)
def xblock_resource(request, block_type, uri): # pylint: disable=unused-argument
......@@ -822,31 +830,20 @@ def xblock_resource(request, block_type, uri): # pylint: disable=unused-argumen
return HttpResponse(content, mimetype=mimetype)
def _invoke_xblock_handler(request, course_id, usage_id, handler, suffix, user):
def _get_module_by_usage_id(request, course_id, usage_id):
"""
Invoke an XBlock handler, either authenticated or not.
Arguments:
request (HttpRequest): the current request
course_id (str): A string of the form org/course/run
usage_id (str): A string of the form i4x://org/course/category/name@revision
handler (str): The name of the handler to invoke
suffix (str): The suffix to pass to the handler when invoked
user (User): The currently logged in user
Gets a module instance based on its `usage_id` in a course, for a given request/user
Returns (instance, tracking_context)
"""
user = request.user
try:
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
usage_key = course_id.make_usage_key_from_deprecated_string(unquote_slashes(usage_id))
except InvalidKeyError:
raise Http404("Invalid location")
# Check submitted files
files = request.FILES or {}
error_msg = _check_files_limits(files)
if error_msg:
return HttpResponse(json.dumps({'success': error_msg}))
try:
descriptor = modulestore().get_item(usage_key)
descriptor_orig_usage_key, descriptor_orig_version = modulestore().get_block_original_usage(usage_key)
......@@ -859,13 +856,13 @@ def _invoke_xblock_handler(request, course_id, usage_id, handler, suffix, user):
)
raise Http404
tracking_context_name = 'module_callback_handler'
tracking_context = {
'module': {
'display_name': descriptor.display_name_with_default,
'usage_key': unicode(descriptor.location),
}
}
# For blocks that are inherited from a content library, we add some additional metadata:
if descriptor_orig_usage_key is not None:
tracking_context['module']['original_usage_key'] = unicode(descriptor_orig_usage_key)
......@@ -884,6 +881,30 @@ def _invoke_xblock_handler(request, course_id, usage_id, handler, suffix, user):
log.debug("No module %s for user %s -- access denied?", usage_key, user)
raise Http404
return (instance, tracking_context)
def _invoke_xblock_handler(request, course_id, usage_id, handler, suffix):
"""
Invoke an XBlock handler, either authenticated or not.
Arguments:
request (HttpRequest): the current request
course_id (str): A string of the form org/course/run
usage_id (str): A string of the form i4x://org/course/category/name@revision
handler (str): The name of the handler to invoke
suffix (str): The suffix to pass to the handler when invoked
"""
# Check submitted files
files = request.FILES or {}
error_msg = _check_files_limits(files)
if error_msg:
return JsonResponse(object={'success': error_msg}, status=413)
instance, tracking_context = _get_module_by_usage_id(request, course_id, usage_id)
tracking_context_name = 'module_callback_handler'
req = django_to_webob_request(request)
try:
with tracker.get_tracker().context(tracking_context_name, tracking_context):
......@@ -912,6 +933,52 @@ def _invoke_xblock_handler(request, course_id, usage_id, handler, suffix, user):
return webob_to_django_response(resp)
def hash_resource(resource):
"""
Hash a :class:`xblock.fragment.FragmentResource
"""
md5 = hashlib.md5()
for data in resource:
md5.update(repr(data))
return md5.hexdigest()
def xblock_view(request, course_id, usage_id, view_name):
"""
Returns the rendered view of a given XBlock, with related resources
Returns a json object containing two keys:
html: The rendered html of the view
resources: A list of tuples where the first element is the resource hash, and
the second is the resource description
"""
if not settings.FEATURES.get('ENABLE_XBLOCK_VIEW_ENDPOINT', False):
log.warn("Attempt to use deactivated XBlock view endpoint -"
" see FEATURES['ENABLE_XBLOCK_VIEW_ENDPOINT']")
raise Http404
if not request.user.is_authenticated():
raise PermissionDenied
instance, tracking_context = _get_module_by_usage_id(request, course_id, usage_id)
try:
fragment = instance.render(view_name, context=request.GET)
except NoSuchViewError:
log.exception("Attempt to render missing view on %s: %s", instance, view_name)
raise Http404
hashed_resources = OrderedDict()
for resource in fragment.resources:
hashed_resources[hash_resource(resource)] = resource
return JsonResponse({
'html': fragment.content,
'resources': hashed_resources.items(),
'csrf_token': str(csrf(request)['csrf_token']),
})
def get_score_bucket(grade, max_grade):
"""
Function to split arbitrary score ranges into 3 buckets.
......
......@@ -16,6 +16,7 @@ from django.contrib.auth.models import AnonymousUser
from mock import MagicMock, patch, Mock
from opaque_keys.edx.keys import UsageKey, CourseKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from courseware.module_render import hash_resource
from xblock.field_data import FieldData
from xblock.runtime import Runtime
from xblock.fields import ScopeIds
......@@ -246,6 +247,14 @@ class ModuleRenderTestCase(ModuleStoreTestCase, LoginEnrollmentTestCase):
render.get_module_for_descriptor(self.mock_user, request, descriptor, field_data_cache, self.toy_course.id)
render.get_module_for_descriptor(self.mock_user, request, descriptor, field_data_cache, self.toy_course.id)
def test_hash_resource(self):
"""
Ensure that the resource hasher works and does not fail on unicode,
decoded or otherwise.
"""
resources = ['ASCII text', u'❄ I am a special snowflake.', "❄ So am I, but I didn't tell you."]
self.assertEqual(hash_resource(resources), 'a76e27c8e80ca3efd7ce743093aa59e0')
class TestHandleXBlockCallback(ModuleStoreTestCase, LoginEnrollmentTestCase):
"""
......@@ -316,7 +325,7 @@ class TestHandleXBlockCallback(ModuleStoreTestCase, LoginEnrollmentTestCase):
json.dumps({
'success': 'Submission aborted! Maximum %d files may be submitted at once' %
settings.MAX_FILEUPLOADS_PER_INPUT
})
}, indent=2)
)
def test_too_large_file(self):
......@@ -336,7 +345,7 @@ class TestHandleXBlockCallback(ModuleStoreTestCase, LoginEnrollmentTestCase):
json.dumps({
'success': 'Submission aborted! Your file "%s" is too large (max size: %d MB)' %
(inputfile.name, settings.STUDENT_FILEUPLOAD_MAX_SIZE / (1000 ** 2))
})
}, indent=2)
)
def test_xmodule_dispatch(self):
......@@ -399,6 +408,29 @@ class TestHandleXBlockCallback(ModuleStoreTestCase, LoginEnrollmentTestCase):
'bad_dispatch',
)
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_XBLOCK_VIEW_ENDPOINT': True})
def test_xblock_view_handler(self):
args = [
'edX/toy/2012_Fall',
quote_slashes('i4x://edX/toy/videosequence/Toy_Videos'),
'student_view'
]
xblock_view_url = reverse(
'xblock_view',
args=args
)
request = self.request_factory.get(xblock_view_url)
request.user = self.mock_user
response = render.xblock_view(request, *args)
self.assertEquals(200, response.status_code)
expected = ['csrf_token', 'html', 'resources']
content = json.loads(response.content)
for section in expected:
self.assertIn(section, content)
self.assertIn('<div class="xblock xblock-student_view xmodule_display', content['html'])
@ddt.ddt
class TestTOC(ModuleStoreTestCase):
......
......@@ -5,6 +5,7 @@ Module implementing `xblock.runtime.Runtime` functionality for the LMS
import re
import xblock.reference.plugins
from django.conf import settings
from django.core.urlresolvers import reverse
from django.conf import settings
from lms.djangoapps.lms_xblock.models import XBlockAsidesConfig
......@@ -118,10 +119,11 @@ class LmsHandlerUrls(object):
"""
local_resource_url for Studio
"""
return reverse('xblock_resource_url', kwargs={
path = reverse('xblock_resource_url', kwargs={
'block_type': block.scope_ids.block_type,
'uri': uri,
})
return '//{}{}'.format(settings.SITE_NAME, path)
class LmsPartitionService(PartitionService):
......
......@@ -104,7 +104,7 @@
"SEGMENT_IO_LMS": true,
"SERVER_EMAIL": "devops@example.com",
"SESSION_COOKIE_DOMAIN": null,
"SITE_NAME": "localhost",
"SITE_NAME": "localhost:8003",
"STATIC_ROOT_BASE": "** OVERRIDDEN **",
"STATIC_URL_BASE": "/static/",
"SYSLOG_SERVER": "",
......
......@@ -133,6 +133,13 @@ FEATURES = {
# Toggles OAuth2 authentication provider
'ENABLE_OAUTH2_PROVIDER': False,
# Allows to enable an API endpoint to serve XBlock view, used for example by external applications.
# See jquey-xblock: https://github.com/edx-solutions/jquery-xblock
'ENABLE_XBLOCK_VIEW_ENDPOINT': False,
# Allows to configure the LMS to provide CORS headers to serve requests from other domains
'ENABLE_CORS_HEADERS': False,
# Can be turned off if course lists need to be hidden. Effects views and templates.
'COURSES_ARE_BROWSABLE': True,
......
......@@ -237,6 +237,11 @@ if settings.COURSEWARE_ENABLED:
url(r'^courses/{course_key}/xblock/{usage_key}/handler/(?P<handler>[^/]*)(?:/(?P<suffix>.*))?$'.format(course_key=settings.COURSE_ID_PATTERN, usage_key=settings.USAGE_ID_PATTERN),
'courseware.module_render.handle_xblock_callback',
name='xblock_handler'),
url(r'^courses/{course_key}/xblock/{usage_key}/view/(?P<view_name>[^/]*)$'.format(
course_key=settings.COURSE_ID_PATTERN,
usage_key=settings.USAGE_ID_PATTERN),
'courseware.module_render.xblock_view',
name='xblock_view'),
url(r'^courses/{course_key}/xblock/{usage_key}/handler_noauth/(?P<handler>[^/]*)(?:/(?P<suffix>.*))?$'.format(course_key=settings.COURSE_ID_PATTERN, usage_key=settings.USAGE_ID_PATTERN),
'courseware.module_render.handle_xblock_callback_noauth',
name='xblock_handler_noauth'),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment