""" Functions that can are used to modify XBlock fragments for use in the LMS and Studio """ import datetime import json import logging import static_replace import uuid import markupsafe from django.conf import settings from django.utils.timezone import UTC from edxmako.shortcuts import render_to_string from xblock.exceptions import InvalidScopeError from xblock.fragment import Fragment from xmodule.seq_module import SequenceModule from xmodule.vertical_module import VerticalModule from xmodule.x_module import shim_xmodule_js, XModuleDescriptor, XModule, PREVIEW_VIEWS, STUDIO_VIEW from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.django import modulestore log = logging.getLogger(__name__) def wrap_fragment(fragment, new_content): """ Returns a new Fragment that has `new_content` and all as its content, and all of the resources from fragment """ wrapper_frag = Fragment(content=new_content) wrapper_frag.add_frag_resources(fragment) return wrapper_frag def request_token(request): """ Return a unique token for the supplied request. This token will be the same for all calls to `request_token` made on the same request object. """ if not hasattr(request, '_xblock_token'): request._xblock_token = uuid.uuid1().get_hex() return request._xblock_token def wrap_xblock(runtime_class, block, view, frag, context, usage_id_serializer, request_token, display_name_only=False, extra_data=None): # pylint: disable=unused-argument """ Wraps the results of rendering an XBlock view in a standard <section> with identifying data so that the appropriate javascript module can be loaded onto it. :param runtime_class: The name of the javascript runtime class to use to load this block :param block: An XBlock (that may be an XModule or XModuleDescriptor) :param view: The name of the view that rendered the fragment being wrapped :param frag: The :class:`Fragment` to be wrapped :param context: The context passed to the view being rendered :param usage_id_serializer: A function to serialize the block's usage_id for use by the front-end Javascript Runtime. :param request_token: An identifier that is unique per-request, so that only xblocks rendered as part of this request will have their javascript initialized. :param display_name_only: If true, don't render the fragment content at all. Instead, just render the `display_name` of `block` :param extra_data: A dictionary with extra data values to be set on the wrapper """ if extra_data is None: extra_data = {} # If any mixins have been applied, then use the unmixed class class_name = getattr(block, 'unmixed_class', block.__class__).__name__ data = {} data.update(extra_data) css_classes = ['xblock', 'xblock-{}'.format(markupsafe.escape(view))] if isinstance(block, (XModule, XModuleDescriptor)): if view in PREVIEW_VIEWS: # The block is acting as an XModule css_classes.append('xmodule_display') elif view == STUDIO_VIEW: # The block is acting as an XModuleDescriptor css_classes.append('xmodule_edit') css_classes.append('xmodule_' + markupsafe.escape(class_name)) data['type'] = block.js_module_name shim_xmodule_js(frag) if frag.js_init_fn: data['init'] = frag.js_init_fn data['runtime-class'] = runtime_class data['runtime-version'] = frag.js_init_version data['block-type'] = block.scope_ids.block_type data['usage-id'] = usage_id_serializer(block.scope_ids.usage_id) data['request-token'] = request_token if block.name: data['name'] = block.name template_context = { 'content': block.display_name if display_name_only else frag.content, 'classes': css_classes, 'display_name': block.display_name_with_default, 'data_attributes': u' '.join(u'data-{}="{}"'.format(markupsafe.escape(key), markupsafe.escape(value)) for key, value in data.iteritems()), } if hasattr(frag, 'json_init_args') and frag.json_init_args is not None: template_context['js_init_parameters'] = json.dumps(frag.json_init_args) template_context['js_pass_parameters'] = True else: template_context['js_init_parameters'] = "" template_context['js_pass_parameters'] = False return wrap_fragment(frag, render_to_string('xblock_wrapper.html', template_context)) def replace_jump_to_id_urls(course_id, jump_to_id_base_url, block, view, frag, context): # pylint: disable=unused-argument """ This will replace a link between courseware in the format /jump_to/<id> with a URL for a page that will correctly redirect This is similar to replace_course_urls, but much more flexible and durable for Studio authored courses. See more comments in static_replace.replace_jump_to_urls course_id: The course_id in which this rewrite happens jump_to_id_base_url: A app-tier (e.g. LMS) absolute path to the base of the handler that will perform the redirect. e.g. /courses/<org>/<course>/<run>/jump_to_id. NOTE the <id> will be appended to the end of this URL at re-write time output: a new :class:`~xblock.fragment.Fragment` that modifies `frag` with content that has been update with /jump_to links replaced """ return wrap_fragment(frag, static_replace.replace_jump_to_id_urls(frag.content, course_id, jump_to_id_base_url)) def replace_course_urls(course_id, block, view, frag, context): # pylint: disable=unused-argument """ Updates the supplied module with a new get_html function that wraps the old get_html function and substitutes urls of the form /course/... with urls that are /courses/<course_id>/... """ return wrap_fragment(frag, static_replace.replace_course_urls(frag.content, course_id)) def replace_static_urls(data_dir, block, view, frag, context, course_id=None, static_asset_path=''): # pylint: disable=unused-argument """ Updates the supplied module with a new get_html function that wraps the old get_html function and substitutes urls of the form /static/... with urls that are /static/<prefix>/... """ return wrap_fragment(frag, static_replace.replace_static_urls( frag.content, data_dir, course_id, static_asset_path=static_asset_path )) def grade_histogram(module_id): ''' Print out a histogram of grades on a given problem in staff member debug info. Warning: If a student has just looked at an xmodule and not attempted it, their grade is None. Since there will always be at least one such student this function almost always returns []. ''' from django.db import connection cursor = connection.cursor() q = """SELECT courseware_studentmodule.grade, COUNT(courseware_studentmodule.student_id) FROM courseware_studentmodule WHERE courseware_studentmodule.module_id=%s GROUP BY courseware_studentmodule.grade""" # Passing module_id this way prevents sql-injection. cursor.execute(q, [module_id.to_deprecated_string()]) grades = list(cursor.fetchall()) grades.sort(key=lambda x: x[0]) # Add ORDER BY to sql query? if len(grades) >= 1 and grades[0][0] is None: return [] return grades def add_staff_markup(user, has_instructor_access, block, view, frag, context): # pylint: disable=unused-argument """ Updates the supplied module with a new get_html function that wraps the output of the old get_html function with additional information for admin users only, including a histogram of student answers, the definition of the xmodule, and a link to view the module in Studio if it is a Studio edited, mongo stored course. Does nothing if module is a SequenceModule. """ # TODO: make this more general, eg use an XModule attribute instead if isinstance(block, VerticalModule) and (not context or not context.get('child_of_vertical', False)): # check that the course is a mongo backed Studio course before doing work is_mongo_course = modulestore().get_modulestore_type(block.location.course_key) != ModuleStoreEnum.Type.xml is_studio_course = block.course_edit_method == "Studio" if is_studio_course and is_mongo_course: # build edit link to unit in CMS. Can't use reverse here as lms doesn't load cms's urls.py edit_link = "//" + settings.CMS_BASE + '/container/' + unicode(block.location) # return edit link in rendered HTML for display return wrap_fragment(frag, render_to_string("edit_unit_link.html", {'frag_content': frag.content, 'edit_link': edit_link})) else: return frag if isinstance(block, SequenceModule): return frag block_id = block.location if block.has_score and settings.FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF'): histogram = grade_histogram(block_id) render_histogram = len(histogram) > 0 else: histogram = None render_histogram = False if settings.FEATURES.get('ENABLE_LMS_MIGRATION') and hasattr(block.runtime, 'filestore'): [filepath, filename] = getattr(block, 'xml_attributes', {}).get('filename', ['', None]) osfs = block.runtime.filestore if filename is not None and osfs.exists(filename): # if original, unmangled filename exists then use it (github # doesn't like symlinks) filepath = filename data_dir = block.static_asset_path or osfs.root_path.rsplit('/')[-1] giturl = block.giturl or 'https://github.com/MITx' edit_link = "%s/%s/tree/master/%s" % (giturl, data_dir, filepath) else: edit_link = False # Need to define all the variables that are about to be used giturl = "" data_dir = "" source_file = block.source_file # source used to generate the problem XML, eg latex or word # useful to indicate to staff if problem has been released or not # TODO (ichuang): use _has_access_descriptor.can_load in lms.courseware.access, instead of now>mstart comparison here now = datetime.datetime.now(UTC()) is_released = "unknown" mstart = block.start if mstart is not None: is_released = "<font color='red'>Yes!</font>" if (now > mstart) else "<font color='green'>Not yet</font>" field_contents = [] for name, field in block.fields.items(): try: field_contents.append((name, field.read_from(block))) except InvalidScopeError: log.warning("Unable to read field in Staff Debug information", exc_info=True) field_contents.append((name, "WARNING: Unable to read field")) staff_context = {'fields': field_contents, 'xml_attributes': getattr(block, 'xml_attributes', {}), 'location': block.location, 'xqa_key': block.xqa_key, 'source_file': source_file, 'source_url': '%s/%s/tree/master/%s' % (giturl, data_dir, source_file), 'category': str(block.__class__.__name__), # Template uses element_id in js function names, so can't allow dashes 'element_id': block.location.html_id().replace('-', '_'), 'edit_link': edit_link, 'user': user, 'xqa_server': settings.FEATURES.get('USE_XQA_SERVER', 'http://xqa:server@content-qa.mitx.mit.edu/xqa'), 'histogram': json.dumps(histogram), 'render_histogram': render_histogram, 'block_content': frag.content, 'is_released': is_released, 'has_instructor_access': has_instructor_access, } return wrap_fragment(frag, render_to_string("staff_problem_info.html", staff_context))