Files
edx-platform/cms/djangoapps/contentstore/views/component.py
Ned Batchelder 25a027475d Rename contentstore/views/requests to helpers
This has caused confusion, since this file gets imported when trying to
import the requests library.  And besides, how was this file about
requests in the first place?  Helpers is a much better name.
2013-10-15 15:53:48 -04:00

419 lines
14 KiB
Python

import json
import logging
from collections import defaultdict
from django.http import (HttpResponse, HttpResponseBadRequest,
HttpResponseForbidden)
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_http_methods
from django.core.exceptions import PermissionDenied
from django_future.csrf import ensure_csrf_cookie
from django.conf import settings
from xmodule.modulestore.exceptions import (ItemNotFoundError,
InvalidLocationError)
from mitxmako.shortcuts import render_to_response
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.util.date_utils import get_default_time_display
from xblock.fields import Scope
from util.json_request import expect_json, JsonResponse
from contentstore.module_info_model import get_module_info, set_module_info
from contentstore.utils import (get_modulestore, get_lms_link_for_item,
compute_unit_state, UnitState, get_course_for_item)
from models.settings.course_grading import CourseGradingModel
from .helpers import _xmodule_recurse
from .access import has_access
from xmodule.x_module import XModuleDescriptor
from xblock.plugin import PluginMissingError
from xblock.runtime import Mixologist
__all__ = ['OPEN_ENDED_COMPONENT_TYPES',
'ADVANCED_COMPONENT_POLICY_KEY',
'edit_subsection',
'edit_unit',
'assignment_type_update',
'create_draft',
'publish_draft',
'unpublish_unit',
'module_info']
log = logging.getLogger(__name__)
# NOTE: edit_unit 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']
ADVANCED_COMPONENT_TYPES = [
'annotatable',
'word_cloud',
'graphical_slider_tool',
'lti',
] + OPEN_ENDED_COMPONENT_TYPES + NOTE_COMPONENT_TYPES
ADVANCED_COMPONENT_CATEGORY = 'advanced'
ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
@login_required
def edit_subsection(request, location):
"Edit the subsection of a course"
# check that we have permissions to edit this item
try:
course = get_course_for_item(location)
except InvalidLocationError:
return HttpResponseBadRequest()
if not has_access(request.user, course.location):
raise PermissionDenied()
try:
item = modulestore().get_item(location, depth=1)
except ItemNotFoundError:
return HttpResponseBadRequest()
lms_link = get_lms_link_for_item(
location, course_id=course.location.course_id
)
preview_link = get_lms_link_for_item(
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_locs = modulestore().get_parent_locations(location, None)
# we're for now assuming a single parent
if len(parent_locs) != 1:
logging.error(
'Multiple (or none) parents have been found for %s',
location
)
# this should blow up if we don't find any parents, which would be erroneous
parent = modulestore().get_item(parent_locs[0])
# 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_unit_state(unit)
if state == UnitState.public or state == UnitState.draft:
can_view_live = True
break
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.location).graders),
'parent_location': course.location,
'parent_item': parent,
'policy_metadata': policy_metadata,
'subsection_units': subsection_units,
'can_view_live': can_view_live
}
)
def load_mixed_class(category):
"""
Load an XBlock by category name, and apply all defined mixins
"""
component_class = XModuleDescriptor.load_class(category)
mixologist = Mixologist(settings.XBLOCK_MIXINS)
return mixologist.mix(component_class)
@login_required
def edit_unit(request, location):
"""
Display an editing page for the specified module.
Expects a GET request with the parameter `id`.
id: A Location URL
"""
try:
course = get_course_for_item(location)
except InvalidLocationError:
return HttpResponseBadRequest()
if not has_access(request.user, course.location):
raise PermissionDenied()
try:
item = modulestore().get_item(location, depth=1)
except ItemNotFoundError:
return HttpResponseBadRequest()
lms_link = get_lms_link_for_item(
item.location,
course_id=course.location.course_id
)
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():
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! %",
course_advanced_keys
)
components = [
component.location.url()
for component
in item.get_children()
]
# 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_locs = modulestore().get_parent_locations(
location, None
)
containing_subsection = modulestore().get_item(containing_subsection_locs[0])
containing_section_locs = modulestore().get_parent_locations(
containing_subsection.location, None
)
containing_section = modulestore().get_item(containing_section_locs[0])
# 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.MITX_FEATURES.get('PREVIEW_LMS_BASE')
preview_lms_link = (
'//{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
)
unit_state = compute_unit_state(item)
return render_to_response('unit.html', {
'context_course': course,
'unit': item,
'unit_location': location,
'components': components,
'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': unit_state,
'published_date': (
get_default_time_display(item.published_date)
if item.published_date is not None else None
),
})
@expect_json
@login_required
@require_http_methods(("GET", "POST", "PUT"))
@ensure_csrf_cookie
def assignment_type_update(request, org, course, category, name):
"""
CRUD operations on assignment types for sections and subsections and
anything else gradable.
"""
location = Location(['i4x', org, course, category, name])
if not has_access(request.user, location):
return HttpResponseForbidden()
if request.method == 'GET':
rsp = CourseGradingModel.get_section_grader_type(location)
elif request.method in ('POST', 'PUT'): # post or put, doesn't matter.
rsp = CourseGradingModel.update_section_grader_type(
location, request.POST
)
return JsonResponse(rsp)
@login_required
@expect_json
def create_draft(request):
"Create a draft"
location = request.POST['id']
# check permissions for this user within this course
if not has_access(request.user, location):
raise PermissionDenied()
# This clones the existing item location to a draft location (the draft is
# implicit, because modulestore is a Draft modulestore)
modulestore().convert_to_draft(location)
return HttpResponse()
@login_required
@expect_json
def publish_draft(request):
"""
Publish a draft
"""
location = request.POST['id']
# check permissions for this user within this course
if not has_access(request.user, location):
raise PermissionDenied()
item = modulestore().get_item(location)
_xmodule_recurse(
item,
lambda i: modulestore().publish(i.location, request.user.id)
)
return HttpResponse()
@login_required
@expect_json
def unpublish_unit(request):
"Unpublish a unit"
location = request.POST['id']
# check permissions for this user within this course
if not has_access(request.user, location):
raise PermissionDenied()
item = modulestore().get_item(location)
_xmodule_recurse(item, lambda i: modulestore().unpublish(i.location))
return HttpResponse()
@expect_json
@require_http_methods(("GET", "POST", "PUT"))
@login_required
@ensure_csrf_cookie
def module_info(request, module_location):
"Get or set information for a module in the modulestore"
location = Location(module_location)
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
rewrite_static_links = request.GET.get('rewrite_url_links', 'True') in ['True', 'true']
logging.debug('rewrite_static_links = {0} {1}'.format(
request.GET.get('rewrite_url_links', False),
rewrite_static_links)
)
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
if request.method == 'GET':
rsp = get_module_info(
get_modulestore(location),
location,
rewrite_static_links=rewrite_static_links
)
elif request.method in ("POST", "PUT"):
rsp = set_module_info(
get_modulestore(location),
location, request.POST
)
return JsonResponse(rsp)