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
This commit is contained in:
@@ -1,3 +1,8 @@
|
||||
"""
|
||||
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'),
|
||||
|
||||
Reference in New Issue
Block a user