from __future__ import absolute_import import json import logging from collections import defaultdict from django.http import HttpResponseBadRequest, Http404 from django.contrib.auth.decorators import login_required from django.views.decorators.http import require_GET from django.core.exceptions import PermissionDenied from django.conf import settings from xmodule.modulestore.exceptions import ItemNotFoundError from edxmako.shortcuts import render_to_response from util.date_utils import get_default_time_display from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import loc_mapper from xmodule.modulestore.locator import BlockUsageLocator from xblock.core import XBlock from xblock.django.request import webob_to_django_response, django_to_webob_request from xblock.exceptions import NoSuchHandlerError from xblock.fields import Scope from xblock.plugin import PluginMissingError from xblock.runtime import Mixologist from lms.lib.xblock.runtime import unquote_slashes from contentstore.utils import get_lms_link_for_item, compute_publish_state, PublishState, get_modulestore from contentstore.views.helpers import get_parent_xblock from models.settings.course_grading import CourseGradingModel from .access import has_course_access __all__ = ['OPEN_ENDED_COMPONENT_TYPES', 'ADVANCED_COMPONENT_POLICY_KEY', 'subsection_handler', 'unit_handler', 'container_handler', 'component_handler' ] log = logging.getLogger(__name__) # NOTE: unit_handler assumes this list is disjoint from ADVANCED_COMPONENT_TYPES COMPONENT_TYPES = ['discussion', 'html', 'problem', 'video'] OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"] NOTE_COMPONENT_TYPES = ['notes'] if settings.FEATURES.get('ALLOW_ALL_ADVANCED_COMPONENTS'): ADVANCED_COMPONENT_TYPES = sorted(set(name for name, class_ in XBlock.load_classes()) - set(COMPONENT_TYPES)) else: ADVANCED_COMPONENT_TYPES = [ 'annotatable', 'textannotation', # module for annotating text (with annotation table) 'videoannotation', # module for annotating video (with annotation table) 'word_cloud', 'graphical_slider_tool', 'lti', 'concept', 'openassessment', # edx-ora2 ] + OPEN_ENDED_COMPONENT_TYPES + NOTE_COMPONENT_TYPES ADVANCED_COMPONENT_CATEGORY = 'advanced' ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules' @require_GET @login_required def subsection_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None): """ The restful handler for subsection-specific requests. GET html: return html page for editing a subsection json: not currently supported """ if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'): locator = BlockUsageLocator(package_id=package_id, branch=branch, version_guid=version_guid, block_id=block) try: old_location, course, item, lms_link = _get_item_in_course(request, locator) except ItemNotFoundError: return HttpResponseBadRequest() preview_link = get_lms_link_for_item(old_location, course_id=course.location.course_id, preview=True) # make sure that location references a 'sequential', otherwise return # BadRequest if item.location.category != 'sequential': return HttpResponseBadRequest() parent = get_parent_xblock(item) # remove all metadata from the generic dictionary that is presented in a # more normalized UI. We only want to display the XBlocks fields, not # the fields from any mixins that have been added fields = getattr(item, 'unmixed_class', item.__class__).fields policy_metadata = dict( (field.name, field.read_from(item)) for field in fields.values() if field.name not in ['display_name', 'start', 'due', 'format'] and field.scope == Scope.settings ) can_view_live = False subsection_units = item.get_children() for unit in subsection_units: state = compute_publish_state(unit) if state in (PublishState.public, PublishState.draft): can_view_live = True break course_locator = loc_mapper().translate_location( course.location.course_id, course.location, False, True ) return render_to_response( 'edit_subsection.html', { 'subsection': item, 'context_course': course, 'new_unit_category': 'vertical', 'lms_link': lms_link, 'preview_link': preview_link, 'course_graders': json.dumps(CourseGradingModel.fetch(course_locator).graders), 'parent_item': parent, 'locator': locator, 'policy_metadata': policy_metadata, 'subsection_units': subsection_units, 'can_view_live': can_view_live } ) else: return HttpResponseBadRequest("Only supports html requests") def _load_mixed_class(category): """ Load an XBlock by category name, and apply all defined mixins """ component_class = XBlock.load_class(category, select=settings.XBLOCK_SELECT_FUNCTION) mixologist = Mixologist(settings.XBLOCK_MIXINS) return mixologist.mix(component_class) @require_GET @login_required def unit_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None): """ The restful handler for unit-specific requests. GET html: return html page for editing a unit json: not currently supported """ if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'): locator = BlockUsageLocator(package_id=package_id, branch=branch, version_guid=version_guid, block_id=block) try: old_location, course, item, lms_link = _get_item_in_course(request, locator) except ItemNotFoundError: return HttpResponseBadRequest() component_templates = defaultdict(list) for category in COMPONENT_TYPES: component_class = _load_mixed_class(category) # add the default template # TODO: Once mixins are defined per-application, rather than per-runtime, # this should use a cms mixed-in class. (cpennington) if hasattr(component_class, 'display_name'): display_name = component_class.display_name.default or 'Blank' else: display_name = 'Blank' component_templates[category].append(( display_name, category, False, # No defaults have markdown (hardcoded current default) None # no boilerplate for overrides )) # add boilerplates if hasattr(component_class, 'templates'): for template in component_class.templates(): filter_templates = getattr(component_class, 'filter_templates', None) if not filter_templates or filter_templates(template, course): component_templates[category].append(( template['metadata'].get('display_name'), category, template['metadata'].get('markdown') is not None, template.get('template_id') )) # Check if there are any advanced modules specified in the course policy. # These modules should be specified as a list of strings, where the strings # are the names of the modules in ADVANCED_COMPONENT_TYPES that should be # enabled for the course. course_advanced_keys = course.advanced_modules # Set component types according to course policy file if isinstance(course_advanced_keys, list): for category in course_advanced_keys: if category in ADVANCED_COMPONENT_TYPES: # Do I need to allow for boilerplates or just defaults on the # class? i.e., can an advanced have more than one entry in the # menu? one for default and others for prefilled boilerplates? try: component_class = _load_mixed_class(category) component_templates['advanced'].append( ( component_class.display_name.default or category, category, False, None # don't override default data ) ) except PluginMissingError: # dhm: I got this once but it can happen any time the # course author configures an advanced component which does # not exist on the server. This code here merely # prevents any authors from trying to instantiate the # non-existent component type by not showing it in the menu pass else: log.error( "Improper format for course advanced keys! %s", course_advanced_keys ) xblocks = item.get_children() locators = [ loc_mapper().translate_location( course.location.course_id, xblock.location, False, True ) for xblock in xblocks ] # TODO (cpennington): If we share units between courses, # this will need to change to check permissions correctly so as # to pick the correct parent subsection containing_subsection = get_parent_xblock(item) containing_section = get_parent_xblock(containing_subsection) # cdodge hack. We're having trouble previewing drafts via jump_to redirect # so let's generate the link url here # need to figure out where this item is in the list of children as the # preview will need this index = 1 for child in containing_subsection.get_children(): if child.location == item.location: break index = index + 1 preview_lms_base = settings.FEATURES.get('PREVIEW_LMS_BASE') preview_lms_link = ( u'//{preview_lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}' ).format( preview_lms_base=preview_lms_base, lms_base=settings.LMS_BASE, org=course.location.org, course=course.location.course, course_name=course.location.name, section=containing_section.location.name, subsection=containing_subsection.location.name, index=index ) return render_to_response('unit.html', { 'context_course': course, 'unit': item, 'unit_locator': locator, 'locators': locators, 'component_templates': component_templates, 'draft_preview_link': preview_lms_link, 'published_preview_link': lms_link, 'subsection': containing_subsection, 'release_date': ( get_default_time_display(containing_subsection.start) if containing_subsection.start is not None else None ), 'section': containing_section, 'new_unit_category': 'vertical', 'unit_state': compute_publish_state(item), 'published_date': ( get_default_time_display(item.published_date) if item.published_date is not None else None ), }) else: return HttpResponseBadRequest("Only supports html requests") # pylint: disable=unused-argument @require_GET @login_required def container_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None): """ The restful handler for container xblock requests. GET html: returns the HTML page for editing a container json: not currently supported """ if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'): locator = BlockUsageLocator(package_id=package_id, branch=branch, version_guid=version_guid, block_id=block) try: __, course, xblock, __ = _get_item_in_course(request, locator) except ItemNotFoundError: return HttpResponseBadRequest() ancestor_xblocks = [] parent = get_parent_xblock(xblock) while parent and parent.category != 'sequential': ancestor_xblocks.append(parent) parent = get_parent_xblock(parent) ancestor_xblocks.reverse() unit = ancestor_xblocks[0] if ancestor_xblocks else None unit_publish_state = compute_publish_state(unit) if unit else None return render_to_response('container.html', { 'context_course': course, 'xblock': xblock, 'xblock_locator': locator, 'unit': unit, 'unit_publish_state': unit_publish_state, 'ancestor_xblocks': ancestor_xblocks, }) else: return HttpResponseBadRequest("Only supports html requests") @login_required def _get_item_in_course(request, locator): """ Helper method for getting the old location, containing course, item, and lms_link for a given locator. Verifies that the caller has permission to access this item. """ if not has_course_access(request.user, locator): raise PermissionDenied() old_location = loc_mapper().translate_locator_to_location(locator) course_location = loc_mapper().translate_locator_to_location(locator, True) course = modulestore().get_item(course_location) item = modulestore().get_item(old_location, depth=1) lms_link = get_lms_link_for_item(old_location, course_id=course.location.course_id) return old_location, course, item, lms_link @login_required def component_handler(request, usage_id, handler, suffix=''): """ Dispatch an AJAX action to an xblock Args: usage_id: The usage-id of the block to dispatch to, passed through `quote_slashes` handler (str): The handler to execute suffix (str): The remainder of the url to be passed to the handler Returns: :class:`django.http.HttpResponse`: The response from the handler, converted to a django response """ location = unquote_slashes(usage_id) descriptor = get_modulestore(location).get_item(location) # Let the module handle the AJAX req = django_to_webob_request(request) try: resp = descriptor.handle(handler, req, suffix) except NoSuchHandlerError: log.info("XBlock %s attempted to access missing handler %r", descriptor, handler, exc_info=True) raise Http404 # unintentional update to handle any side effects of handle call; so, request user didn't author # the change get_modulestore(location).update_item(descriptor, None) return webob_to_django_response(resp)