template is empty (very awkward b/c it's a template descriptor not an instance one) Changed display conditionals to get the right things to show in the right tabs for problems
1470 lines
55 KiB
Python
1470 lines
55 KiB
Python
from util.json_request import expect_json
|
|
import json
|
|
import logging
|
|
import os
|
|
import sys
|
|
import time
|
|
import tarfile
|
|
import shutil
|
|
from datetime import datetime
|
|
from collections import defaultdict
|
|
from uuid import uuid4
|
|
from path import path
|
|
from xmodule.modulestore.xml_exporter import export_to_xml
|
|
from tempfile import mkdtemp
|
|
from django.core.servers.basehttp import FileWrapper
|
|
from django.core.files.temp import NamedTemporaryFile
|
|
|
|
# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
|
|
from PIL import Image
|
|
|
|
from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.core.exceptions import PermissionDenied
|
|
from django.core.context_processors import csrf
|
|
from django_future.csrf import ensure_csrf_cookie
|
|
from django.core.urlresolvers import reverse
|
|
from django.conf import settings
|
|
|
|
from xmodule.modulestore import Location
|
|
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
|
|
from xmodule.x_module import ModuleSystem
|
|
from xmodule.error_module import ErrorDescriptor
|
|
from xmodule.errortracker import exc_info_to_str
|
|
import static_replace
|
|
from external_auth.views import ssl_login_shortcut
|
|
|
|
from mitxmako.shortcuts import render_to_response, render_to_string
|
|
from xmodule.modulestore.django import modulestore
|
|
from xmodule_modifiers import replace_static_urls, wrap_xmodule
|
|
from xmodule.exceptions import NotFoundError
|
|
from functools import partial
|
|
|
|
from xmodule.contentstore.django import contentstore
|
|
from xmodule.contentstore.content import StaticContent
|
|
|
|
from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role
|
|
from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group
|
|
from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME, create_all_course_groups
|
|
from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state, get_date_display, UnitState, get_course_for_item
|
|
|
|
from xmodule.modulestore.xml_importer import import_from_xml
|
|
from contentstore.course_info_model import get_course_updates,\
|
|
update_course_updates, delete_course_update
|
|
from cache_toolbox.core import del_cached_content
|
|
from xmodule.timeparse import stringify_time
|
|
from contentstore.module_info_model import get_module_info, set_module_info
|
|
from cms.djangoapps.models.settings.course_details import CourseDetails,\
|
|
CourseSettingsEncoder
|
|
from cms.djangoapps.models.settings.course_grading import CourseGradingModel
|
|
from cms.djangoapps.contentstore.utils import get_modulestore
|
|
from lxml import etree
|
|
|
|
# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']
|
|
|
|
# cdodge: these are categories which should not be parented, they are detached from the hierarchy
|
|
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
|
|
|
|
|
|
# ==== Public views ==================================================
|
|
|
|
@ensure_csrf_cookie
|
|
def signup(request):
|
|
"""
|
|
Display the signup form.
|
|
"""
|
|
csrf_token = csrf(request)['csrf_token']
|
|
return render_to_response('signup.html', {'csrf': csrf_token})
|
|
|
|
|
|
@ssl_login_shortcut
|
|
@ensure_csrf_cookie
|
|
def login_page(request):
|
|
"""
|
|
Display the login form.
|
|
"""
|
|
csrf_token = csrf(request)['csrf_token']
|
|
return render_to_response('login.html', {
|
|
'csrf': csrf_token,
|
|
'forgot_password_link': "//{base}/#forgot-password-modal".format(base=settings.LMS_BASE),
|
|
})
|
|
|
|
|
|
# ==== Views for any logged-in user ==================================
|
|
|
|
@login_required
|
|
@ensure_csrf_cookie
|
|
def index(request):
|
|
"""
|
|
List all courses available to the logged in user
|
|
"""
|
|
courses = modulestore().get_items(['i4x', None, None, 'course', None])
|
|
|
|
# filter out courses that we don't have access too
|
|
def course_filter(course):
|
|
return (has_access(request.user, course.location)
|
|
and course.location.course != 'templates'
|
|
and course.location.org != ''
|
|
and course.location.course != ''
|
|
and course.location.name != '')
|
|
courses = filter(course_filter, courses)
|
|
|
|
return render_to_response('index.html', {
|
|
'new_course_template': Location('i4x', 'edx', 'templates', 'course', 'Empty'),
|
|
'courses': [(course.metadata.get('display_name'),
|
|
reverse('course_index', args=[
|
|
course.location.org,
|
|
course.location.course,
|
|
course.location.name]))
|
|
for course in courses],
|
|
'user': request.user,
|
|
'disable_course_creation': settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff
|
|
})
|
|
|
|
|
|
# ==== Views with per-item permissions================================
|
|
|
|
def has_access(user, location, role=STAFF_ROLE_NAME):
|
|
'''
|
|
Return True if user allowed to access this piece of data
|
|
Note that the CMS permissions model is with respect to courses
|
|
There is a super-admin permissions if user.is_staff is set
|
|
Also, since we're unifying the user database between LMS and CAS,
|
|
I'm presuming that the course instructor (formally known as admin)
|
|
will not be in both INSTRUCTOR and STAFF groups, so we have to cascade our queries here as INSTRUCTOR
|
|
has all the rights that STAFF do
|
|
'''
|
|
course_location = get_course_location_for_item(location)
|
|
_has_access = is_user_in_course_group_role(user, course_location, role)
|
|
# if we're not in STAFF, perhaps we're in INSTRUCTOR groups
|
|
if not _has_access and role == STAFF_ROLE_NAME:
|
|
_has_access = is_user_in_course_group_role(user, course_location, INSTRUCTOR_ROLE_NAME)
|
|
return _has_access
|
|
|
|
|
|
@login_required
|
|
@ensure_csrf_cookie
|
|
def course_index(request, org, course, name):
|
|
"""
|
|
Display an editable course overview.
|
|
|
|
org, course, name: Attributes of the Location for the item to edit
|
|
"""
|
|
location = ['i4x', org, course, 'course', name]
|
|
|
|
# check that logged in user has permissions to this item
|
|
if not has_access(request.user, location):
|
|
raise PermissionDenied()
|
|
|
|
upload_asset_callback_url = reverse('upload_asset', kwargs={
|
|
'org': org,
|
|
'course': course,
|
|
'coursename': name
|
|
})
|
|
|
|
course = modulestore().get_item(location)
|
|
sections = course.get_children()
|
|
|
|
return render_to_response('overview.html', {
|
|
'active_tab': 'courseware',
|
|
'context_course': course,
|
|
'sections': sections,
|
|
'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
|
|
'parent_location': course.location,
|
|
'new_section_template': Location('i4x', 'edx', 'templates', 'chapter', 'Empty'),
|
|
'new_subsection_template': Location('i4x', 'edx', 'templates', 'sequential', 'Empty'), # for now they are the same, but the could be different at some point...
|
|
'upload_asset_callback_url': upload_asset_callback_url,
|
|
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty')
|
|
})
|
|
|
|
|
|
@login_required
|
|
def edit_subsection(request, location):
|
|
# check that we have permissions to edit this item
|
|
if not has_access(request.user, location):
|
|
raise PermissionDenied()
|
|
|
|
item = modulestore().get_item(location)
|
|
|
|
# TODO: we need a smarter way to figure out what course an item is in
|
|
for course in modulestore().get_courses():
|
|
if (course.location.org == item.location.org and
|
|
course.location.course == item.location.course):
|
|
break
|
|
|
|
lms_link = get_lms_link_for_item(location)
|
|
preview_link = get_lms_link_for_item(location, 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 {0}'.format(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
|
|
|
|
policy_metadata = dict((key, value) for key, value in item.metadata.iteritems()
|
|
if key not in ['display_name', 'start', 'due', 'format'] and key not in item.system_metadata_fields)
|
|
|
|
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,
|
|
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
|
|
'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
|
|
})
|
|
|
|
|
|
@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
|
|
"""
|
|
# check that we have permissions to edit this item
|
|
if not has_access(request.user, location):
|
|
raise PermissionDenied()
|
|
|
|
item = modulestore().get_item(location)
|
|
|
|
# TODO: we need a smarter way to figure out what course an item is in
|
|
for course in modulestore().get_courses():
|
|
if (course.location.org == item.location.org and
|
|
course.location.course == item.location.course):
|
|
break
|
|
|
|
lms_link = get_lms_link_for_item(item.location)
|
|
|
|
component_templates = defaultdict(list)
|
|
|
|
templates = modulestore().get_items(Location('i4x', 'edx', 'templates'))
|
|
for template in templates:
|
|
if template.location.category in COMPONENT_TYPES:
|
|
component_templates[template.location.category].append((
|
|
template.display_name,
|
|
template.location.url(),
|
|
'markdown' in template.metadata,
|
|
'empty' in template.metadata
|
|
))
|
|
|
|
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_link = '//{preview}{lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'.format(
|
|
preview='preview.',
|
|
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)
|
|
|
|
try:
|
|
published_date = time.strftime('%B %d, %Y', item.metadata.get('published_date'))
|
|
except TypeError:
|
|
published_date = None
|
|
|
|
return render_to_response('unit.html', {
|
|
'context_course': course,
|
|
'active_tab': 'courseware',
|
|
'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_date_display(datetime.fromtimestamp(time.mktime(containing_subsection.start))) if containing_subsection.start is not None else None,
|
|
'section': containing_section,
|
|
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
|
|
'unit_state': unit_state,
|
|
'published_date': published_date,
|
|
})
|
|
|
|
|
|
@login_required
|
|
def preview_component(request, location):
|
|
# TODO (vshnayder): change name from id to location in coffee+html as well.
|
|
if not has_access(request.user, location):
|
|
raise HttpResponseForbidden()
|
|
|
|
component = modulestore().get_item(location)
|
|
|
|
return render_to_response('component.html', {
|
|
'preview': get_module_previews(request, component)[0],
|
|
'editor': wrap_xmodule(component.get_html, component, 'xmodule_edit.html')(),
|
|
})
|
|
|
|
|
|
@expect_json
|
|
@login_required
|
|
@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):
|
|
raise HttpResponseForbidden()
|
|
|
|
if request.method == 'GET':
|
|
return HttpResponse(json.dumps(CourseGradingModel.get_section_grader_type(location)),
|
|
mimetype="application/json")
|
|
elif request.method == 'POST': # post or put, doesn't matter.
|
|
return HttpResponse(json.dumps(CourseGradingModel.update_section_grader_type(location, request.POST)),
|
|
mimetype="application/json")
|
|
|
|
|
|
def user_author_string(user):
|
|
'''Get an author string for commits by this user. Format:
|
|
first last <email@email.com>.
|
|
|
|
If the first and last names are blank, uses the username instead.
|
|
Assumes that the email is not blank.
|
|
'''
|
|
f = user.first_name
|
|
l = user.last_name
|
|
if f == '' and l == '':
|
|
f = user.username
|
|
return '{first} {last} <{email}>'.format(first=f,
|
|
last=l,
|
|
email=user.email)
|
|
|
|
|
|
@login_required
|
|
def preview_dispatch(request, preview_id, location, dispatch=None):
|
|
"""
|
|
Dispatch an AJAX action to a preview XModule
|
|
|
|
Expects a POST request, and passes the arguments to the module
|
|
|
|
preview_id (str): An identifier specifying which preview this module is used for
|
|
location: The Location of the module to dispatch to
|
|
dispatch: The action to execute
|
|
"""
|
|
|
|
instance_state, shared_state = load_preview_state(request, preview_id, location)
|
|
descriptor = modulestore().get_item(location)
|
|
instance = load_preview_module(request, preview_id, descriptor, instance_state, shared_state)
|
|
# Let the module handle the AJAX
|
|
try:
|
|
ajax_return = instance.handle_ajax(dispatch, request.POST)
|
|
except NotFoundError:
|
|
log.exception("Module indicating to user that request doesn't exist")
|
|
raise Http404
|
|
except:
|
|
log.exception("error processing ajax call")
|
|
raise
|
|
|
|
save_preview_state(request, preview_id, location, instance.get_instance_state(), instance.get_shared_state())
|
|
return HttpResponse(ajax_return)
|
|
|
|
|
|
def load_preview_state(request, preview_id, location):
|
|
"""
|
|
Load the state of a preview module from the request
|
|
|
|
preview_id (str): An identifier specifying which preview this module is used for
|
|
location: The Location of the module to dispatch to
|
|
"""
|
|
if 'preview_states' not in request.session:
|
|
request.session['preview_states'] = defaultdict(dict)
|
|
|
|
instance_state = request.session['preview_states'][preview_id, location].get('instance')
|
|
shared_state = request.session['preview_states'][preview_id, location].get('shared')
|
|
|
|
return instance_state, shared_state
|
|
|
|
|
|
def save_preview_state(request, preview_id, location, instance_state, shared_state):
|
|
"""
|
|
Save the state of a preview module to the request
|
|
|
|
preview_id (str): An identifier specifying which preview this module is used for
|
|
location: The Location of the module to dispatch to
|
|
instance_state: The instance state to save
|
|
shared_state: The shared state to save
|
|
"""
|
|
if 'preview_states' not in request.session:
|
|
request.session['preview_states'] = defaultdict(dict)
|
|
|
|
# request.session doesn't notice indirect changes; so, must set its dict w/ every change to get
|
|
# it to persist: http://www.djangobook.com/en/2.0/chapter14.html
|
|
preview_states = request.session['preview_states']
|
|
preview_states[preview_id, location]['instance'] = instance_state
|
|
preview_states[preview_id, location]['shared'] = shared_state
|
|
request.session['preview_states'] = preview_states # make session mgmt notice the update
|
|
|
|
|
|
def render_from_lms(template_name, dictionary, context=None, namespace='main'):
|
|
"""
|
|
Render a template using the LMS MAKO_TEMPLATES
|
|
"""
|
|
return render_to_string(template_name, dictionary, context, namespace="lms." + namespace)
|
|
|
|
|
|
def preview_module_system(request, preview_id, descriptor):
|
|
"""
|
|
Returns a ModuleSystem for the specified descriptor that is specialized for
|
|
rendering module previews.
|
|
|
|
request: The active django request
|
|
preview_id (str): An identifier specifying which preview this module is used for
|
|
descriptor: An XModuleDescriptor
|
|
"""
|
|
|
|
return ModuleSystem(
|
|
ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']).rstrip('/'),
|
|
# TODO (cpennington): Do we want to track how instructors are using the preview problems?
|
|
track_function=lambda type, event: None,
|
|
filestore=descriptor.system.resources_fs,
|
|
get_module=partial(get_preview_module, request, preview_id),
|
|
render_template=render_from_lms,
|
|
debug=True,
|
|
replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_namespace=descriptor.location),
|
|
user=request.user,
|
|
)
|
|
|
|
|
|
def get_preview_module(request, preview_id, descriptor):
|
|
"""
|
|
Returns a preview XModule at the specified location. The preview_data is chosen arbitrarily
|
|
from the set of preview data for the descriptor specified by Location
|
|
|
|
request: The active django request
|
|
preview_id (str): An identifier specifying which preview this module is used for
|
|
location: A Location
|
|
"""
|
|
instance_state, shared_state = descriptor.get_sample_state()[0]
|
|
return load_preview_module(request, preview_id, descriptor, instance_state, shared_state)
|
|
|
|
|
|
def load_preview_module(request, preview_id, descriptor, instance_state, shared_state):
|
|
"""
|
|
Return a preview XModule instantiated from the supplied descriptor, instance_state, and shared_state
|
|
|
|
request: The active django request
|
|
preview_id (str): An identifier specifying which preview this module is used for
|
|
descriptor: An XModuleDescriptor
|
|
instance_state: An instance state string
|
|
shared_state: A shared state string
|
|
"""
|
|
system = preview_module_system(request, preview_id, descriptor)
|
|
try:
|
|
module = descriptor.xmodule_constructor(system)(instance_state, shared_state)
|
|
except:
|
|
module = ErrorDescriptor.from_descriptor(
|
|
descriptor,
|
|
error_msg=exc_info_to_str(sys.exc_info())
|
|
).xmodule_constructor(system)(None, None)
|
|
|
|
# cdodge: Special case
|
|
if module.location.category == 'static_tab':
|
|
module.get_html = wrap_xmodule(
|
|
module.get_html,
|
|
module,
|
|
"xmodule_tab_display.html",
|
|
)
|
|
else:
|
|
module.get_html = wrap_xmodule(
|
|
module.get_html,
|
|
module,
|
|
"xmodule_display.html",
|
|
)
|
|
|
|
module.get_html = replace_static_urls(
|
|
module.get_html,
|
|
module.metadata.get('data_dir', module.location.course),
|
|
course_namespace=Location([module.location.tag, module.location.org, module.location.course, None, None])
|
|
)
|
|
save_preview_state(request, preview_id, descriptor.location.url(),
|
|
module.get_instance_state(), module.get_shared_state())
|
|
|
|
return module
|
|
|
|
|
|
def get_module_previews(request, descriptor):
|
|
"""
|
|
Returns a list of preview XModule html contents. One preview is returned for each
|
|
pair of states returned by get_sample_state() for the supplied descriptor.
|
|
|
|
descriptor: An XModuleDescriptor
|
|
"""
|
|
preview_html = []
|
|
for idx, (instance_state, shared_state) in enumerate(descriptor.get_sample_state()):
|
|
module = load_preview_module(request, str(idx), descriptor, instance_state, shared_state)
|
|
preview_html.append(module.get_html())
|
|
return preview_html
|
|
|
|
|
|
def _xmodule_recurse(item, action):
|
|
for child in item.get_children():
|
|
_xmodule_recurse(child, action)
|
|
|
|
action(item)
|
|
|
|
|
|
@login_required
|
|
@expect_json
|
|
def delete_item(request):
|
|
item_location = request.POST['id']
|
|
item_loc = Location(item_location)
|
|
|
|
# check permissions for this user within this course
|
|
if not has_access(request.user, item_location):
|
|
raise PermissionDenied()
|
|
|
|
# optional parameter to delete all children (default False)
|
|
delete_children = request.POST.get('delete_children', False)
|
|
delete_all_versions = request.POST.get('delete_all_versions', False)
|
|
|
|
item = modulestore().get_item(item_location)
|
|
|
|
store = get_modulestore(item_loc)
|
|
|
|
|
|
# @TODO: this probably leaves draft items dangling. My preferance would be for the semantic to be
|
|
# if item.location.revision=None, then delete both draft and published version
|
|
# if caller wants to only delete the draft than the caller should put item.location.revision='draft'
|
|
|
|
if delete_children:
|
|
_xmodule_recurse(item, lambda i: store.delete_item(i.location))
|
|
else:
|
|
store.delete_item(item.location)
|
|
|
|
# cdodge: this is a bit of a hack until I can talk with Cale about the
|
|
# semantics of delete_item whereby the store is draft aware. Right now calling
|
|
# delete_item on a vertical tries to delete the draft version leaving the
|
|
# requested delete to never occur
|
|
if item.location.revision is None and item.location.category == 'vertical' and delete_all_versions:
|
|
modulestore('direct').delete_item(item.location)
|
|
|
|
return HttpResponse()
|
|
|
|
|
|
@login_required
|
|
@expect_json
|
|
def save_item(request):
|
|
item_location = request.POST['id']
|
|
|
|
# check permissions for this user within this course
|
|
if not has_access(request.user, item_location):
|
|
raise PermissionDenied()
|
|
|
|
store = get_modulestore(Location(item_location));
|
|
|
|
if request.POST.get('data') is not None:
|
|
data = request.POST['data']
|
|
store.update_item(item_location, data)
|
|
|
|
# cdodge: note calling request.POST.get('children') will return None if children is an empty array
|
|
# so it lead to a bug whereby the last component to be deleted in the UI was not actually
|
|
# deleting the children object from the children collection
|
|
if 'children' in request.POST and request.POST['children'] is not None:
|
|
children = request.POST['children']
|
|
store.update_children(item_location, children)
|
|
|
|
# cdodge: also commit any metadata which might have been passed along in the
|
|
# POST from the client, if it is there
|
|
# NOTE, that the postback is not the complete metadata, as there's system metadata which is
|
|
# not presented to the end-user for editing. So let's fetch the original and
|
|
# 'apply' the submitted metadata, so we don't end up deleting system metadata
|
|
if request.POST.get('metadata') is not None:
|
|
posted_metadata = request.POST['metadata']
|
|
# fetch original
|
|
existing_item = modulestore().get_item(item_location)
|
|
|
|
# update existing metadata with submitted metadata (which can be partial)
|
|
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
|
|
for metadata_key in posted_metadata.keys():
|
|
|
|
# let's strip out any metadata fields from the postback which have been identified as system metadata
|
|
# and therefore should not be user-editable, so we should accept them back from the client
|
|
if metadata_key in existing_item.system_metadata_fields:
|
|
del posted_metadata[metadata_key]
|
|
elif posted_metadata[metadata_key] is None:
|
|
# remove both from passed in collection as well as the collection read in from the modulestore
|
|
if metadata_key in existing_item.metadata:
|
|
del existing_item.metadata[metadata_key]
|
|
del posted_metadata[metadata_key]
|
|
|
|
# overlay the new metadata over the modulestore sourced collection to support partial updates
|
|
existing_item.metadata.update(posted_metadata)
|
|
|
|
# commit to datastore
|
|
store.update_metadata(item_location, existing_item.metadata)
|
|
|
|
return HttpResponse()
|
|
|
|
|
|
@login_required
|
|
@expect_json
|
|
def create_draft(request):
|
|
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().clone_item(location, location)
|
|
|
|
return HttpResponse()
|
|
|
|
|
|
@login_required
|
|
@expect_json
|
|
def publish_draft(request):
|
|
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):
|
|
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()
|
|
|
|
|
|
@login_required
|
|
@expect_json
|
|
def clone_item(request):
|
|
parent_location = Location(request.POST['parent_location'])
|
|
template = Location(request.POST['template'])
|
|
|
|
display_name = request.POST.get('display_name')
|
|
|
|
if not has_access(request.user, parent_location):
|
|
raise PermissionDenied()
|
|
|
|
parent = get_modulestore(template).get_item(parent_location)
|
|
dest_location = parent_location._replace(category=template.category, name=uuid4().hex)
|
|
|
|
new_item = get_modulestore(template).clone_item(template, dest_location)
|
|
|
|
# TODO: This needs to be deleted when we have proper storage for static content
|
|
new_item.metadata['data_dir'] = parent.metadata['data_dir']
|
|
|
|
# replace the display name with an optional parameter passed in from the caller
|
|
if display_name is not None:
|
|
new_item.metadata['display_name'] = display_name
|
|
|
|
get_modulestore(template).update_metadata(new_item.location.url(), new_item.own_metadata)
|
|
|
|
if new_item.location.category not in DETACHED_CATEGORIES:
|
|
get_modulestore(parent.location).update_children(parent_location, parent.definition.get('children', []) + [new_item.location.url()])
|
|
|
|
return HttpResponse(json.dumps({'id': dest_location.url()}))
|
|
|
|
#@login_required
|
|
#@ensure_csrf_cookie
|
|
|
|
|
|
def upload_asset(request, org, course, coursename):
|
|
'''
|
|
cdodge: this method allows for POST uploading of files into the course asset library, which will
|
|
be supported by GridFS in MongoDB.
|
|
'''
|
|
if request.method != 'POST':
|
|
# (cdodge) @todo: Is there a way to do a - say - 'raise Http400'?
|
|
return HttpResponseBadRequest()
|
|
|
|
# construct a location from the passed in path
|
|
location = ['i4x', org, course, 'course', coursename]
|
|
if not has_access(request.user, location):
|
|
return HttpResponseForbidden()
|
|
|
|
# Does the course actually exist?!? Get anything from it to prove its existance
|
|
|
|
try:
|
|
item = modulestore().get_item(location)
|
|
except:
|
|
# no return it as a Bad Request response
|
|
logging.error('Could not find course' + location)
|
|
return HttpResponseBadRequest()
|
|
|
|
# compute a 'filename' which is similar to the location formatting, we're using the 'filename'
|
|
# nomenclature since we're using a FileSystem paradigm here. We're just imposing
|
|
# the Location string formatting expectations to keep things a bit more consistent
|
|
|
|
filename = request.FILES['file'].name
|
|
mime_type = request.FILES['file'].content_type
|
|
filedata = request.FILES['file'].read()
|
|
|
|
content_loc = StaticContent.compute_location(org, course, filename)
|
|
content = StaticContent(content_loc, filename, mime_type, filedata)
|
|
|
|
# first let's see if a thumbnail can be created
|
|
(thumbnail_content, thumbnail_location) = contentstore().generate_thumbnail(content)
|
|
|
|
# delete cached thumbnail even if one couldn't be created this time (else the old thumbnail will continue to show)
|
|
del_cached_content(thumbnail_location)
|
|
# now store thumbnail location only if we could create it
|
|
if thumbnail_content is not None:
|
|
content.thumbnail_location = thumbnail_location
|
|
|
|
#then commit the content
|
|
contentstore().save(content)
|
|
del_cached_content(content.location)
|
|
|
|
# readback the saved content - we need the database timestamp
|
|
readback = contentstore().find(content.location)
|
|
|
|
response_payload = {'displayname': content.name,
|
|
'uploadDate': get_date_display(readback.last_modified_at),
|
|
'url': StaticContent.get_url_path_from_location(content.location),
|
|
'thumb_url': StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None,
|
|
'msg': 'Upload completed'
|
|
}
|
|
|
|
response = HttpResponse(json.dumps(response_payload))
|
|
response['asset_url'] = StaticContent.get_url_path_from_location(content.location)
|
|
return response
|
|
|
|
'''
|
|
This view will return all CMS users who are editors for the specified course
|
|
'''
|
|
|
|
|
|
@login_required
|
|
@ensure_csrf_cookie
|
|
def manage_users(request, location):
|
|
|
|
# check that logged in user has permissions to this item
|
|
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME) and not has_access(request.user, location, role=STAFF_ROLE_NAME):
|
|
raise PermissionDenied()
|
|
|
|
course_module = modulestore().get_item(location)
|
|
|
|
return render_to_response('manage_users.html', {
|
|
'active_tab': 'users',
|
|
'context_course': course_module,
|
|
'staff': get_users_in_course_group_by_role(location, STAFF_ROLE_NAME),
|
|
'add_user_postback_url': reverse('add_user', args=[location]).rstrip('/'),
|
|
'remove_user_postback_url': reverse('remove_user', args=[location]).rstrip('/'),
|
|
'allow_actions': has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME),
|
|
'request_user_id': request.user.id
|
|
})
|
|
|
|
|
|
def create_json_response(errmsg=None):
|
|
if errmsg is not None:
|
|
resp = HttpResponse(json.dumps({'Status': 'Failed', 'ErrMsg': errmsg}))
|
|
else:
|
|
resp = HttpResponse(json.dumps({'Status': 'OK'}))
|
|
|
|
return resp
|
|
|
|
'''
|
|
This POST-back view will add a user - specified by email - to the list of editors for
|
|
the specified course
|
|
'''
|
|
|
|
|
|
@expect_json
|
|
@login_required
|
|
@ensure_csrf_cookie
|
|
def add_user(request, location):
|
|
email = request.POST["email"]
|
|
|
|
if email == '':
|
|
return create_json_response('Please specify an email address.')
|
|
|
|
# check that logged in user has admin permissions to this course
|
|
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
|
|
raise PermissionDenied()
|
|
|
|
user = get_user_by_email(email)
|
|
|
|
# user doesn't exist?!? Return error.
|
|
if user is None:
|
|
return create_json_response('Could not find user by email address \'{0}\'.'.format(email))
|
|
|
|
# user exists, but hasn't activated account?!?
|
|
if not user.is_active:
|
|
return create_json_response('User {0} has registered but has not yet activated his/her account.'.format(email))
|
|
|
|
# ok, we're cool to add to the course group
|
|
add_user_to_course_group(request.user, user, location, STAFF_ROLE_NAME)
|
|
|
|
return create_json_response()
|
|
|
|
'''
|
|
This POST-back view will remove a user - specified by email - from the list of editors for
|
|
the specified course
|
|
'''
|
|
|
|
|
|
@expect_json
|
|
@login_required
|
|
@ensure_csrf_cookie
|
|
def remove_user(request, location):
|
|
email = request.POST["email"]
|
|
|
|
# check that logged in user has admin permissions on this course
|
|
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
|
|
raise PermissionDenied()
|
|
|
|
user = get_user_by_email(email)
|
|
if user is None:
|
|
return create_json_response('Could not find user by email address \'{0}\'.'.format(email))
|
|
|
|
# make sure we're not removing ourselves
|
|
if user.id == request.user.id:
|
|
raise PermissionDenied()
|
|
|
|
remove_user_from_course_group(request.user, user, location, STAFF_ROLE_NAME)
|
|
|
|
return create_json_response()
|
|
|
|
|
|
# points to the temporary course landing page with log in and sign up
|
|
def landing(request, org, course, coursename):
|
|
return render_to_response('temp-course-landing.html', {})
|
|
|
|
|
|
@login_required
|
|
@ensure_csrf_cookie
|
|
def static_pages(request, org, course, coursename):
|
|
|
|
location = ['i4x', org, course, 'course', coursename]
|
|
|
|
# check that logged in user has permissions to this item
|
|
if not has_access(request.user, location):
|
|
raise PermissionDenied()
|
|
|
|
course = modulestore().get_item(location)
|
|
|
|
return render_to_response('static-pages.html', {
|
|
'active_tab': 'pages',
|
|
'context_course': course,
|
|
})
|
|
|
|
|
|
def edit_static(request, org, course, coursename):
|
|
return render_to_response('edit-static-page.html', {})
|
|
|
|
|
|
@login_required
|
|
@expect_json
|
|
def reorder_static_tabs(request):
|
|
tabs = request.POST['tabs']
|
|
course = get_course_for_item(tabs[0])
|
|
|
|
if not has_access(request.user, course.location):
|
|
raise PermissionDenied()
|
|
|
|
# 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(Location(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].metadata.get('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
|
|
modulestore('direct').update_metadata(course.location, course.metadata)
|
|
return HttpResponse()
|
|
|
|
|
|
@login_required
|
|
@ensure_csrf_cookie
|
|
def edit_tabs(request, org, course, coursename):
|
|
location = ['i4x', org, course, 'course', coursename]
|
|
course_item = modulestore().get_item(location)
|
|
static_tabs_loc = Location('i4x', org, course, 'static_tab', None)
|
|
|
|
# check that logged in user has permissions to this item
|
|
if not has_access(request.user, location):
|
|
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)
|
|
|
|
# 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 = Location(location)._replace(category='static_tab', name=static_tab_ref['url_slug'])
|
|
static_tabs.append(modulestore('direct').get_item(static_tab_loc))
|
|
|
|
components = [
|
|
static_tab.location.url()
|
|
for static_tab
|
|
in static_tabs
|
|
]
|
|
|
|
return render_to_response('edit-tabs.html', {
|
|
'active_tab': 'pages',
|
|
'context_course': course_item,
|
|
'components': components
|
|
})
|
|
|
|
|
|
def not_found(request):
|
|
return render_to_response('error.html', {'error': '404'})
|
|
|
|
|
|
def server_error(request):
|
|
return render_to_response('error.html', {'error': '500'})
|
|
|
|
|
|
@login_required
|
|
@ensure_csrf_cookie
|
|
def course_info(request, org, course, name, provided_id=None):
|
|
"""
|
|
Send models and views as well as html for editing the course info to the client.
|
|
|
|
org, course, name: Attributes of the Location for the item to edit
|
|
"""
|
|
location = ['i4x', org, course, 'course', name]
|
|
|
|
# check that logged in user has permissions to this item
|
|
if not has_access(request.user, location):
|
|
raise PermissionDenied()
|
|
|
|
course_module = modulestore().get_item(location)
|
|
|
|
# get current updates
|
|
location = ['i4x', org, course, 'course_info', "updates"]
|
|
|
|
return render_to_response('course_info.html', {
|
|
'active_tab': 'courseinfo-tab',
|
|
'context_course': course_module,
|
|
'url_base': "/" + org + "/" + course + "/",
|
|
'course_updates': json.dumps(get_course_updates(location)),
|
|
'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url()
|
|
})
|
|
|
|
|
|
@expect_json
|
|
@login_required
|
|
@ensure_csrf_cookie
|
|
def course_info_updates(request, org, course, provided_id=None):
|
|
"""
|
|
restful CRUD operations on course_info updates.
|
|
|
|
org, course: Attributes of the Location for the item to edit
|
|
provided_id should be none if it's new (create) and a composite of the update db id + index otherwise.
|
|
"""
|
|
# ??? No way to check for access permission afaik
|
|
# get current updates
|
|
location = ['i4x', org, course, 'course_info', "updates"]
|
|
|
|
# Hmmm, provided_id is coming as empty string on create whereas I believe it used to be None :-(
|
|
# Possibly due to my removing the seemingly redundant pattern in urls.py
|
|
if provided_id == '':
|
|
provided_id = None
|
|
|
|
# check that logged in user has permissions to this item
|
|
if not has_access(request.user, location):
|
|
raise PermissionDenied()
|
|
|
|
# NB: we're setting Backbone.emulateHTTP to true on the client so everything comes as a post!!!
|
|
if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
|
|
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
|
|
else:
|
|
real_method = request.method
|
|
|
|
if request.method == 'GET':
|
|
return HttpResponse(json.dumps(get_course_updates(location)), mimetype="application/json")
|
|
elif real_method == 'DELETE': # coming as POST need to pull from Request Header X-HTTP-Method-Override DELETE
|
|
return HttpResponse(json.dumps(delete_course_update(location, request.POST, provided_id)), mimetype="application/json")
|
|
elif request.method == 'POST':
|
|
try:
|
|
return HttpResponse(json.dumps(update_course_updates(location, request.POST, provided_id)), mimetype="application/json")
|
|
except:
|
|
return HttpResponseBadRequest("Failed to save: malformed html", content_type="text/plain")
|
|
|
|
|
|
@expect_json
|
|
@login_required
|
|
@ensure_csrf_cookie
|
|
def module_info(request, module_location):
|
|
location = Location(module_location)
|
|
|
|
# check that logged in user has permissions to this item
|
|
if not has_access(request.user, location):
|
|
raise PermissionDenied()
|
|
|
|
# NB: we're setting Backbone.emulateHTTP to true on the client so everything comes as a post!!!
|
|
if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
|
|
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
|
|
else:
|
|
real_method = request.method
|
|
|
|
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 real_method == 'GET':
|
|
return HttpResponse(json.dumps(get_module_info(get_modulestore(location), location, rewrite_static_links=rewrite_static_links)), mimetype="application/json")
|
|
elif real_method == 'POST' or real_method == 'PUT':
|
|
return HttpResponse(json.dumps(set_module_info(get_modulestore(location), location, request.POST)), mimetype="application/json")
|
|
else:
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
@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 = ['i4x', org, course, 'course', name]
|
|
|
|
# check that logged in user has permissions to this item
|
|
if not has_access(request.user, location):
|
|
raise PermissionDenied()
|
|
|
|
course_module = modulestore().get_item(location)
|
|
course_details = CourseDetails.fetch(location)
|
|
|
|
return render_to_response('settings.html', {
|
|
'active_tab': 'settings',
|
|
'context_course': course_module,
|
|
'course_details': json.dumps(course_details, cls=CourseSettingsEncoder)
|
|
})
|
|
|
|
|
|
@expect_json
|
|
@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
|
|
"""
|
|
location = ['i4x', org, course, 'course', name]
|
|
|
|
# check that logged in user has permissions to this item
|
|
if not has_access(request.user, location):
|
|
raise PermissionDenied()
|
|
|
|
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 HttpResponse(json.dumps(manager.fetch(Location(['i4x', org, course, 'course', name])), cls=CourseSettingsEncoder),
|
|
mimetype="application/json")
|
|
elif request.method == 'POST': # post or put, doesn't matter.
|
|
return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder),
|
|
mimetype="application/json")
|
|
|
|
|
|
@expect_json
|
|
@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 = ['i4x', org, course, 'course', name]
|
|
|
|
# check that logged in user has permissions to this item
|
|
if not has_access(request.user, location):
|
|
raise PermissionDenied()
|
|
|
|
if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
|
|
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
|
|
else:
|
|
real_method = request.method
|
|
|
|
if real_method == 'GET':
|
|
# Cannot just do a get w/o knowing the course name :-(
|
|
return HttpResponse(json.dumps(CourseGradingModel.fetch_grader(Location(['i4x', org, course, 'course', name]), grader_index)),
|
|
mimetype="application/json")
|
|
elif real_method == "DELETE":
|
|
# ??? Shoudl this return anything? Perhaps success fail?
|
|
CourseGradingModel.delete_grader(Location(['i4x', org, course, 'course', name]), grader_index)
|
|
return HttpResponse()
|
|
elif request.method == 'POST': # post or put, doesn't matter.
|
|
return HttpResponse(json.dumps(CourseGradingModel.update_grader_from_json(Location(['i4x', org, course, 'course', name]), request.POST)),
|
|
mimetype="application/json")
|
|
|
|
|
|
@login_required
|
|
@ensure_csrf_cookie
|
|
def asset_index(request, org, course, name):
|
|
"""
|
|
Display an editable asset library
|
|
|
|
org, course, name: Attributes of the Location for the item to edit
|
|
"""
|
|
location = ['i4x', org, course, 'course', name]
|
|
|
|
# check that logged in user has permissions to this item
|
|
if not has_access(request.user, location):
|
|
raise PermissionDenied()
|
|
|
|
|
|
upload_asset_callback_url = reverse('upload_asset', kwargs={
|
|
'org': org,
|
|
'course': course,
|
|
'coursename': name
|
|
})
|
|
|
|
course_module = modulestore().get_item(location)
|
|
|
|
course_reference = StaticContent.compute_location(org, course, name)
|
|
assets = contentstore().get_all_content_for_course(course_reference)
|
|
|
|
# sort in reverse upload date order
|
|
assets = sorted(assets, key=lambda asset: asset['uploadDate'], reverse=True)
|
|
|
|
thumbnails = contentstore().get_all_content_thumbnails_for_course(course_reference)
|
|
asset_display = []
|
|
for asset in assets:
|
|
id = asset['_id']
|
|
display_info = {}
|
|
display_info['displayname'] = asset['displayname']
|
|
display_info['uploadDate'] = get_date_display(asset['uploadDate'])
|
|
|
|
asset_location = StaticContent.compute_location(id['org'], id['course'], id['name'])
|
|
display_info['url'] = StaticContent.get_url_path_from_location(asset_location)
|
|
|
|
# note, due to the schema change we may not have a 'thumbnail_location' in the result set
|
|
_thumbnail_location = asset.get('thumbnail_location', None)
|
|
thumbnail_location = Location(_thumbnail_location) if _thumbnail_location is not None else None
|
|
display_info['thumb_url'] = StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_location is not None else None
|
|
|
|
asset_display.append(display_info)
|
|
|
|
return render_to_response('asset_index.html', {
|
|
'active_tab': 'assets',
|
|
'context_course': course_module,
|
|
'assets': asset_display,
|
|
'upload_asset_callback_url': upload_asset_callback_url
|
|
})
|
|
|
|
|
|
# points to the temporary edge page
|
|
def edge(request):
|
|
return render_to_response('university_profiles/edge.html', {})
|
|
|
|
|
|
@login_required
|
|
@expect_json
|
|
def create_new_course(request):
|
|
|
|
if settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff:
|
|
raise PermissionDenied()
|
|
|
|
# This logic is repeated in xmodule/modulestore/tests/factories.py
|
|
# so if you change anything here, you need to also change it there.
|
|
# TODO: write a test that creates two courses, one with the factory and
|
|
# the other with this method, then compare them to make sure they are
|
|
# equivalent.
|
|
template = Location(request.POST['template'])
|
|
org = request.POST.get('org')
|
|
number = request.POST.get('number')
|
|
display_name = request.POST.get('display_name')
|
|
|
|
try:
|
|
dest_location = Location('i4x', org, number, 'course', Location.clean(display_name))
|
|
except InvalidLocationError as e:
|
|
return HttpResponse(json.dumps({'ErrMsg': "Unable to create course '" + display_name + "'.\n\n" + e.message}))
|
|
|
|
# see if the course already exists
|
|
existing_course = None
|
|
try:
|
|
existing_course = modulestore('direct').get_item(dest_location)
|
|
except ItemNotFoundError:
|
|
pass
|
|
|
|
if existing_course is not None:
|
|
return HttpResponse(json.dumps({'ErrMsg': 'There is already a course defined with this name.'}))
|
|
|
|
course_search_location = ['i4x', dest_location.org, dest_location.course, 'course', None]
|
|
courses = modulestore().get_items(course_search_location)
|
|
|
|
if len(courses) > 0:
|
|
return HttpResponse(json.dumps({'ErrMsg': 'There is already a course defined with the same organization and course number.'}))
|
|
|
|
new_course = modulestore('direct').clone_item(template, dest_location)
|
|
|
|
if display_name is not None:
|
|
new_course.metadata['display_name'] = display_name
|
|
|
|
# we need a 'data_dir' for legacy reasons
|
|
new_course.metadata['data_dir'] = uuid4().hex
|
|
|
|
# set a default start date to now
|
|
new_course.metadata['start'] = stringify_time(time.gmtime())
|
|
|
|
initialize_course_tabs(new_course)
|
|
|
|
create_all_course_groups(request.user, new_course.location)
|
|
|
|
return HttpResponse(json.dumps({'id': new_course.location.url()}))
|
|
|
|
|
|
def initialize_course_tabs(course):
|
|
# set up the default tabs
|
|
# I've added this because when we add static tabs, the LMS either expects a None for the tabs list or
|
|
# at least a list populated with the minimal times
|
|
# @TODO: I don't like the fact that the presentation tier is away of these data related constraints, let's find a better
|
|
# place for this. Also rather than using a simple list of dictionaries a nice class model would be helpful here
|
|
|
|
# This logic is repeated in xmodule/modulestore/tests/factories.py
|
|
# so if you change anything here, you need to also change it there.
|
|
course.tabs = [{"type": "courseware"},
|
|
{"type": "course_info", "name": "Course Info"},
|
|
{"type": "discussion", "name": "Discussion"},
|
|
{"type": "wiki", "name": "Wiki"},
|
|
{"type": "progress", "name": "Progress"}]
|
|
|
|
modulestore('direct').update_metadata(course.location.url(), course.own_metadata)
|
|
|
|
|
|
@ensure_csrf_cookie
|
|
@login_required
|
|
def import_course(request, org, course, name):
|
|
|
|
location = ['i4x', org, course, 'course', name]
|
|
|
|
# check that logged in user has permissions to this item
|
|
if not has_access(request.user, location):
|
|
raise PermissionDenied()
|
|
|
|
if request.method == 'POST':
|
|
filename = request.FILES['course-data'].name
|
|
|
|
if not filename.endswith('.tar.gz'):
|
|
return HttpResponse(json.dumps({'ErrMsg': 'We only support uploading a .tar.gz file.'}))
|
|
|
|
data_root = path(settings.GITHUB_REPO_ROOT)
|
|
|
|
course_subdir = "{0}-{1}-{2}".format(org, course, name)
|
|
course_dir = data_root / course_subdir
|
|
if not course_dir.isdir():
|
|
os.mkdir(course_dir)
|
|
|
|
temp_filepath = course_dir / filename
|
|
|
|
logging.debug('importing course to {0}'.format(temp_filepath))
|
|
|
|
# stream out the uploaded files in chunks to disk
|
|
temp_file = open(temp_filepath, 'wb+')
|
|
for chunk in request.FILES['course-data'].chunks():
|
|
temp_file.write(chunk)
|
|
temp_file.close()
|
|
|
|
tf = tarfile.open(temp_filepath)
|
|
tf.extractall(course_dir + '/')
|
|
|
|
# find the 'course.xml' file
|
|
|
|
for r, d, f in os.walk(course_dir):
|
|
for files in f:
|
|
if files == 'course.xml':
|
|
break
|
|
if files == 'course.xml':
|
|
break
|
|
|
|
if files != 'course.xml':
|
|
return HttpResponse(json.dumps({'ErrMsg': 'Could not find the course.xml file in the package.'}))
|
|
|
|
logging.debug('found course.xml at {0}'.format(r))
|
|
|
|
if r != course_dir:
|
|
for fname in os.listdir(r):
|
|
shutil.move(r / fname, course_dir)
|
|
|
|
module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT,
|
|
[course_subdir], load_error_modules=False, static_content_store=contentstore(), target_location_namespace=Location(location))
|
|
|
|
# we can blow this away when we're done importing.
|
|
shutil.rmtree(course_dir)
|
|
|
|
logging.debug('new course at {0}'.format(course_items[0].location))
|
|
|
|
create_all_course_groups(request.user, course_items[0].location)
|
|
|
|
return HttpResponse(json.dumps({'Status': 'OK'}))
|
|
else:
|
|
course_module = modulestore().get_item(location)
|
|
|
|
return render_to_response('import.html', {
|
|
'context_course': course_module,
|
|
'active_tab': 'import',
|
|
'successful_import_redirect_url': reverse('course_index', args=[
|
|
course_module.location.org,
|
|
course_module.location.course,
|
|
course_module.location.name])
|
|
})
|
|
|
|
|
|
@ensure_csrf_cookie
|
|
@login_required
|
|
def generate_export_course(request, org, course, name):
|
|
location = ['i4x', org, course, 'course', name]
|
|
# check that logged in user has permissions to this item
|
|
if not has_access(request.user, location):
|
|
raise PermissionDenied()
|
|
|
|
loc = Location(location)
|
|
export_file = NamedTemporaryFile(prefix=name + '.', suffix=".tar.gz")
|
|
|
|
root_dir = path(mkdtemp())
|
|
|
|
# export out to a tempdir
|
|
|
|
logging.debug('root = {0}'.format(root_dir))
|
|
|
|
export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name)
|
|
#filename = root_dir / name + '.tar.gz'
|
|
|
|
logging.debug('tar file being generated at {0}'.format(export_file.name))
|
|
tf = tarfile.open(name=export_file.name, mode='w:gz')
|
|
tf.add(root_dir / name, arcname=name)
|
|
tf.close()
|
|
|
|
# remove temp dir
|
|
shutil.rmtree(root_dir / name)
|
|
|
|
wrapper = FileWrapper(export_file)
|
|
response = HttpResponse(wrapper, content_type='application/x-tgz')
|
|
response['Content-Disposition'] = 'attachment; filename=%s' % os.path.basename(export_file.name)
|
|
response['Content-Length'] = os.path.getsize(export_file.name)
|
|
return response
|
|
|
|
|
|
@ensure_csrf_cookie
|
|
@login_required
|
|
def export_course(request, org, course, name):
|
|
|
|
location = ['i4x', org, course, 'course', name]
|
|
course_module = modulestore().get_item(location)
|
|
# check that logged in user has permissions to this item
|
|
if not has_access(request.user, location):
|
|
raise PermissionDenied()
|
|
|
|
return render_to_response('export.html', {
|
|
'context_course': course_module,
|
|
'active_tab': 'export',
|
|
'successful_import_redirect_url': ''
|
|
})
|
|
|
|
|
|
def event(request):
|
|
'''
|
|
A noop to swallow the analytics call so that cms methods don't spook and poor developers looking at
|
|
console logs don't get distracted :-)
|
|
'''
|
|
return HttpResponse(True)
|