Resolve conflicts merging master to rc/2013-11-21

This commit is contained in:
Ned Batchelder
2013-11-27 11:55:44 -05:00
170 changed files with 4162 additions and 5811 deletions

View File

@@ -5,7 +5,6 @@ from util.json_request import JsonResponse
from django.http import HttpResponseBadRequest
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_http_methods
from django.core.urlresolvers import reverse
from django_future.csrf import ensure_csrf_cookie
from mitxmako.shortcuts import render_to_response
from django.http import HttpResponseNotFound
@@ -22,6 +21,8 @@ from xmodule.modulestore.locator import BlockUsageLocator
__all__ = ['checklists_handler']
# pylint: disable=unused-argument
@require_http_methods(("GET", "POST", "PUT"))
@login_required
@ensure_csrf_cookie
@@ -85,8 +86,8 @@ def checklists_handler(request, tag=None, course_id=None, branch=None, version_g
return JsonResponse(expanded_checklist)
else:
return HttpResponseBadRequest(
( "Could not save checklist state because the checklist index "
"was out of range or unspecified."),
("Could not save checklist state because the checklist index "
"was out of range or unspecified."),
content_type="text/plain"
)
else:
@@ -113,14 +114,12 @@ def expand_checklist_action_url(course_module, checklist):
The method does a copy of the input checklist and does not modify the input argument.
"""
expanded_checklist = copy.deepcopy(checklist)
oldurlconf_map = {
"SettingsDetails": "settings_details",
"SettingsGrading": "settings_grading"
}
urlconf_map = {
"ManageUsers": "course_team",
"CourseOutline": "course"
"CourseOutline": "course",
"SettingsDetails": "settings/details",
"SettingsGrading": "settings/grading",
}
for item in expanded_checklist.get('items'):
@@ -130,12 +129,5 @@ def expand_checklist_action_url(course_module, checklist):
ctx_loc = course_module.location
location = loc_mapper().translate_location(ctx_loc.course_id, ctx_loc, False, True)
item['action_url'] = location.url_reverse(url_prefix, '')
elif action_url in oldurlconf_map:
urlconf_name = oldurlconf_map[action_url]
item['action_url'] = reverse(urlconf_name, kwargs={
'org': course_module.location.org,
'course': course_module.location.course,
'name': course_module.location.name,
})
return expanded_checklist

View File

@@ -2,21 +2,19 @@ import json
import logging
from collections import defaultdict
from django.http import (HttpResponse, HttpResponseBadRequest,
HttpResponseForbidden)
from django.http import HttpResponseBadRequest
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 xmodule.modulestore.exceptions import ItemNotFoundError
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 xmodule.modulestore.django import loc_mapper
from xmodule.modulestore.locator import BlockUsageLocator
from xblock.fields import Scope
from util.json_request import expect_json, JsonResponse
@@ -25,7 +23,6 @@ from contentstore.utils import get_lms_link_for_item, compute_unit_state, UnitSt
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
@@ -33,17 +30,13 @@ 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',
'subsection_handler',
'unit_handler'
]
log = logging.getLogger(__name__)
# NOTE: edit_unit assumes this list is disjoint from ADVANCED_COMPONENT_TYPES
# NOTE: unit_handler assumes this list is disjoint from ADVANCED_COMPONENT_TYPES
COMPONENT_TYPES = ['discussion', 'html', 'problem', 'video']
OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"]
@@ -58,93 +51,87 @@ ADVANCED_COMPONENT_CATEGORY = 'advanced'
ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
@require_http_methods(["GET"])
@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()
def subsection_handler(request, tag=None, course_id=None, branch=None, version_guid=None, block=None):
"""
The restful handler for subsection-specific requests.
if not has_access(request.user, course.location):
raise PermissionDenied()
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(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block)
try:
old_location, course, item, lms_link = _get_item_in_course(request, locator)
except ItemNotFoundError:
return HttpResponseBadRequest()
try:
item = modulestore().get_item(location, depth=1)
except ItemNotFoundError:
return HttpResponseBadRequest()
preview_link = get_lms_link_for_item(old_location, course_id=course.location.course_id, preview=True)
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()
# make sure that location references a 'sequential', otherwise return
# BadRequest
if item.location.category != 'sequential':
return HttpResponseBadRequest()
parent_locs = modulestore().get_parent_locations(old_location, None)
parent_locs = modulestore().get_parent_locations(location, None)
# we're for now assuming a single parent
if len(parent_locs) != 1:
logging.error(
# 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
unicode(locator)
)
# 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
)
# this should blow up if we don't find any parents, which would be erroneous
parent = modulestore().get_item(parent_locs[0])
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
# 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
course_locator = loc_mapper().translate_location(
course.location.course_id, course.location, False, True
)
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
locator = loc_mapper().translate_location(
course.location.course_id, item.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.location).graders),
# For grader, which is not yet converted
'parent_location': course.location,
'parent_item': parent,
'locator': locator,
'policy_metadata': policy_metadata,
'subsection_units': subsection_units,
'can_view_live': can_view_live
}
)
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):
def _load_mixed_class(category):
"""
Load an XBlock by category name, and apply all defined mixins
"""
@@ -153,139 +140,125 @@ def load_mixed_class(category):
return mixologist.mix(component_class)
@require_http_methods(["GET"])
@login_required
def edit_unit(request, location):
def unit_handler(request, tag=None, course_id=None, branch=None, version_guid=None, block=None):
"""
Display an editing page for the specified module.
The restful handler for unit-specific requests.
Expects a GET request with the parameter `id`.
id: A Location URL
GET
html: return html page for editing a unit
json: not currently supported
"""
try:
course = get_course_for_item(location)
except InvalidLocationError:
return HttpResponseBadRequest()
if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'):
locator = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block)
try:
old_location, course, item, lms_link = _get_item_in_course(request, locator)
except ItemNotFoundError:
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
)
# Note that the unit_state (draft, public, private) does not match up with the published value
# passed to translate_location. The two concepts are different at this point.
unit_locator = loc_mapper().translate_location(
course.location.course_id, Location(location), False, True
)
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
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')
))
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
)
components = [
[
component.location.url(),
loc_mapper().translate_location(
course.location.course_id, component.location, False, True
# 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
)
components = [
[
# TODO: old location needed for video transcripts.
component.location.url(),
loc_mapper().translate_location(
course.location.course_id, component.location, False, True
)
]
for component
in item.get_children()
]
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
# 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_locs = modulestore().get_parent_locations(old_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])
)
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
# 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
# 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_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE')
preview_lms_link = (
preview_lms_link = (
'//{preview_lms_base}/courses/{org}/{course}/'
'{course_name}/courseware/{section}/{subsection}/{index}'
).format(
@@ -299,102 +272,46 @@ def edit_unit(request, location):
index=index
)
return render_to_response('unit.html', {
'context_course': course,
'unit': item,
# Still needed for creating a draft.
'unit_location': location,
'unit_locator': unit_locator,
'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': compute_unit_state(item),
'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.json
)
return JsonResponse(rsp)
return render_to_response('unit.html', {
'context_course': course,
'unit': item,
'unit_locator': locator,
'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': compute_unit_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")
@login_required
@expect_json
def create_draft(request):
"Create a draft"
location = request.json['id']
def _get_item_in_course(request, locator):
"""
Helper method for getting the old location, containing course,
item, and lms_link for a given locator.
# check permissions for this user within this course
if not has_access(request.user, location):
Verifies that the caller has permission to access this item.
"""
if not has_access(request.user, locator):
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)
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 HttpResponse()
@login_required
@expect_json
def publish_draft(request):
"""
Publish a draft
"""
location = request.json['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.json['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()
return old_location, course, item, lms_link

View File

@@ -27,13 +27,11 @@ from xmodule.modulestore.exceptions import (
ItemNotFoundError, InvalidLocationError)
from xmodule.modulestore import Location
from contentstore.course_info_model import (
get_course_updates, update_course_updates, delete_course_update)
from contentstore.course_info_model import get_course_updates, update_course_updates, delete_course_update
from contentstore.utils import (
get_lms_link_for_item, add_extra_panel_tab, remove_extra_panel_tab,
get_modulestore)
from models.settings.course_details import (
CourseDetails, CourseSettingsEncoder)
from models.settings.course_details import CourseDetails, CourseSettingsEncoder
from models.settings.course_grading import CourseGradingModel
from models.settings.course_metadata import CourseMetadata
@@ -53,14 +51,13 @@ from student.models import CourseEnrollment
from xmodule.html_module import AboutDescriptor
from xmodule.modulestore.locator import BlockUsageLocator
from course_creators.views import get_course_creator_status, add_user_with_status_unrequested
from contentstore import utils
__all__ = ['course_info_handler', 'course_handler', 'course_info_update_handler',
'get_course_settings',
'course_config_graders_page',
'course_config_advanced_page',
'course_settings_updates',
'course_grader_updates',
'course_advanced_updates', 'textbook_index', 'textbook_by_id',
'settings_handler',
'grading_handler',
'advanced_settings_handler',
'textbook_index', 'textbook_by_id',
'create_textbook']
@@ -177,7 +174,6 @@ def course_index(request, course_id, branch, version_guid, block):
if not has_access(request.user, location):
raise PermissionDenied()
old_location = loc_mapper().translate_locator_to_location(location)
lms_link = get_lms_link_for_item(old_location)
@@ -190,10 +186,8 @@ def course_index(request, course_id, branch, version_guid, block):
'lms_link': lms_link,
'sections': sections,
'course_graders': json.dumps(
CourseGradingModel.fetch(course.location).graders
CourseGradingModel.fetch(location).graders
),
# This is used by course grader, which has not yet been updated.
'parent_location': course.location,
'parent_locator': location,
'new_section_category': 'chapter',
'new_subsection_category': 'sequential',
@@ -232,14 +226,20 @@ def create_new_course(request):
pass
if existing_course is not None:
return JsonResponse({
'ErrMsg': _('There is already a course defined with the same '
'ErrMsg': _(
'There is already a course defined with the same '
'organization, course number, and course run. Please '
'change either organization or course number to be '
'unique.'),
'OrgErrMsg': _('Please change either the organization or '
'course number so that it is unique.'),
'CourseErrMsg': _('Please change either the organization or '
'course number so that it is unique.'),
'unique.'
),
'OrgErrMsg': _(
'Please change either the organization or '
'course number so that it is unique.'
),
'CourseErrMsg': _(
'Please change either the organization or '
'course number so that it is unique.'
),
})
# dhm: this query breaks the abstraction, but I'll fix it when I do my suspended refactoring of this
@@ -254,12 +254,15 @@ def create_new_course(request):
courses = modulestore().collection.find(course_search_location, fields=('_id'))
if courses.count() > 0:
return JsonResponse({
'ErrMsg': _('There is already a course defined with the same '
'ErrMsg': _(
'There is already a course defined with the same '
'organization and course number. Please '
'change at least one field to be unique.'),
'OrgErrMsg': _('Please change either the organization or '
'OrgErrMsg': _(
'Please change either the organization or '
'course number so that it is unique.'),
'CourseErrMsg': _('Please change either the organization or '
'CourseErrMsg': _(
'Please change either the organization or '
'course number so that it is unique.'),
})
@@ -347,9 +350,8 @@ def course_info_handler(request, tag=None, course_id=None, branch=None, version_
@ensure_csrf_cookie
@require_http_methods(("GET", "POST", "PUT", "DELETE"))
@expect_json
def course_info_update_handler(
request, tag=None, course_id=None, branch=None, version_guid=None, block=None, provided_id=None
):
def course_info_update_handler(request, tag=None, course_id=None, branch=None, version_guid=None, block=None,
provided_id=None):
"""
restful CRUD operations on course_info updates.
provided_id should be none if it's new (create) and index otherwise.
@@ -394,232 +396,206 @@ def course_info_update_handler(
@login_required
@ensure_csrf_cookie
def get_course_settings(request, org, course, name):
"""
Send models and views as well as html for editing the course settings to
the client.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
new_loc = loc_mapper().translate_location(location.course_id, location, False, True)
upload_asset_url = new_loc.url_reverse('assets/', '')
return render_to_response('settings.html', {
'context_course': course_module,
'course_location': location,
'details_url': reverse(course_settings_updates,
kwargs={"org": org,
"course": course,
"name": name,
"section": "details"}),
'about_page_editable': not settings.MITX_FEATURES.get(
'ENABLE_MKTG_SITE', False
),
'upload_asset_url': upload_asset_url
})
@login_required
@ensure_csrf_cookie
def course_config_graders_page(request, org, course, name):
"""
Send models and views as well as html for editing the course settings to
the client.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
course_details = CourseGradingModel.fetch(location)
return render_to_response('settings_graders.html', {
'context_course': course_module,
'course_location': location,
'course_details': json.dumps(course_details, cls=CourseSettingsEncoder)
})
@login_required
@ensure_csrf_cookie
def course_config_advanced_page(request, org, course, name):
"""
Send models and views as well as html for editing the advanced course
settings to the client.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
return render_to_response('settings_advanced.html', {
'context_course': course_module,
'course_location': location,
'advanced_dict': json.dumps(CourseMetadata.fetch(location)),
})
@require_http_methods(("GET", "PUT", "POST"))
@expect_json
def settings_handler(request, tag=None, course_id=None, branch=None, version_guid=None, block=None):
"""
Course settings for dates and about pages
GET
html: get the page
json: get the CourseDetails model
PUT
json: update the Course and About xblocks through the CourseDetails model
"""
locator = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block)
if not has_access(request.user, locator):
raise PermissionDenied()
if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET':
course_old_location = loc_mapper().translate_locator_to_location(locator)
course_module = modulestore().get_item(course_old_location)
upload_asset_url = locator.url_reverse('assets/')
return render_to_response('settings.html', {
'context_course': course_module,
'course_locator': locator,
'lms_link_for_about_page': utils.get_lms_link_for_about_page(course_old_location),
'course_image_url': utils.course_image_url(course_module),
'details_url': locator.url_reverse('/settings/details/'),
'about_page_editable': not settings.MITX_FEATURES.get(
'ENABLE_MKTG_SITE', False
),
'upload_asset_url': upload_asset_url
})
elif 'application/json' in request.META.get('HTTP_ACCEPT', ''):
if request.method == 'GET':
return JsonResponse(
CourseDetails.fetch(locator),
# encoder serializes dates, old locations, and instances
encoder=CourseSettingsEncoder
)
else: # post or put, doesn't matter.
return JsonResponse(
CourseDetails.update_from_json(locator, request.json),
encoder=CourseSettingsEncoder
)
@login_required
@ensure_csrf_cookie
def course_settings_updates(request, org, course, name, section):
"""
Restful CRUD operations on course settings. This differs from
get_course_settings by communicating purely through json (not rendering any
html) and handles section level operations rather than whole page.
org, course: Attributes of the Location for the item to edit
section: one of details, faculty, grading, problems, discussions
"""
get_location_and_verify_access(request, org, course, name)
if section == 'details':
manager = CourseDetails
elif section == 'grading':
manager = CourseGradingModel
else:
return
if request.method == 'GET':
# Cannot just do a get w/o knowing the course name :-(
return JsonResponse(
manager.fetch(Location(['i4x', org, course, 'course', name])),
encoder=CourseSettingsEncoder
)
elif request.method in ('POST', 'PUT'): # post or put, doesn't matter.
return JsonResponse(
manager.update_from_json(request.json),
encoder=CourseSettingsEncoder
)
@expect_json
@require_http_methods(("GET", "POST", "PUT", "DELETE"))
@login_required
@ensure_csrf_cookie
def course_grader_updates(request, org, course, name, grader_index=None):
"""
Restful CRUD operations on course_info updates. This differs from
get_course_settings by communicating purely through json (not rendering any
html) and handles section level operations rather than whole page.
org, course: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
if request.method == 'GET':
# Cannot just do a get w/o knowing the course name :-(
return JsonResponse(CourseGradingModel.fetch_grader(
Location(location), grader_index
))
elif request.method == "DELETE":
# ??? Should this return anything? Perhaps success fail?
CourseGradingModel.delete_grader(Location(location), grader_index)
return JsonResponse()
else: # post or put, doesn't matter.
return JsonResponse(CourseGradingModel.update_grader_from_json(
Location(location),
request.json
))
@require_http_methods(("GET", "POST", "PUT", "DELETE"))
@login_required
@ensure_csrf_cookie
@expect_json
def course_advanced_updates(request, org, course, name):
def grading_handler(request, tag=None, course_id=None, branch=None, version_guid=None, block=None, grader_index=None):
"""
Restful CRUD operations on metadata. The payload is a json rep of the
metadata dicts. For delete, otoh, the payload is either a key or a list of
keys to delete.
org, course: Attributes of the Location for the item to edit
Course Grading policy configuration
GET
html: get the page
json no grader_index: get the CourseGrading model (graceperiod, cutoffs, and graders)
json w/ grader_index: get the specific grader
PUT
json no grader_index: update the Course through the CourseGrading model
json w/ grader_index: create or update the specific grader (create if index out of range)
"""
location = get_location_and_verify_access(request, org, course, name)
locator = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block)
if not has_access(request.user, locator):
raise PermissionDenied()
if request.method == 'GET':
return JsonResponse(CourseMetadata.fetch(location))
elif request.method == 'DELETE':
return JsonResponse(CourseMetadata.delete_key(
location,
json.loads(request.body)
))
else:
# Whether or not to filter the tabs key out of the settings metadata
filter_tabs = True
if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET':
course_old_location = loc_mapper().translate_locator_to_location(locator)
course_module = modulestore().get_item(course_old_location)
course_details = CourseGradingModel.fetch(locator)
# Check to see if the user instantiated any advanced components. This
# is a hack that does the following :
# 1) adds/removes the open ended panel tab to a course automatically
# if the user has indicated that they want to edit the
# combinedopendended or peergrading module
# 2) adds/removes the notes panel tab to a course automatically if
# the user has indicated that they want the notes module enabled in
# their course
# TODO refactor the above into distinct advanced policy settings
if ADVANCED_COMPONENT_POLICY_KEY in request.json:
# Get the course so that we can scrape current tabs
course_module = modulestore().get_item(location)
return render_to_response('settings_graders.html', {
'context_course': course_module,
'course_locator': locator,
'course_details': json.dumps(course_details, cls=CourseSettingsEncoder),
'grading_url': locator.url_reverse('/settings/grading/'),
})
elif 'application/json' in request.META.get('HTTP_ACCEPT', ''):
if request.method == 'GET':
if grader_index is None:
return JsonResponse(
CourseGradingModel.fetch(locator),
# encoder serializes dates, old locations, and instances
encoder=CourseSettingsEncoder
)
else:
return JsonResponse(CourseGradingModel.fetch_grader(locator, grader_index))
elif request.method in ('POST', 'PUT'): # post or put, doesn't matter.
# None implies update the whole model (cutoffs, graceperiod, and graders) not a specific grader
if grader_index is None:
return JsonResponse(
CourseGradingModel.update_from_json(locator, request.json),
encoder=CourseSettingsEncoder
)
else:
return JsonResponse(
CourseGradingModel.update_grader_from_json(locator, request.json)
)
elif request.method == "DELETE" and grader_index is not None:
CourseGradingModel.delete_grader(locator, grader_index)
return JsonResponse()
# Maps tab types to components
tab_component_map = {
'open_ended': OPEN_ENDED_COMPONENT_TYPES,
'notes': NOTE_COMPONENT_TYPES,
}
# Check to see if the user instantiated any notes or open ended
# components
for tab_type in tab_component_map.keys():
component_types = tab_component_map.get(tab_type)
found_ac_type = False
for ac_type in component_types:
if ac_type in request.json[ADVANCED_COMPONENT_POLICY_KEY]:
# Add tab to the course if needed
changed, new_tabs = add_extra_panel_tab(
tab_type,
course_module
)
# If a tab has been added to the course, then send the
# metadata along to CourseMetadata.update_from_json
if changed:
course_module.tabs = new_tabs
request.json.update({'tabs': new_tabs})
# Indicate that tabs should not be filtered out of
# the metadata
filter_tabs = False
# Set this flag to avoid the tab removal code below.
found_ac_type = True
break
# If we did not find a module type in the advanced settings,
# we may need to remove the tab from the course.
if not found_ac_type:
# Remove tab from the course if needed
changed, new_tabs = remove_extra_panel_tab(
tab_type, course_module
)
# pylint: disable=invalid-name
def _config_course_advanced_components(request, course_module):
"""
Check to see if the user instantiated any advanced components. This
is a hack that does the following :
1) adds/removes the open ended panel tab to a course automatically
if the user has indicated that they want to edit the
combinedopendended or peergrading module
2) adds/removes the notes panel tab to a course automatically if
the user has indicated that they want the notes module enabled in
their course
"""
# TODO refactor the above into distinct advanced policy settings
filter_tabs = True # Exceptional conditions will pull this to False
if ADVANCED_COMPONENT_POLICY_KEY in request.json: # Maps tab types to components
tab_component_map = {
'open_ended':OPEN_ENDED_COMPONENT_TYPES,
'notes':NOTE_COMPONENT_TYPES,
}
# Check to see if the user instantiated any notes or open ended
# components
for tab_type in tab_component_map.keys():
component_types = tab_component_map.get(tab_type)
found_ac_type = False
for ac_type in component_types:
if ac_type in request.json[ADVANCED_COMPONENT_POLICY_KEY]:
# Add tab to the course if needed
changed, new_tabs = add_extra_panel_tab(tab_type, course_module)
# If a tab has been added to the course, then send the
# metadata along to CourseMetadata.update_from_json
if changed:
course_module.tabs = new_tabs
request.json.update({'tabs': new_tabs})
# Indicate that tabs should *not* be filtered out of
# Indicate that tabs should not be filtered out of
# the metadata
filter_tabs = False
try:
return JsonResponse(CourseMetadata.update_from_json(
location,
request.json,
filter_tabs=filter_tabs
))
except (TypeError, ValueError) as err:
return HttpResponseBadRequest(
"Incorrect setting format. " + str(err),
content_type="text/plain"
)
filter_tabs = False # Set this flag to avoid the tab removal code below.
found_ac_type = True #break
# If we did not find a module type in the advanced settings,
# we may need to remove the tab from the course.
if not found_ac_type: # Remove tab from the course if needed
changed, new_tabs = remove_extra_panel_tab(tab_type, course_module)
if changed:
course_module.tabs = new_tabs
request.json.update({'tabs':new_tabs})
# Indicate that tabs should *not* be filtered out of
# the metadata
filter_tabs = False
return filter_tabs
@login_required
@ensure_csrf_cookie
@require_http_methods(("GET", "POST", "PUT"))
@expect_json
def advanced_settings_handler(request, course_id=None, branch=None, version_guid=None, block=None, tag=None):
"""
Course settings configuration
GET
html: get the page
json: get the model
PUT, POST
json: update the Course's settings. The payload is a json rep of the
metadata dicts. The dict can include a "unsetKeys" entry which is a list
of keys whose values to unset: i.e., revert to default
"""
locator = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block)
if not has_access(request.user, locator):
raise PermissionDenied()
course_old_location = loc_mapper().translate_locator_to_location(locator)
course_module = modulestore().get_item(course_old_location)
if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET':
return render_to_response('settings_advanced.html', {
'context_course': course_module,
'advanced_dict': json.dumps(CourseMetadata.fetch(course_module)),
'advanced_settings_url': locator.url_reverse('settings/advanced')
})
elif 'application/json' in request.META.get('HTTP_ACCEPT', ''):
if request.method == 'GET':
return JsonResponse(CourseMetadata.fetch(course_module))
else:
# Whether or not to filter the tabs key out of the settings metadata
filter_tabs = _config_course_advanced_components(request, course_module)
try:
return JsonResponse(CourseMetadata.update_from_json(
course_module,
request.json,
filter_tabs=filter_tabs
))
except (TypeError, ValueError) as err:
return HttpResponseBadRequest(
"Incorrect setting format. {}".format(err),
content_type="text/plain"
)
class TextbookValidationError(Exception):

View File

@@ -14,7 +14,6 @@ from django.conf import settings
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required
from django_future.csrf import ensure_csrf_cookie
from django.core.urlresolvers import reverse
from django.core.servers.basehttp import FileWrapper
from django.core.files.temp import NamedTemporaryFile
from django.core.exceptions import SuspiciousOperation, PermissionDenied
@@ -140,7 +139,7 @@ def import_handler(request, tag=None, course_id=None, branch=None, version_guid=
"size": size,
"deleteUrl": "",
"deleteType": "",
"url": location.url_reverse('import/', ''),
"url": location.url_reverse('import'),
"thumbnailUrl": ""
}]
})
@@ -252,8 +251,8 @@ def import_handler(request, tag=None, course_id=None, branch=None, version_guid=
course_module = modulestore().get_item(old_location)
return render_to_response('import.html', {
'context_course': course_module,
'successful_import_redirect_url': location.url_reverse("course/", ""),
'import_status_url': location.url_reverse("import_status/", "fillerName"),
'successful_import_redirect_url': location.url_reverse("course"),
'import_status_url': location.url_reverse("import_status", "fillerName"),
})
else:
return HttpResponseNotFound()
@@ -313,7 +312,7 @@ def export_handler(request, tag=None, course_id=None, branch=None, version_guid=
# an _accept URL parameter will be preferred over HTTP_ACCEPT in the header.
requested_format = request.REQUEST.get('_accept', request.META.get('HTTP_ACCEPT', 'text/html'))
export_url = location.url_reverse('export/', '') + '?_accept=application/x-tgz'
export_url = location.url_reverse('export') + '?_accept=application/x-tgz'
if 'application/x-tgz' in requested_format:
name = old_location.name
export_file = NamedTemporaryFile(prefix=name + '.', suffix=".tar.gz")
@@ -339,16 +338,16 @@ def export_handler(request, tag=None, course_id=None, branch=None, version_guid=
# if we have a nested exception, then we'll show the more generic error message
pass
unit_locator = loc_mapper().translate_location(old_location.course_id, parent.location, False, True)
return render_to_response('export.html', {
'context_course': course_module,
'in_err': True,
'raw_err_msg': str(e),
'failed_module': failed_item,
'unit': unit,
'edit_unit_url': reverse('edit_unit', kwargs={
'location': parent.location
}) if parent else '',
'course_home_url': location.url_reverse("course/", ""),
'edit_unit_url': unit_locator.url_reverse("unit") if parent else "",
'course_home_url': location.url_reverse("course"),
'export_url': export_url
})
@@ -359,7 +358,7 @@ def export_handler(request, tag=None, course_id=None, branch=None, version_guid=
'in_err': True,
'unit': None,
'raw_err_msg': str(e),
'course_home_url': location.url_reverse("course/", ""),
'course_home_url': location.url_reverse("course"),
'export_url': export_url
})

View File

@@ -3,7 +3,9 @@
import logging
from uuid import uuid4
from functools import partial
from static_replace import replace_static_urls
from xmodule_modifiers import wrap_xblock
from django.core.exceptions import PermissionDenied
from django.contrib.auth.decorators import login_required
@@ -27,6 +29,9 @@ from xmodule.modulestore.locator import BlockUsageLocator
from student.models import CourseEnrollment
from django.http import HttpResponseBadRequest
from xblock.fields import Scope
from preview import handler_prefix, get_preview_html
from mitxmako.shortcuts import render_to_response, render_to_string
from models.settings.course_grading import CourseGradingModel
__all__ = ['orphan_handler', 'xblock_handler']
@@ -51,17 +56,21 @@ def xblock_handler(request, tag=None, course_id=None, branch=None, version_guid=
all children and "all_versions" to delete from all (mongo) versions.
GET
json: returns representation of the xblock (locator id, data, and metadata).
if ?fields=graderType, it returns the graderType for the unit instead of the above.
html: returns HTML for rendering the xblock (which includes both the "preview" view and the "editor" view)
PUT or POST
json: if xblock location is specified, update the xblock instance. The json payload can contain
json: if xblock locator is specified, update the xblock instance. The json payload can contain
these fields, all optional:
:data: the new value for the data.
:children: the locator ids of children for this xblock.
:metadata: new values for the metadata fields. Any whose values are None will be deleted not set
to None! Absent ones will be left alone.
:nullout: which metadata fields to set to None
:graderType: change how this unit is graded
:publish: can be one of three values, 'make_public, 'make_private', or 'create_draft'
The JSON representation on the updated xblock (minus children) is returned.
if xblock location is not specified, create a new xblock instance. The json playload can contain
if xblock locator is not specified, create a new xblock instance. The json playload can contain
these fields:
:parent_locator: parent for new xblock, required
:category: type of xblock, required
@@ -70,14 +79,38 @@ def xblock_handler(request, tag=None, course_id=None, branch=None, version_guid=
The locator (and old-style id) for the created xblock (minus children) is returned.
"""
if course_id is not None:
location = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block)
if not has_access(request.user, location):
locator = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block)
if not has_access(request.user, locator):
raise PermissionDenied()
old_location = loc_mapper().translate_locator_to_location(location)
old_location = loc_mapper().translate_locator_to_location(locator)
if request.method == 'GET':
rsp = _get_module_info(location)
return JsonResponse(rsp)
if 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'):
fields = request.REQUEST.get('fields', '').split(',')
if 'graderType' in fields:
# right now can't combine output of this w/ output of _get_module_info, but worthy goal
return JsonResponse(CourseGradingModel.get_section_grader_type(locator))
# TODO: pass fields to _get_module_info and only return those
rsp = _get_module_info(locator)
return JsonResponse(rsp)
else:
component = modulestore().get_item(old_location)
# Wrap the generated fragment in the xmodule_editor div so that the javascript
# can bind to it correctly
component.runtime.wrappers.append(partial(wrap_xblock, handler_prefix))
try:
content = component.render('studio_view').content
# catch exceptions indiscriminately, since after this point they escape the
# dungeon and surface as uneditable, unsaveable, and undeletable
# component-goblins.
except Exception as exc: # pylint: disable=W0703
content = render_to_string('html_error.html', {'message': str(exc)})
return render_to_response('component.html', {
'preview': get_preview_html(request, component),
'editor': content
})
elif request.method == 'DELETE':
delete_children = str_to_bool(request.REQUEST.get('recurse', 'False'))
delete_all_versions = str_to_bool(request.REQUEST.get('all_versions', 'False'))
@@ -85,12 +118,15 @@ def xblock_handler(request, tag=None, course_id=None, branch=None, version_guid=
return _delete_item_at_location(old_location, delete_children, delete_all_versions)
else: # Since we have a course_id, we are updating an existing xblock.
return _save_item(
location,
request,
locator,
old_location,
data=request.json.get('data'),
children=request.json.get('children'),
metadata=request.json.get('metadata'),
nullout=request.json.get('nullout')
nullout=request.json.get('nullout'),
grader_type=request.json.get('graderType'),
publish=request.json.get('publish'),
)
elif request.method in ('PUT', 'POST'):
return _create_item(request)
@@ -101,11 +137,14 @@ def xblock_handler(request, tag=None, course_id=None, branch=None, version_guid=
)
def _save_item(usage_loc, item_location, data=None, children=None, metadata=None, nullout=None):
def _save_item(request, usage_loc, item_location, data=None, children=None, metadata=None, nullout=None,
grader_type=None, publish=None):
"""
Saves certain properties (data, children, metadata, nullout) for a given xblock item.
Saves xblock w/ its fields. Has special processing for grader_type, publish, and nullout and Nones in metadata.
nullout means to truly set the field to None whereas nones in metadata mean to unset them (so they revert
to default).
The item_location is still the old-style location.
The item_location is still the old-style location whereas usage_loc is a BlockUsageLocator
"""
store = get_modulestore(item_location)
@@ -123,6 +162,14 @@ def _save_item(usage_loc, item_location, data=None, children=None, metadata=None
log.error("Can't find item by location.")
return JsonResponse({"error": "Can't find item by location: " + str(item_location)}, 404)
if publish:
if publish == 'make_private':
_xmodule_recurse(existing_item, lambda i: modulestore().unpublish(i.location))
elif publish == 'create_draft':
# This clones the existing item location to a draft location (the draft is
# implicit, because modulestore is a Draft modulestore)
modulestore().convert_to_draft(item_location)
if data:
store.update_item(item_location, data)
else:
@@ -170,12 +217,25 @@ def _save_item(usage_loc, item_location, data=None, children=None, metadata=None
if existing_item.category == 'video':
manage_video_subtitles_save(existing_item, existing_item)
# Note that children aren't being returned until we have a use case.
return JsonResponse({
result = {
'id': unicode(usage_loc),
'data': data,
'metadata': own_metadata(existing_item)
})
}
if grader_type is not None:
result.update(CourseGradingModel.update_section_grader_type(existing_item, grader_type))
# Make public after updating the xblock, in case the caller asked
# for both an update and a publish.
if publish and publish == 'make_public':
_xmodule_recurse(
existing_item,
lambda i: modulestore().publish(i.location, request.user.id)
)
# Note that children aren't being returned until we have a use case.
return JsonResponse(result)
@login_required
@@ -192,10 +252,7 @@ def _create_item(request):
raise PermissionDenied()
parent = get_modulestore(category).get_item(parent_location)
# Necessary to set revision=None or else metadata inheritance does not work
# (the ID with @draft will be used as the key in the inherited metadata map,
# and that is not expected by the code that later references it).
dest_location = parent_location.replace(category=category, name=uuid4().hex, revision=None)
dest_location = parent_location.replace(category=category, name=uuid4().hex)
# get the metadata, display_name, and definition from the request
metadata = {}
@@ -224,7 +281,7 @@ def _create_item(request):
course_location = loc_mapper().translate_locator_to_location(parent_locator, get_course=True)
locator = loc_mapper().translate_location(course_location.course_id, dest_location, False, True)
return JsonResponse({'id': dest_location.url(), "locator": unicode(locator)})
return JsonResponse({"locator": unicode(locator)})
def _delete_item_at_location(item_location, delete_children=False, delete_all_versions=False):

View File

@@ -3,7 +3,7 @@ from functools import partial
from django.conf import settings
from django.core.urlresolvers import reverse
from django.http import Http404, HttpResponseBadRequest, HttpResponseForbidden
from django.http import Http404, HttpResponseBadRequest
from django.contrib.auth.decorators import login_required
from mitxmako.shortcuts import render_to_response, render_to_string
@@ -24,10 +24,9 @@ from util.sandboxing import can_execute_unsafe_code
import static_replace
from .session_kv_store import SessionKeyValueStore
from .helpers import render_from_lms
from .access import has_access
from ..utils import get_course_for_item
__all__ = ['preview_handler', 'preview_component']
__all__ = ['preview_handler']
log = logging.getLogger(__name__)
@@ -53,13 +52,13 @@ def preview_handler(request, usage_id, handler, suffix=''):
usage_id: The usage-id of the block to dispatch to, passed through `quote_slashes`
handler: The handler to execute
suffix: The remaineder of the url to be passed to the handler
suffix: The remainder of the url to be passed to the handler
"""
location = unquote_slashes(usage_id)
descriptor = modulestore().get_item(location)
instance = load_preview_module(request, descriptor)
instance = _load_preview_module(request, descriptor)
# Let the module handle the AJAX
req = django_to_webob_request(request)
try:
@@ -85,32 +84,6 @@ def preview_handler(request, usage_id, handler, suffix=''):
return webob_to_django_response(resp)
@login_required
def preview_component(request, location):
"Return the HTML preview of a component"
# TODO (vshnayder): change name from id to location in coffee+html as well.
if not has_access(request.user, location):
return HttpResponseForbidden()
component = modulestore().get_item(location)
# Wrap the generated fragment in the xmodule_editor div so that the javascript
# can bind to it correctly
component.runtime.wrappers.append(partial(wrap_xblock, handler_prefix))
try:
content = component.render('studio_view').content
# catch exceptions indiscriminately, since after this point they escape the
# dungeon and surface as uneditable, unsaveable, and undeletable
# component-goblins.
except Exception as exc: # pylint: disable=W0703
content = render_to_string('html_error.html', {'message': str(exc)})
return render_to_response('component.html', {
'preview': get_preview_html(request, component),
'editor': content
})
class PreviewModuleSystem(ModuleSystem): # pylint: disable=abstract-method
"""
An XModule ModuleSystem for use in Studio previews
@@ -119,7 +92,7 @@ class PreviewModuleSystem(ModuleSystem): # pylint: disable=abstract-method
return handler_prefix(block, handler_name, suffix) + '?' + query
def preview_module_system(request, descriptor):
def _preview_module_system(request, descriptor):
"""
Returns a ModuleSystem for the specified descriptor that is specialized for
rendering module previews.
@@ -135,7 +108,7 @@ def preview_module_system(request, descriptor):
# TODO (cpennington): Do we want to track how instructors are using the preview problems?
track_function=lambda event_type, event: None,
filestore=descriptor.runtime.resources_fs,
get_module=partial(load_preview_module, request),
get_module=partial(_load_preview_module, request),
render_template=render_from_lms,
debug=True,
replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_id=course_id),
@@ -162,7 +135,7 @@ def preview_module_system(request, descriptor):
)
def load_preview_module(request, descriptor):
def _load_preview_module(request, descriptor):
"""
Return a preview XModule instantiated from the supplied descriptor.
@@ -171,7 +144,7 @@ def load_preview_module(request, descriptor):
"""
student_data = DbModel(SessionKeyValueStore(request))
descriptor.bind_for_student(
preview_module_system(request, descriptor),
_preview_module_system(request, descriptor),
LmsFieldData(descriptor._field_data, student_data), # pylint: disable=protected-access
)
return descriptor
@@ -182,7 +155,7 @@ def get_preview_html(request, descriptor):
Returns the HTML returned by the XModule's student_view,
specified by the descriptor and idx.
"""
module = load_preview_module(request, descriptor)
module = _load_preview_module(request, descriptor)
try:
content = module.render("student_view").content
except Exception as exc: # pylint: disable=W0703

View File

@@ -10,7 +10,7 @@ from mitxmako.shortcuts import render_to_response
from external_auth.views import ssl_login_shortcut
__all__ = ['signup', 'old_login_redirect', 'login_page', 'howitworks']
__all__ = ['signup', 'login_page', 'howitworks']
@ensure_csrf_cookie
@@ -22,13 +22,6 @@ def signup(request):
return render_to_response('signup.html', {'csrf': csrf_token})
def old_login_redirect(request):
'''
Redirect to the active login url.
'''
return redirect('login', permanent=True)
@ssl_login_shortcut
@ensure_csrf_cookie
def login_page(request):

View File

@@ -2,12 +2,13 @@
Views related to course tabs
"""
from access import has_access
from util.json_request import expect_json
from util.json_request import expect_json, JsonResponse
from django.http import HttpResponse, HttpResponseBadRequest
from django.http import HttpResponseNotFound
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django_future.csrf import ensure_csrf_cookie
from django.views.decorators.http import require_http_methods
from mitxmako.shortcuts import render_to_response
from xmodule.modulestore import Location
from xmodule.modulestore.inheritance import own_metadata
@@ -19,7 +20,7 @@ from ..utils import get_modulestore
from django.utils.translation import ugettext as _
__all__ = ['edit_tabs', 'reorder_static_tabs']
__all__ = ['tabs_handler']
def initialize_course_tabs(course):
@@ -43,107 +44,113 @@ def initialize_course_tabs(course):
modulestore('direct').update_metadata(course.location.url(), own_metadata(course))
@login_required
@expect_json
def reorder_static_tabs(request):
"Order the static tabs in the requested order"
def get_location_for_tab(tab):
tab_locator = BlockUsageLocator(tab)
return loc_mapper().translate_locator_to_location(tab_locator)
tabs = request.json['tabs']
course_location = loc_mapper().translate_locator_to_location(BlockUsageLocator(tabs[0]), get_course=True)
if not has_access(request.user, course_location):
raise PermissionDenied()
course = get_modulestore(course_location).get_item(course_location)
# get list of existing static tabs in course
# make sure they are the same lengths (i.e. the number of passed in tabs equals the number
# that we know about) otherwise we can drop some!
existing_static_tabs = [t for t in course.tabs if t['type'] == 'static_tab']
if len(existing_static_tabs) != len(tabs):
return HttpResponseBadRequest()
# load all reference tabs, return BadRequest if we can't find any of them
tab_items = []
for tab in tabs:
item = modulestore('direct').get_item(get_location_for_tab(tab))
if item is None:
return HttpResponseBadRequest()
tab_items.append(item)
# now just go through the existing course_tabs and re-order the static tabs
reordered_tabs = []
static_tab_idx = 0
for tab in course.tabs:
if tab['type'] == 'static_tab':
reordered_tabs.append({'type': 'static_tab',
'name': tab_items[static_tab_idx].display_name,
'url_slug': tab_items[static_tab_idx].location.name})
static_tab_idx += 1
else:
reordered_tabs.append(tab)
# OK, re-assemble the static tabs in the new order
course.tabs = reordered_tabs
# Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore.
course.save()
modulestore('direct').update_metadata(course.location, own_metadata(course))
# TODO: above two lines are used for the primitive-save case. Maybe factor them out?
return HttpResponse()
@login_required
@ensure_csrf_cookie
def edit_tabs(request, org, course, coursename):
"Edit tabs"
location = ['i4x', org, course, 'course', coursename]
store = get_modulestore(location)
course_item = store.get_item(location)
@require_http_methods(("GET", "POST", "PUT"))
def tabs_handler(request, tag=None, course_id=None, branch=None, version_guid=None, block=None):
"""
The restful handler for static tabs.
# check that logged in user has permissions to this item
if not has_access(request.user, location):
GET
html: return page for editing static tabs
json: not supported
PUT or POST
json: update the tab order. It is expected that the request body contains a JSON-encoded dict with entry "tabs".
The value for "tabs" is an array of tab locators, indicating the desired order of the tabs.
Creating a tab, deleting a tab, or changing its contents is not supported through this method.
Instead use the general xblock URL (see item.xblock_handler).
"""
locator = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block)
if not has_access(request.user, locator):
raise PermissionDenied()
# see tabs have been uninitialized (e.g. supporing courses created before tab support in studio)
if course_item.tabs is None or len(course_item.tabs) == 0:
initialize_course_tabs(course_item)
old_location = loc_mapper().translate_locator_to_location(locator)
store = get_modulestore(old_location)
course_item = store.get_item(old_location)
# first get all static tabs from the tabs list
# we do this because this is also the order in which items are displayed in the LMS
static_tabs_refs = [t for t in course_item.tabs if t['type'] == 'static_tab']
if 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'):
if request.method == 'GET':
raise NotImplementedError('coming soon')
else:
if 'tabs' in request.json:
def get_location_for_tab(tab):
""" Returns the location (old-style) for a tab. """
return loc_mapper().translate_locator_to_location(BlockUsageLocator(tab))
static_tabs = []
for static_tab_ref in static_tabs_refs:
static_tab_loc = Location(location)._replace(category='static_tab', name=static_tab_ref['url_slug'])
static_tabs.append(modulestore('direct').get_item(static_tab_loc))
tabs = request.json['tabs']
components = [
[
static_tab.location.url(),
# get list of existing static tabs in course
# make sure they are the same lengths (i.e. the number of passed in tabs equals the number
# that we know about) otherwise we will inadvertently drop some!
existing_static_tabs = [t for t in course_item.tabs if t['type'] == 'static_tab']
if len(existing_static_tabs) != len(tabs):
return JsonResponse(
{"error": "number of tabs must be {}".format(len(existing_static_tabs))}, status=400
)
# load all reference tabs, return BadRequest if we can't find any of them
tab_items = []
for tab in tabs:
item = modulestore('direct').get_item(get_location_for_tab(tab))
if item is None:
return JsonResponse(
{"error": "no tab for found location {}".format(tab)}, status=400
)
tab_items.append(item)
# now just go through the existing course_tabs and re-order the static tabs
reordered_tabs = []
static_tab_idx = 0
for tab in course_item.tabs:
if tab['type'] == 'static_tab':
reordered_tabs.append(
{'type': 'static_tab',
'name': tab_items[static_tab_idx].display_name,
'url_slug': tab_items[static_tab_idx].location.name,
}
)
static_tab_idx += 1
else:
reordered_tabs.append(tab)
# OK, re-assemble the static tabs in the new order
course_item.tabs = reordered_tabs
modulestore('direct').update_metadata(course_item.location, own_metadata(course_item))
return JsonResponse()
else:
raise NotImplementedError('Creating or changing tab content is not supported.')
elif request.method == 'GET': # assume html
# see tabs have been uninitialized (e.g. supporting courses created before tab support in studio)
if course_item.tabs is None or len(course_item.tabs) == 0:
initialize_course_tabs(course_item)
# first get all static tabs from the tabs list
# we do this because this is also the order in which items are displayed in the LMS
static_tabs_refs = [t for t in course_item.tabs if t['type'] == 'static_tab']
static_tabs = []
for static_tab_ref in static_tabs_refs:
static_tab_loc = old_location.replace(category='static_tab', name=static_tab_ref['url_slug'])
static_tabs.append(modulestore('direct').get_item(static_tab_loc))
components = [
loc_mapper().translate_location(
course_item.location.course_id, static_tab.location, False, True
)
for static_tab
in static_tabs
]
for static_tab
in static_tabs
]
course_locator = loc_mapper().translate_location(
course_item.location.course_id, course_item.location, False, True
)
return render_to_response('edit-tabs.html', {
'context_course': course_item,
'components': components,
'locator': course_locator
})
return render_to_response('edit-tabs.html', {
'context_course': course_item,
'components': components,
'course_locator': locator
})
else:
return HttpResponseNotFound()
# "primitive" tab edit functions driven by the command line.
@@ -167,7 +174,7 @@ def primitive_delete(course, num):
# Note for future implementations: if you delete a static_tab, then Chris Dodge
# points out that there's other stuff to delete beyond this element.
# This code happens to not delete static_tab so it doesn't come up.
primitive_save(course)
modulestore('direct').update_metadata(course.location, own_metadata(course))
def primitive_insert(course, num, tab_type, name):
@@ -176,11 +183,5 @@ def primitive_insert(course, num, tab_type, name):
new_tab = {u'type': unicode(tab_type), u'name': unicode(name)}
tabs = course.tabs
tabs.insert(num, new_tab)
primitive_save(course)
def primitive_save(course):
"Saves the course back to modulestore."
# This code copied from reorder_static_tabs above
course.save()
modulestore('direct').update_metadata(course.location, own_metadata(course))