From 0bea50ede17a190a8f0a9ba61e71535be639090b Mon Sep 17 00:00:00 2001 From: Steve Strassmann Date: Thu, 9 May 2013 16:24:21 -0400 Subject: [PATCH 01/10] start refactoring views.py --- cms/djangoapps/contentstore/views.py | 1685 ----------------- cms/djangoapps/contentstore/views/__init__.py | 17 + cms/djangoapps/contentstore/views/access.py | 39 + cms/djangoapps/contentstore/views/assets.py | 118 ++ .../contentstore/views/checklist.py | 98 + cms/djangoapps/contentstore/views/course.py | 202 ++ cms/djangoapps/contentstore/views/error.py | 21 + cms/djangoapps/contentstore/views/item.py | 123 ++ cms/djangoapps/contentstore/views/preview.py | 156 ++ cms/djangoapps/contentstore/views/public.py | 50 + cms/djangoapps/contentstore/views/requests.py | 91 + .../contentstore/views/session_kv_store.py | 27 + cms/djangoapps/contentstore/views/user.py | 107 ++ 13 files changed, 1049 insertions(+), 1685 deletions(-) delete mode 100644 cms/djangoapps/contentstore/views.py create mode 100644 cms/djangoapps/contentstore/views/__init__.py create mode 100644 cms/djangoapps/contentstore/views/access.py create mode 100644 cms/djangoapps/contentstore/views/assets.py create mode 100644 cms/djangoapps/contentstore/views/checklist.py create mode 100644 cms/djangoapps/contentstore/views/course.py create mode 100644 cms/djangoapps/contentstore/views/error.py create mode 100644 cms/djangoapps/contentstore/views/item.py create mode 100644 cms/djangoapps/contentstore/views/preview.py create mode 100644 cms/djangoapps/contentstore/views/public.py create mode 100644 cms/djangoapps/contentstore/views/requests.py create mode 100644 cms/djangoapps/contentstore/views/session_kv_store.py create mode 100644 cms/djangoapps/contentstore/views/user.py diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py deleted file mode 100644 index 824d2119f1..0000000000 --- a/cms/djangoapps/contentstore/views.py +++ /dev/null @@ -1,1685 +0,0 @@ -from util.json_request import expect_json -import json -import logging -import os -import sys -import time -import tarfile -import shutil -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 - -from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseServerError -from django.http import HttpResponseNotFound -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.modulestore.inheritance import own_metadata -from xblock.core import Scope -from xblock.runtime import KeyValueStore, DbModel, InvalidScopeError -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 xmodule.modulestore.mongo import MongoUsage - -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, ProcessingError -from functools import partial - -from xmodule.contentstore.django import contentstore -from xmodule.contentstore.content import StaticContent -from xmodule.util.date_utils import get_default_time_display - -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, \ - UnitState, get_course_for_item, get_url_reverse, add_open_ended_panel_tab, \ - remove_open_ended_panel_tab - -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 contentstore.module_info_model import get_module_info, set_module_info -from models.settings.course_details import CourseDetails, \ - CourseSettingsEncoder -from models.settings.course_grading import CourseGradingModel -from contentstore.utils import get_modulestore -from django.shortcuts import redirect -from models.settings.course_metadata import CourseMetadata - -# 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'] - -OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"] -ADVANCED_COMPONENT_TYPES = ['annotatable'] + OPEN_ENDED_COMPONENT_TYPES -ADVANCED_COMPONENT_CATEGORY = 'advanced' -ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules' - -# 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}) - - -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): - """ - 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), - }) - - -def howitworks(request): - if request.user.is_authenticated(): - return index(request) - else: - return render_to_response('howitworks.html', {}) - - -# static/proof-of-concept views -def ux_alerts(request): - return render_to_response('ux-alerts.html', {}) - - -# ==== 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('direct').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.display_name, - get_url_reverse('CourseOutline', course), - get_lms_link_for_item(course.location, course_id=course.location.course_id)) - 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 = get_location_and_verify_access(request, org, course, name) - - lms_link = get_lms_link_for_item(location) - - upload_asset_callback_url = reverse('upload_asset', kwargs={ - 'org': org, - 'course': course, - 'coursename': name - }) - - course = modulestore().get_item(location, depth=3) - sections = course.get_children() - - return render_to_response('overview.html', { - 'active_tab': 'courseware', - 'context_course': course, - 'lms_link': lms_link, - '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 - course = get_course_for_item(location) - if not has_access(request.user, course.location): - raise PermissionDenied() - - item = modulestore().get_item(location, depth=1) - - lms_link = get_lms_link_for_item(location, course_id=course.location.course_id) - preview_link = get_lms_link_for_item(location, course_id=course.location.course_id, preview=True) - - # make sure that location references a 'sequential', otherwise return BadRequest - if item.location.category != 'sequential': - return HttpResponseBadRequest() - - parent_locs = modulestore().get_parent_locations(location, None) - - # we're for now assuming a single parent - if len(parent_locs) != 1: - logging.error('Multiple (or none) parents have been found for {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( - (field.name, field.read_from(item)) - for field - in item.fields - if field.name not in ['display_name', 'start', 'due', 'format'] and field.scope == Scope.settings - ) - - can_view_live = False - subsection_units = item.get_children() - for unit in subsection_units: - state = compute_unit_state(unit) - if state == UnitState.public or state == UnitState.draft: - can_view_live = True - break - - return render_to_response('edit_subsection.html', - {'subsection': item, - 'context_course': course, - '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 - """ - course = get_course_for_item(location) - if not has_access(request.user, course.location): - raise PermissionDenied() - - item = modulestore().get_item(location, depth=1) - - lms_link = get_lms_link_for_item(item.location, course_id=course.location.course_id) - - component_templates = defaultdict(list) - - # 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 - component_types = list(COMPONENT_TYPES) - if isinstance(course_advanced_keys, list): - course_advanced_keys = [c for c in course_advanced_keys if c in ADVANCED_COMPONENT_TYPES] - if len(course_advanced_keys) > 0: - component_types.append(ADVANCED_COMPONENT_CATEGORY) - else: - log.error("Improper format for course advanced keys! {0}".format(course_advanced_keys)) - - templates = modulestore().get_items(Location('i4x', 'edx', 'templates')) - for template in templates: - category = template.location.category - - if category in course_advanced_keys: - category = ADVANCED_COMPONENT_CATEGORY - - if category in component_types: - # This is a hack to create categories for different xmodules - component_templates[category].append(( - template.display_name_with_default, - template.location.url(), - hasattr(template, 'markdown') and template.markdown is not None, - template.cms.empty, - )) - - components = [ - component.location.url() - for component - in item.get_children() - ] - - # TODO (cpennington): If we share units between courses, - # this will need to change to check permissions correctly so as - # to pick the correct parent subsection - - containing_subsection_locs = modulestore().get_parent_locations(location, None) - containing_subsection = modulestore().get_item(containing_subsection_locs[0]) - - containing_section_locs = modulestore().get_parent_locations(containing_subsection.location, None) - containing_section = modulestore().get_item(containing_section_locs[0]) - - # cdodge hack. We're having trouble previewing drafts via jump_to redirect - # so let's generate the link url here - - # need to figure out where this item is in the list of children as the preview will need this - index = 1 - for child in containing_subsection.get_children(): - if child.location == item.location: - break - index = index + 1 - - preview_lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE', - 'preview.' + settings.LMS_BASE) - - preview_lms_link = '//{preview_lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'.format( - preview_lms_base=preview_lms_base, - lms_base=settings.LMS_BASE, - org=course.location.org, - course=course.location.course, - course_name=course.location.name, - section=containing_section.location.name, - subsection=containing_subsection.location.name, - index=index) - - unit_state = compute_unit_state(item) - - return render_to_response('unit.html', { - 'context_course': course, - '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_default_time_display(containing_subsection.lms.start) if containing_subsection.lms.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': item.cms.published_date.strftime('%B %d, %Y') if item.cms.published_date is not None else None, - }) - - -@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 . - - 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 - """ - - descriptor = modulestore().get_item(location) - instance = load_preview_module(request, preview_id, descriptor) - # 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 ProcessingError: - log.warning("Module raised an error while processing AJAX request", - exc_info=True) - return HttpResponseBadRequest() - - except: - log.exception("error processing ajax call") - raise - - return HttpResponse(ajax_return) - - -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) - - -class SessionKeyValueStore(KeyValueStore): - def __init__(self, request, model_data): - self._model_data = model_data - self._session = request.session - - def get(self, key): - try: - return self._model_data[key.field_name] - except (KeyError, InvalidScopeError): - return self._session[tuple(key)] - - def set(self, key, value): - try: - self._model_data[key.field_name] = value - except (KeyError, InvalidScopeError): - self._session[tuple(key)] = value - - def delete(self, key): - try: - del self._model_data[key.field_name] - except (KeyError, InvalidScopeError): - del self._session[tuple(key)] - - def has(self, key): - return key in self._model_data or key in self._session - - -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 - """ - - def preview_model_data(descriptor): - return DbModel( - SessionKeyValueStore(request, descriptor._model_data), - descriptor.module_class, - preview_id, - MongoUsage(preview_id, descriptor.location.url()), - ) - - 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, - xblock_model_data=preview_model_data, - ) - - -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 - """ - - return load_preview_module(request, preview_id, descriptor) - - -def load_preview_module(request, preview_id, descriptor): - """ - 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(system) - except: - log.debug("Unable to load preview module", exc_info=True) - module = ErrorDescriptor.from_descriptor( - descriptor, - error_msg=exc_info_to_str(sys.exc_info()) - ).xmodule(system) - - # 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, - getattr(module, 'data_dir', module.location.course), - course_namespace=Location([module.location.tag, module.location.org, module.location.course, None, None]) - ) - - 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) - 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) - - store = modulestore() - - item = store.get_item(item_location) - - if delete_children: - _xmodule_recurse(item, lambda i: store.delete_item(i.location, delete_all_versions)) - else: - store.delete_item(item.location, delete_all_versions) - - # cdodge: we need to remove our parent's pointer to us so that it is no longer dangling - if delete_all_versions: - parent_locs = modulestore('direct').get_parent_locations(item_loc, None) - - for parent_loc in parent_locs: - parent = modulestore('direct').get_item(parent_loc) - item_url = item_loc.url() - if item_url in parent.children: - children = parent.children - children.remove(item_url) - parent.children = children - modulestore('direct').update_children(parent.location, parent.children) - - 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, value in posted_metadata.items(): - - if 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._model_data: - del existing_item._model_data[metadata_key] - del posted_metadata[metadata_key] - else: - existing_item._model_data[metadata_key] = value - - # commit to datastore - # TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata - store.update_metadata(item_location, own_metadata(existing_item)) - - 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) - - # replace the display name with an optional parameter passed in from the caller - if display_name is not None: - new_item.display_name = display_name - - get_modulestore(template).update_metadata(new_item.location.url(), own_metadata(new_item)) - - if new_item.location.category not in DETACHED_CATEGORIES: - get_modulestore(parent.location).update_children(parent_location, parent.children + [new_item.location.url()]) - - return HttpResponse(json.dumps({'id': dest_location.url()})) - - -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 = get_location_and_verify_access(request, org, course, coursename) - - # Does the course actually exist?!? Get anything from it to prove its existance - - try: - 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_default_time_display(readback.last_modified_at.timetuple()), - '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 - - -@login_required -@ensure_csrf_cookie -def manage_users(request, location): - ''' - This view will return all CMS users who are editors for the specified course - ''' - # 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 - - -@expect_json -@login_required -@ensure_csrf_cookie -def add_user(request, location): - ''' - This POST-back view will add a user - specified by email - to the list of editors for - the specified course - ''' - 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() - - -@expect_json -@login_required -@ensure_csrf_cookie -def remove_user(request, location): - ''' - This POST-back view will remove a user - specified by email - from the list of editors for - the specified course - ''' - - 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 = get_location_and_verify_access(request, org, course, coursename) - - 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].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, own_metadata(course)) - 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) - - # 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 = get_location_and_verify_access(request, org, course, name) - - 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() - - real_method = get_request_method(request) - - if request.method == 'GET': - return HttpResponse(json.dumps(get_course_updates(location)), - mimetype="application/json") - elif real_method == 'DELETE': - try: - return HttpResponse(json.dumps(delete_course_update(location, - request.POST, provided_id)), mimetype="application/json") - except: - return HttpResponseBadRequest("Failed to delete", - content_type="text/plain") - 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", - 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() - - real_method = get_request_method(request) - - 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 = get_location_and_verify_access(request, org, course, name) - - course_module = modulestore().get_item(location) - - 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"}) - }) - - -@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)), - }) - - -@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 - """ - 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 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 = get_location_and_verify_access(request, org, course, name) - - real_method = get_request_method(request) - - if real_method == 'GET': - # Cannot just do a get w/o knowing the course name :-( - return HttpResponse(json.dumps(CourseGradingModel.fetch_grader(Location(location), grader_index)), - mimetype="application/json") - elif real_method == "DELETE": - # ??? Should this return anything? Perhaps success fail? - CourseGradingModel.delete_grader(Location(location), grader_index) - return HttpResponse() - elif request.method == 'POST': # post or put, doesn't matter. - return HttpResponse(json.dumps(CourseGradingModel.update_grader_from_json(Location(location), request.POST)), - mimetype="application/json") - - -# # NB: expect_json failed on ["key", "key2"] and json payload -@login_required -@ensure_csrf_cookie -def course_advanced_updates(request, org, course, name): - """ - 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 - """ - location = get_location_and_verify_access(request, org, course, name) - - real_method = get_request_method(request) - - if real_method == 'GET': - return HttpResponse(json.dumps(CourseMetadata.fetch(location)), mimetype="application/json") - elif real_method == 'DELETE': - return HttpResponse(json.dumps(CourseMetadata.delete_key(location, json.loads(request.body))), - mimetype="application/json") - elif real_method == 'POST' or real_method == 'PUT': - # NOTE: request.POST is messed up because expect_json cloned_request.POST.copy() is creating a defective entry w/ the whole payload as the key - request_body = json.loads(request.body) - #Whether or not to filter the tabs key out of the settings metadata - filter_tabs = True - #Check to see if the user instantiated any advanced components. This is a hack to add the open ended panel tab - #to a course automatically if the user has indicated that they want to edit the combinedopenended or peergrading - #module, and to remove it if they have removed the open ended elements. - if ADVANCED_COMPONENT_POLICY_KEY in request_body: - #Check to see if the user instantiated any open ended components - found_oe_type = False - #Get the course so that we can scrape current tabs - course_module = modulestore().get_item(location) - for oe_type in OPEN_ENDED_COMPONENT_TYPES: - if oe_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]: - #Add an open ended tab to the course if needed - changed, new_tabs = add_open_ended_panel_tab(course_module) - #If a tab has been added to the course, then send the metadata along to CourseMetadata.update_from_json - if changed: - request_body.update({'tabs': new_tabs}) - #Indicate that tabs should not be filtered out of the metadata - filter_tabs = False - #Set this flag to avoid the open ended tab removal code below. - found_oe_type = True - break - #If we did not find an open ended module type in the advanced settings, - # we may need to remove the open ended tab from the course. - if not found_oe_type: - #Remove open ended tab to the course if needed - changed, new_tabs = remove_open_ended_panel_tab(course_module) - if changed: - request_body.update({'tabs': new_tabs}) - #Indicate that tabs should not be filtered out of the metadata - filter_tabs = False - response_json = json.dumps(CourseMetadata.update_from_json(location, request_body, filter_tabs=filter_tabs)) - return HttpResponse(response_json, mimetype="application/json") - - -@ensure_csrf_cookie -@login_required -def get_checklists(request, org, course, name): - """ - Send models, views, and html for displaying the course checklists. - - org, course, name: Attributes of the Location for the item to edit - """ - location = get_location_and_verify_access(request, org, course, name) - - modulestore = get_modulestore(location) - course_module = modulestore.get_item(location) - new_course_template = Location('i4x', 'edx', 'templates', 'course', 'Empty') - template_module = modulestore.get_item(new_course_template) - - # If course was created before checklists were introduced, copy them over from the template. - copied = False - if not course_module.checklists: - course_module.checklists = template_module.checklists - copied = True - - checklists, modified = expand_checklist_action_urls(course_module) - if copied or modified: - modulestore.update_metadata(location, own_metadata(course_module)) - return render_to_response('checklists.html', - { - 'context_course': course_module, - 'checklists': checklists - }) - - -@ensure_csrf_cookie -@login_required -def update_checklist(request, org, course, name, checklist_index=None): - """ - restful CRUD operations on course checklists. The payload is a json rep of - the modified checklist. For PUT or POST requests, the index of the - checklist being modified must be included; the returned payload will - be just that one checklist. For GET requests, the returned payload - is a json representation of the list of all checklists. - - org, course, name: Attributes of the Location for the item to edit - """ - location = get_location_and_verify_access(request, org, course, name) - modulestore = get_modulestore(location) - course_module = modulestore.get_item(location) - - real_method = get_request_method(request) - if real_method == 'POST' or real_method == 'PUT': - if checklist_index is not None and 0 <= int(checklist_index) < len(course_module.checklists): - index = int(checklist_index) - course_module.checklists[index] = json.loads(request.body) - checklists, modified = expand_checklist_action_urls(course_module) - modulestore.update_metadata(location, own_metadata(course_module)) - return HttpResponse(json.dumps(checklists[index]), mimetype="application/json") - else: - return HttpResponseBadRequest( - "Could not save checklist state because the checklist index was out of range or unspecified.", - content_type="text/plain") - elif request.method == 'GET': - # In the JavaScript view initialize method, we do a fetch to get all the checklists. - checklists, modified = expand_checklist_action_urls(course_module) - if modified: - modulestore.update_metadata(location, own_metadata(course_module)) - return HttpResponse(json.dumps(checklists), mimetype="application/json") - else: - return HttpResponseBadRequest("Unsupported request.", content_type="text/plain") - - -def expand_checklist_action_urls(course_module): - """ - Gets the checklists out of the course module and expands their action urls - if they have not yet been expanded. - - Returns the checklists with modified urls, as well as a boolean - indicating whether or not the checklists were modified. - """ - checklists = course_module.checklists - modified = False - for checklist in checklists: - if not checklist.get('action_urls_expanded', False): - for item in checklist.get('items'): - item['action_url'] = get_url_reverse(item.get('action_url'), course_module) - checklist['action_urls_expanded'] = True - modified = True - - return checklists, modified - - -@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 = get_location_and_verify_access(request, org, course, name) - - 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) - - asset_display = [] - for asset in assets: - id = asset['_id'] - display_info = {} - display_info['displayname'] = asset['displayname'] - display_info['uploadDate'] = get_default_time_display(asset['uploadDate'].timetuple()) - - 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) - - # clone a default 'about' module as well - - about_template_location = Location(['i4x', 'edx', 'templates', 'about', 'overview']) - dest_about_location = dest_location._replace(category='about', name='overview') - modulestore('direct').clone_item(about_template_location, dest_about_location) - - if display_name is not None: - new_course.display_name = display_name - - # set a default start date to now - new_course.start = 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(), own_metadata(course)) - - -@ensure_csrf_cookie -@login_required -def import_course(request, org, course, name): - - location = get_location_and_verify_access(request, org, course, name) - - 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), - draft_store=modulestore()) - - # 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': get_url_reverse('CourseOutline', course_module) - }) - - -@ensure_csrf_cookie -@login_required -def generate_export_course(request, org, course, name): - location = get_location_and_verify_access(request, org, course, name) - - 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, modulestore()) - #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 = get_location_and_verify_access(request, org, course, name) - - course_module = modulestore().get_item(location) - - 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) - - -def render_404(request): - return HttpResponseNotFound(render_to_string('404.html', {})) - - -def render_500(request): - return HttpResponseServerError(render_to_string('500.html', {})) - - -def get_location_and_verify_access(request, org, course, name): - """ - Create the location tuple verify that the user has permissions - to view the location. Returns the location. - """ - 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() - - return location - - -def get_request_method(request): - """ - Using HTTP_X_HTTP_METHOD_OVERRIDE, in the request metadata, determine - what type of request came from the client, and return it. - """ - # 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 - - return real_method diff --git a/cms/djangoapps/contentstore/views/__init__.py b/cms/djangoapps/contentstore/views/__init__.py new file mode 100644 index 0000000000..0b7c271b1e --- /dev/null +++ b/cms/djangoapps/contentstore/views/__init__.py @@ -0,0 +1,17 @@ +from new import * +from error import * +from course import * +from item import * +from public import * +from user import * +from preview import * +from assets import * +from checklist import * +from requests import landing + + +""" + +from main import * + +""" diff --git a/cms/djangoapps/contentstore/views/access.py b/cms/djangoapps/contentstore/views/access.py new file mode 100644 index 0000000000..dd3add1099 --- /dev/null +++ b/cms/djangoapps/contentstore/views/access.py @@ -0,0 +1,39 @@ +#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 auth.authz import STAFF_ROLE_NAME, INSTRUCTOR_ROLE_NAME +from auth.authz import is_user_in_course_group_role +from contentstore.utils import get_course_location_for_item + + +def get_location_and_verify_access(request, org, course, name): + """ + Create the location tuple verify that the user has permissions + to view the location. Returns the location. + """ + 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() + + return location + + +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 diff --git a/cms/djangoapps/contentstore/views/assets.py b/cms/djangoapps/contentstore/views/assets.py new file mode 100644 index 0000000000..616b04342d --- /dev/null +++ b/cms/djangoapps/contentstore/views/assets.py @@ -0,0 +1,118 @@ +from django.http import HttpResponse, HttpResponseBadRequest +from django.contrib.auth.decorators import login_required +from django_future.csrf import ensure_csrf_cookie + +from xmodule.contentstore.content import StaticContent +from access import get_location_and_verify_access +from xmodule.util.date_utils import get_default_time_display +from mitxmako.shortcuts import render_to_response + + +@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 = get_location_and_verify_access(request, org, course, name) + + 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) + + asset_display = [] + for asset in assets: + id = asset['_id'] + display_info = {} + display_info['displayname'] = asset['displayname'] + display_info['uploadDate'] = get_default_time_display(asset['uploadDate'].timetuple()) + + 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 + }) + + +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 = get_location_and_verify_access(request, org, course, coursename) + + # Does the course actually exist?!? Get anything from it to prove its existance + + try: + 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_default_time_display(readback.last_modified_at.timetuple()), + '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 + diff --git a/cms/djangoapps/contentstore/views/checklist.py b/cms/djangoapps/contentstore/views/checklist.py new file mode 100644 index 0000000000..376e041523 --- /dev/null +++ b/cms/djangoapps/contentstore/views/checklist.py @@ -0,0 +1,98 @@ +from django.http import HttpResponse, HttpResponseBadRequest +from django.contrib.auth.decorators import login_required +from django_future.csrf import ensure_csrf_cookie +from access import get_location_and_verify_access +from mitxmako.shortcuts import render_to_response +from contentstore.utils import get_modulestore, get_url_reverse + +@ensure_csrf_cookie +@login_required +def get_checklists(request, org, course, name): + """ + Send models, views, and html for displaying the course checklists. + + org, course, name: Attributes of the Location for the item to edit + """ + location = get_location_and_verify_access(request, org, course, name) + + modulestore = get_modulestore(location) + course_module = modulestore.get_item(location) + new_course_template = Location('i4x', 'edx', 'templates', 'course', 'Empty') + template_module = modulestore.get_item(new_course_template) + + # If course was created before checklists were introduced, copy them over from the template. + copied = False + if not course_module.checklists: + course_module.checklists = template_module.checklists + copied = True + + checklists, modified = expand_checklist_action_urls(course_module) + if copied or modified: + modulestore.update_metadata(location, own_metadata(course_module)) + return render_to_response('checklists.html', + { + 'context_course': course_module, + 'checklists': checklists + }) + + +@ensure_csrf_cookie +@login_required +def update_checklist(request, org, course, name, checklist_index=None): + """ + restful CRUD operations on course checklists. The payload is a json rep of + the modified checklist. For PUT or POST requests, the index of the + checklist being modified must be included; the returned payload will + be just that one checklist. For GET requests, the returned payload + is a json representation of the list of all checklists. + + org, course, name: Attributes of the Location for the item to edit + """ + location = get_location_and_verify_access(request, org, course, name) + modulestore = get_modulestore(location) + course_module = modulestore.get_item(location) + + real_method = get_request_method(request) + if real_method == 'POST' or real_method == 'PUT': + if checklist_index is not None and 0 <= int(checklist_index) < len(course_module.checklists): + index = int(checklist_index) + course_module.checklists[index] = json.loads(request.body) + checklists, modified = expand_checklist_action_urls(course_module) + modulestore.update_metadata(location, own_metadata(course_module)) + return HttpResponse(json.dumps(checklists[index]), mimetype="application/json") + else: + return HttpResponseBadRequest( + "Could not save checklist state because the checklist index was out of range or unspecified.", + content_type="text/plain") + elif request.method == 'GET': + # In the JavaScript view initialize method, we do a fetch to get all the checklists. + checklists, modified = expand_checklist_action_urls(course_module) + if modified: + modulestore.update_metadata(location, own_metadata(course_module)) + return HttpResponse(json.dumps(checklists), mimetype="application/json") + else: + return HttpResponseBadRequest("Unsupported request.", content_type="text/plain") + + +def expand_checklist_action_urls(course_module): + """ + Gets the checklists out of the course module and expands their action urls + if they have not yet been expanded. + + Returns the checklists with modified urls, as well as a boolean + indicating whether or not the checklists were modified. + """ + checklists = course_module.checklists + modified = False + for checklist in checklists: + if not checklist.get('action_urls_expanded', False): + for item in checklist.get('items'): + item['action_url'] = get_url_reverse(item.get('action_url'), course_module) + checklist['action_urls_expanded'] = True + modified = True + + return checklists, modified + + + + diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py new file mode 100644 index 0000000000..fc2214f970 --- /dev/null +++ b/cms/djangoapps/contentstore/views/course.py @@ -0,0 +1,202 @@ +from django.contrib.auth.decorators import login_required +from django_future.csrf import ensure_csrf_cookie + +from util.json_request import expect_json +from mitxmako.shortcuts import render_to_response + +@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) + + # clone a default 'about' module as well + + about_template_location = Location(['i4x', 'edx', 'templates', 'about', 'overview']) + dest_about_location = dest_location._replace(category='about', name='overview') + modulestore('direct').clone_item(about_template_location, dest_about_location) + + if display_name is not None: + new_course.display_name = display_name + + # set a default start date to now + new_course.start = 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(), own_metadata(course)) + + +@ensure_csrf_cookie +@login_required +def import_course(request, org, course, name): + + location = get_location_and_verify_access(request, org, course, name) + + 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), + draft_store=modulestore()) + + # 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': get_url_reverse('CourseOutline', course_module) + }) + + +@ensure_csrf_cookie +@login_required +def generate_export_course(request, org, course, name): + location = get_location_and_verify_access(request, org, course, name) + + 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, modulestore()) + #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 = get_location_and_verify_access(request, org, course, name) + + course_module = modulestore().get_item(location) + + return render_to_response('export.html', { + 'context_course': course_module, + 'active_tab': 'export', + 'successful_import_redirect_url': '' + }) + diff --git a/cms/djangoapps/contentstore/views/error.py b/cms/djangoapps/contentstore/views/error.py new file mode 100644 index 0000000000..527f137b9e --- /dev/null +++ b/cms/djangoapps/contentstore/views/error.py @@ -0,0 +1,21 @@ +from django.http import HttpResponse, HttpResponseServerError, HttpResponseNotFound + +from mitxmako.shortcuts import render_to_string, render_to_response + + +def not_found(request): + return render_to_response('error.html', {'error': '404'}) + + +def server_error(request): + return render_to_response('error.html', {'error': '500'}) + + +def render_404(request): + return HttpResponseNotFound(render_to_string('404.html', {})) + + +def render_500(request): + return HttpResponseServerError(render_to_string('500.html', {})) + + diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py new file mode 100644 index 0000000000..876251203e --- /dev/null +++ b/cms/djangoapps/contentstore/views/item.py @@ -0,0 +1,123 @@ +from django.http import HttpResponse +from django.contrib.auth.decorators import login_required +from util.json_request import expect_json +from mitxmako.shortcuts import render_to_response + + +@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, value in posted_metadata.items(): + + if 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._model_data: + del existing_item._model_data[metadata_key] + del posted_metadata[metadata_key] + else: + existing_item._model_data[metadata_key] = value + + # commit to datastore + # TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata + store.update_metadata(item_location, own_metadata(existing_item)) + + 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) + + # replace the display name with an optional parameter passed in from the caller + if display_name is not None: + new_item.display_name = display_name + + get_modulestore(template).update_metadata(new_item.location.url(), own_metadata(new_item)) + + if new_item.location.category not in DETACHED_CATEGORIES: + get_modulestore(parent.location).update_children(parent_location, parent.children + [new_item.location.url()]) + + return HttpResponse(json.dumps({'id': dest_location.url()})) + + +@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) + + store = modulestore() + + item = store.get_item(item_location) + + if delete_children: + _xmodule_recurse(item, lambda i: store.delete_item(i.location, delete_all_versions)) + else: + store.delete_item(item.location, delete_all_versions) + + # cdodge: we need to remove our parent's pointer to us so that it is no longer dangling + if delete_all_versions: + parent_locs = modulestore('direct').get_parent_locations(item_loc, None) + + for parent_loc in parent_locs: + parent = modulestore('direct').get_item(parent_loc) + item_url = item_loc.url() + if item_url in parent.children: + children = parent.children + children.remove(item_url) + parent.children = children + modulestore('direct').update_children(parent.location, parent.children) + + return HttpResponse() + diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py new file mode 100644 index 0000000000..f473b962c5 --- /dev/null +++ b/cms/djangoapps/contentstore/views/preview.py @@ -0,0 +1,156 @@ +from django.contrib.auth.decorators import login_required +from xblock.runtime import DbModel +from xmodule.x_module import ModuleSystem +from xmodule.modulestore.mongo import MongoUsage +from xmodule_modifiers import wrap_xmodule +from session_kv_store import SessionKeyValueStore +from requests import render_from_lms +from functools import partial + +@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 + """ + + descriptor = modulestore().get_item(location) + instance = load_preview_module(request, preview_id, descriptor) + # 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 ProcessingError: + log.warning("Module raised an error while processing AJAX request", + exc_info=True) + return HttpResponseBadRequest() + + except: + log.exception("error processing ajax call") + raise + + return HttpResponse(ajax_return) + +@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')(), + }) + + + +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 + """ + + def preview_model_data(descriptor): + return DbModel( + SessionKeyValueStore(request, descriptor._model_data), + descriptor.module_class, + preview_id, + MongoUsage(preview_id, descriptor.location.url()), + ) + + 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, + xblock_model_data=preview_model_data, + ) + +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 + """ + + return load_preview_module(request, preview_id, descriptor) + + +def load_preview_module(request, preview_id, descriptor): + """ + 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(system) + except: + log.debug("Unable to load preview module", exc_info=True) + module = ErrorDescriptor.from_descriptor( + descriptor, + error_msg=exc_info_to_str(sys.exc_info()) + ).xmodule(system) + + # 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, + getattr(module, 'data_dir', module.location.course), + course_namespace=Location([module.location.tag, module.location.org, module.location.course, None, None]) + ) + + 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) + preview_html.append(module.get_html()) + return preview_html + diff --git a/cms/djangoapps/contentstore/views/public.py b/cms/djangoapps/contentstore/views/public.py new file mode 100644 index 0000000000..7c207b7893 --- /dev/null +++ b/cms/djangoapps/contentstore/views/public.py @@ -0,0 +1,50 @@ +from external_auth.views import ssl_login_shortcut +from mitxmako.shortcuts import render_to_response +from django_future.csrf import ensure_csrf_cookie +from requests import index + +""" +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}) + + +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): + """ + 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), + }) + + +def howitworks(request): + if request.user.is_authenticated(): + return index(request) + else: + return render_to_response('howitworks.html', {}) + +def ux_alerts(request): + """ + static/proof-of-concept views + """ + return render_to_response('ux-alerts.html', {}) + diff --git a/cms/djangoapps/contentstore/views/requests.py b/cms/djangoapps/contentstore/views/requests.py new file mode 100644 index 0000000000..131068768a --- /dev/null +++ b/cms/djangoapps/contentstore/views/requests.py @@ -0,0 +1,91 @@ +from django.contrib.auth.decorators import login_required +from django_future.csrf import ensure_csrf_cookie +from mitxmako.shortcuts import render_to_response +from xmodule.modulestore import Location +from xmodule.modulestore.django import modulestore +from access import has_access +from contentstore.utils import get_url_reverse, get_lms_link_for_item +from django.conf import settings + +@login_required +@ensure_csrf_cookie +def index(request): + """ + List all courses available to the logged in user + """ + courses = modulestore('direct').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.display_name, + get_url_reverse('CourseOutline', course), + get_lms_link_for_item(course.location, course_id=course.location.course_id)) + 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================================ + + +# 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', {}) + +# points to the temporary edge page +def edge(request): + return render_to_response('university_profiles/edge.html', {}) + + +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) + + +def get_request_method(request): + """ + Using HTTP_X_HTTP_METHOD_OVERRIDE, in the request metadata, determine + what type of request came from the client, and return it. + """ + # 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 + + return real_method + +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 + +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 _xmodule_recurse(item, action): + for child in item.get_children(): + _xmodule_recurse(child, action) + + action(item) + diff --git a/cms/djangoapps/contentstore/views/session_kv_store.py b/cms/djangoapps/contentstore/views/session_kv_store.py new file mode 100644 index 0000000000..2f6868ee81 --- /dev/null +++ b/cms/djangoapps/contentstore/views/session_kv_store.py @@ -0,0 +1,27 @@ +from xblock.runtime import KeyValueStore + +class SessionKeyValueStore(KeyValueStore): + def __init__(self, request, model_data): + self._model_data = model_data + self._session = request.session + + def get(self, key): + try: + return self._model_data[key.field_name] + except (KeyError, InvalidScopeError): + return self._session[tuple(key)] + + def set(self, key, value): + try: + self._model_data[key.field_name] = value + except (KeyError, InvalidScopeError): + self._session[tuple(key)] = value + + def delete(self, key): + try: + del self._model_data[key.field_name] + except (KeyError, InvalidScopeError): + del self._session[tuple(key)] + + def has(self, key): + return key in self._model_data or key in self._session diff --git a/cms/djangoapps/contentstore/views/user.py b/cms/djangoapps/contentstore/views/user.py new file mode 100644 index 0000000000..5be78a0c37 --- /dev/null +++ b/cms/djangoapps/contentstore/views/user.py @@ -0,0 +1,107 @@ +from django.contrib.auth.decorators import login_required +from django_future.csrf import ensure_csrf_cookie + +from util.json_request import expect_json +from mitxmako.shortcuts import render_to_response + +def user_author_string(user): + '''Get an author string for commits by this user. Format: + first last . + + 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 +@ensure_csrf_cookie +def manage_users(request, location): + ''' + This view will return all CMS users who are editors for the specified course + ''' + # 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 + }) + + + + +@expect_json +@login_required +@ensure_csrf_cookie +def add_user(request, location): + ''' + This POST-back view will add a user - specified by email - to the list of editors for + the specified course + ''' + 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() + + +@expect_json +@login_required +@ensure_csrf_cookie +def remove_user(request, location): + ''' + This POST-back view will remove a user - specified by email - from the list of editors for + the specified course + ''' + + 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() + From 9350a2c0674eee804ba7aae96c6d358a5156b291 Mon Sep 17 00:00:00 2001 From: Steve Strassmann Date: Fri, 10 May 2013 13:56:30 -0400 Subject: [PATCH 02/10] refactoring views --- cms/djangoapps/contentstore/views/__init__.py | 28 +- cms/djangoapps/contentstore/views/access.py | 6 +- cms/djangoapps/contentstore/views/assets.py | 142 +++++- .../contentstore/views/checklist.py | 9 +- .../contentstore/views/component.py | 313 +++++++++++++ cms/djangoapps/contentstore/views/course.py | 420 ++++++++++++------ cms/djangoapps/contentstore/views/error.py | 2 +- cms/djangoapps/contentstore/views/item.py | 16 +- cms/djangoapps/contentstore/views/preview.py | 15 + cms/djangoapps/contentstore/views/public.py | 6 +- cms/djangoapps/contentstore/views/requests.py | 41 +- .../contentstore/views/session_kv_store.py | 2 +- cms/djangoapps/contentstore/views/tabs.py | 110 +++++ cms/djangoapps/contentstore/views/user.py | 43 +- 14 files changed, 956 insertions(+), 197 deletions(-) create mode 100644 cms/djangoapps/contentstore/views/component.py create mode 100644 cms/djangoapps/contentstore/views/tabs.py diff --git a/cms/djangoapps/contentstore/views/__init__.py b/cms/djangoapps/contentstore/views/__init__.py index 0b7c271b1e..37f786ac3c 100644 --- a/cms/djangoapps/contentstore/views/__init__.py +++ b/cms/djangoapps/contentstore/views/__init__.py @@ -1,17 +1,13 @@ -from new import * -from error import * +# TODO: replace asterisks, should explicitly enumerate imports instead + +from assets import asset_index, upload_asset, import_course, generate_export_course, export_course +from checklist import get_checklists, update_checklist +from component import * from course import * -from item import * -from public import * -from user import * -from preview import * -from assets import * -from checklist import * -from requests import landing - - -""" - -from main import * - -""" +from error import not_found, server_error, render_404, render_500 +from item import save_item, clone_item, delete_item +from preview import preview_dispatch, preview_component +from public import signup, old_login_redirect, login_page, howitworks, ux_alerts +from user import index, add_user, remove_user, manage_users +from tabs import edit_tabs, reorder_static_tabs +from requests import edge, event, landing diff --git a/cms/djangoapps/contentstore/views/access.py b/cms/djangoapps/contentstore/views/access.py index dd3add1099..37f6fcb767 100644 --- a/cms/djangoapps/contentstore/views/access.py +++ b/cms/djangoapps/contentstore/views/access.py @@ -1,11 +1,7 @@ -#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 auth.authz import STAFF_ROLE_NAME, INSTRUCTOR_ROLE_NAME from auth.authz import is_user_in_course_group_role from contentstore.utils import get_course_location_for_item - +from django.core.exceptions import PermissionDenied def get_location_and_verify_access(request, org, course, name): """ diff --git a/cms/djangoapps/contentstore/views/assets.py b/cms/djangoapps/contentstore/views/assets.py index 616b04342d..c2aa52b1e1 100644 --- a/cms/djangoapps/contentstore/views/assets.py +++ b/cms/djangoapps/contentstore/views/assets.py @@ -1,12 +1,29 @@ +import logging, json, os, tarfile, shutil +from tempfile import mkdtemp +from path import path + +from django.conf import settings from django.http import HttpResponse, HttpResponseBadRequest 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 xmodule.contentstore.content import StaticContent -from access import get_location_and_verify_access -from xmodule.util.date_utils import get_default_time_display from mitxmako.shortcuts import render_to_response +from cache_toolbox.core import del_cached_content +from contentstore.utils import get_url_reverse +from xmodule.modulestore.xml_importer import import_from_xml +from xmodule.contentstore.django import contentstore +from xmodule.modulestore.xml_exporter import export_to_xml +from xmodule.modulestore.django import modulestore +from xmodule.modulestore import Location +from xmodule.contentstore.content import StaticContent +from xmodule.util.date_utils import get_default_time_display + +from access import get_location_and_verify_access +from auth.authz import create_all_course_groups @login_required @ensure_csrf_cookie @@ -116,3 +133,122 @@ def upload_asset(request, org, course, coursename): response['asset_url'] = StaticContent.get_url_path_from_location(content.location) return response +@ensure_csrf_cookie +@login_required +def import_course(request, org, course, name): + + location = get_location_and_verify_access(request, org, course, name) + + 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), + draft_store=modulestore()) + + # 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': get_url_reverse('CourseOutline', course_module) + }) + + +@ensure_csrf_cookie +@login_required +def generate_export_course(request, org, course, name): + location = get_location_and_verify_access(request, org, course, name) + + 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, modulestore()) + #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 = get_location_and_verify_access(request, org, course, name) + + course_module = modulestore().get_item(location) + + return render_to_response('export.html', { + 'context_course': course_module, + 'active_tab': 'export', + 'successful_import_redirect_url': '' + }) + diff --git a/cms/djangoapps/contentstore/views/checklist.py b/cms/djangoapps/contentstore/views/checklist.py index 376e041523..4a97ddc1df 100644 --- a/cms/djangoapps/contentstore/views/checklist.py +++ b/cms/djangoapps/contentstore/views/checklist.py @@ -1,9 +1,16 @@ +import json + from django.http import HttpResponse, HttpResponseBadRequest from django.contrib.auth.decorators import login_required from django_future.csrf import ensure_csrf_cookie -from access import get_location_and_verify_access from mitxmako.shortcuts import render_to_response + +from xmodule.modulestore import Location +from xmodule.modulestore.inheritance import own_metadata + from contentstore.utils import get_modulestore, get_url_reverse +from requests import get_request_method +from access import get_location_and_verify_access @ensure_csrf_cookie @login_required diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py new file mode 100644 index 0000000000..2dd7307976 --- /dev/null +++ b/cms/djangoapps/contentstore/views/component.py @@ -0,0 +1,313 @@ +import json, logging +from collections import defaultdict + +from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden +from django.contrib.auth.decorators import login_required +from django.core.exceptions import PermissionDenied +from django_future.csrf import ensure_csrf_cookie +from django.conf import settings + +from mitxmako.shortcuts import render_to_response + +from xmodule.modulestore import Location +from xmodule.modulestore.django import modulestore +from xmodule.util.date_utils import get_default_time_display + +from xblock.core import Scope +from util.json_request import expect_json + +from contentstore.module_info_model import get_module_info, set_module_info +from contentstore.utils import get_modulestore, get_lms_link_for_item, \ + compute_unit_state, UnitState, get_course_for_item + +from models.settings.course_grading import CourseGradingModel + +from requests import get_request_method, _xmodule_recurse +from access import has_access, get_location_and_verify_access + +# 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'] + +OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"] +ADVANCED_COMPONENT_TYPES = ['annotatable'] + OPEN_ENDED_COMPONENT_TYPES +ADVANCED_COMPONENT_CATEGORY = 'advanced' +ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules' + + +@login_required +def edit_subsection(request, location): + # check that we have permissions to edit this item + course = get_course_for_item(location) + if not has_access(request.user, course.location): + raise PermissionDenied() + + item = modulestore().get_item(location, depth=1) + + lms_link = get_lms_link_for_item(location, course_id=course.location.course_id) + preview_link = get_lms_link_for_item(location, course_id=course.location.course_id, preview=True) + + # make sure that location references a 'sequential', otherwise return BadRequest + if item.location.category != 'sequential': + return HttpResponseBadRequest() + + parent_locs = modulestore().get_parent_locations(location, None) + + # we're for now assuming a single parent + if len(parent_locs) != 1: + logging.error('Multiple (or none) parents have been found for {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( + (field.name, field.read_from(item)) + for field + in item.fields + if field.name not in ['display_name', 'start', 'due', 'format'] and field.scope == Scope.settings + ) + + can_view_live = False + subsection_units = item.get_children() + for unit in subsection_units: + state = compute_unit_state(unit) + if state == UnitState.public or state == UnitState.draft: + can_view_live = True + break + + return render_to_response('edit_subsection.html', + {'subsection': item, + 'context_course': course, + '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 + """ + course = get_course_for_item(location) + if not has_access(request.user, course.location): + raise PermissionDenied() + + item = modulestore().get_item(location, depth=1) + + lms_link = get_lms_link_for_item(item.location, course_id=course.location.course_id) + + component_templates = defaultdict(list) + + # 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 + component_types = list(COMPONENT_TYPES) + if isinstance(course_advanced_keys, list): + course_advanced_keys = [c for c in course_advanced_keys if c in ADVANCED_COMPONENT_TYPES] + if len(course_advanced_keys) > 0: + component_types.append(ADVANCED_COMPONENT_CATEGORY) + else: + log.error("Improper format for course advanced keys! {0}".format(course_advanced_keys)) + + templates = modulestore().get_items(Location('i4x', 'edx', 'templates')) + for template in templates: + category = template.location.category + + if category in course_advanced_keys: + category = ADVANCED_COMPONENT_CATEGORY + + if category in component_types: + # This is a hack to create categories for different xmodules + component_templates[category].append(( + template.display_name_with_default, + template.location.url(), + hasattr(template, 'markdown') and template.markdown is not None, + template.cms.empty, + )) + + components = [ + component.location.url() + for component + in item.get_children() + ] + + # TODO (cpennington): If we share units between courses, + # this will need to change to check permissions correctly so as + # to pick the correct parent subsection + + containing_subsection_locs = modulestore().get_parent_locations(location, None) + containing_subsection = modulestore().get_item(containing_subsection_locs[0]) + + containing_section_locs = modulestore().get_parent_locations(containing_subsection.location, None) + containing_section = modulestore().get_item(containing_section_locs[0]) + + # cdodge hack. We're having trouble previewing drafts via jump_to redirect + # so let's generate the link url here + + # need to figure out where this item is in the list of children as the preview will need this + index = 1 + for child in containing_subsection.get_children(): + if child.location == item.location: + break + index = index + 1 + + preview_lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE', + 'preview.' + settings.LMS_BASE) + + preview_lms_link = '//{preview_lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'.format( + preview_lms_base=preview_lms_base, + lms_base=settings.LMS_BASE, + org=course.location.org, + course=course.location.course, + course_name=course.location.name, + section=containing_section.location.name, + subsection=containing_subsection.location.name, + index=index) + + unit_state = compute_unit_state(item) + + return render_to_response('unit.html', { + 'context_course': course, + '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_default_time_display(containing_subsection.lms.start) if containing_subsection.lms.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': item.cms.published_date.strftime('%B %d, %Y') if item.cms.published_date is not None else None, + }) + + +@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") + + +@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 +@ensure_csrf_cookie +def static_pages(request, org, course, coursename): + + location = get_location_and_verify_access(request, org, course, coursename) + + 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', {}) + +@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() + + real_method = get_request_method(request) + + 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() + + diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index fc2214f970..c0f6acc808 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -1,9 +1,65 @@ +import json, time + from django.contrib.auth.decorators import login_required from django_future.csrf import ensure_csrf_cookie - -from util.json_request import expect_json +from django.conf import settings +from django.core.exceptions import PermissionDenied +from django.http import HttpResponse, HttpResponseBadRequest +from django.core.urlresolvers import reverse from mitxmako.shortcuts import render_to_response +from xmodule.modulestore.django import modulestore +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.utils import get_lms_link_for_item, add_open_ended_panel_tab, remove_open_ended_panel_tab +from models.settings.course_details import CourseDetails, CourseSettingsEncoder +from models.settings.course_grading import CourseGradingModel +from models.settings.course_metadata import CourseMetadata +from component import OPEN_ENDED_COMPONENT_TYPES, ADVANCED_COMPONENT_POLICY_KEY +from auth.authz import create_all_course_groups +from util.json_request import expect_json +from access import has_access, get_location_and_verify_access +from requests import get_request_method +from tabs import initialize_course_tabs + + +@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 = get_location_and_verify_access(request, org, course, name) + + lms_link = get_lms_link_for_item(location) + + upload_asset_callback_url = reverse('upload_asset', kwargs={ + 'org': org, + 'course': course, + 'coursename': name + }) + + course = modulestore().get_item(location, depth=3) + sections = course.get_children() + + return render_to_response('overview.html', { + 'active_tab': 'courseware', + 'context_course': course, + 'lms_link': lms_link, + '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 @expect_json def create_new_course(request): @@ -63,140 +119,246 @@ def create_new_course(request): 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(), own_metadata(course)) - - -@ensure_csrf_cookie @login_required -def import_course(request, org, course, name): - - location = get_location_and_verify_access(request, org, course, name) - - 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), - draft_store=modulestore()) - - # 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': get_url_reverse('CourseOutline', course_module) - }) - - @ensure_csrf_cookie -@login_required -def generate_export_course(request, org, course, name): - location = get_location_and_verify_access(request, org, course, name) - - 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, modulestore()) - #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): +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 = get_location_and_verify_access(request, org, course, name) course_module = modulestore().get_item(location) - return render_to_response('export.html', { + # 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, - 'active_tab': 'export', - 'successful_import_redirect_url': '' + '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() + + real_method = get_request_method(request) + + if request.method == 'GET': + return HttpResponse(json.dumps(get_course_updates(location)), + mimetype="application/json") + elif real_method == 'DELETE': + try: + return HttpResponse(json.dumps(delete_course_update(location, + request.POST, provided_id)), mimetype="application/json") + except: + return HttpResponseBadRequest("Failed to delete", + content_type="text/plain") + 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", + content_type="text/plain") + +@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) + + 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"}) + }) + + +@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)), + }) + + +@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 + """ + 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 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 = get_location_and_verify_access(request, org, course, name) + + real_method = get_request_method(request) + + if real_method == 'GET': + # Cannot just do a get w/o knowing the course name :-( + return HttpResponse(json.dumps(CourseGradingModel.fetch_grader(Location(location), grader_index)), + mimetype="application/json") + elif real_method == "DELETE": + # ??? Should this return anything? Perhaps success fail? + CourseGradingModel.delete_grader(Location(location), grader_index) + return HttpResponse() + elif request.method == 'POST': # post or put, doesn't matter. + return HttpResponse(json.dumps(CourseGradingModel.update_grader_from_json(Location(location), request.POST)), + mimetype="application/json") + + +# # NB: expect_json failed on ["key", "key2"] and json payload +@login_required +@ensure_csrf_cookie +def course_advanced_updates(request, org, course, name): + """ + 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 + """ + location = get_location_and_verify_access(request, org, course, name) + + real_method = get_request_method(request) + + if real_method == 'GET': + return HttpResponse(json.dumps(CourseMetadata.fetch(location)), mimetype="application/json") + elif real_method == 'DELETE': + return HttpResponse(json.dumps(CourseMetadata.delete_key(location, json.loads(request.body))), + mimetype="application/json") + elif real_method == 'POST' or real_method == 'PUT': + # NOTE: request.POST is messed up because expect_json cloned_request.POST.copy() is creating a defective entry w/ the whole payload as the key + request_body = json.loads(request.body) + #Whether or not to filter the tabs key out of the settings metadata + filter_tabs = True + #Check to see if the user instantiated any advanced components. This is a hack to add the open ended panel tab + #to a course automatically if the user has indicated that they want to edit the combinedopenended or peergrading + #module, and to remove it if they have removed the open ended elements. + if ADVANCED_COMPONENT_POLICY_KEY in request_body: + #Check to see if the user instantiated any open ended components + found_oe_type = False + #Get the course so that we can scrape current tabs + course_module = modulestore().get_item(location) + for oe_type in OPEN_ENDED_COMPONENT_TYPES: + if oe_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]: + #Add an open ended tab to the course if needed + changed, new_tabs = add_open_ended_panel_tab(course_module) + #If a tab has been added to the course, then send the metadata along to CourseMetadata.update_from_json + if changed: + request_body.update({'tabs': new_tabs}) + #Indicate that tabs should not be filtered out of the metadata + filter_tabs = False + #Set this flag to avoid the open ended tab removal code below. + found_oe_type = True + break + #If we did not find an open ended module type in the advanced settings, + # we may need to remove the open ended tab from the course. + if not found_oe_type: + #Remove open ended tab to the course if needed + changed, new_tabs = remove_open_ended_panel_tab(course_module) + if changed: + request_body.update({'tabs': new_tabs}) + #Indicate that tabs should not be filtered out of the metadata + filter_tabs = False + response_json = json.dumps(CourseMetadata.update_from_json(location, request_body, filter_tabs=filter_tabs)) + return HttpResponse(response_json, mimetype="application/json") + + diff --git a/cms/djangoapps/contentstore/views/error.py b/cms/djangoapps/contentstore/views/error.py index 527f137b9e..814af96104 100644 --- a/cms/djangoapps/contentstore/views/error.py +++ b/cms/djangoapps/contentstore/views/error.py @@ -1,4 +1,4 @@ -from django.http import HttpResponse, HttpResponseServerError, HttpResponseNotFound +from django.http import HttpResponseServerError, HttpResponseNotFound from mitxmako.shortcuts import render_to_string, render_to_response diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py index 876251203e..b6d03e3f81 100644 --- a/cms/djangoapps/contentstore/views/item.py +++ b/cms/djangoapps/contentstore/views/item.py @@ -1,7 +1,21 @@ +import json +from uuid import uuid4 + +from django.core.exceptions import PermissionDenied from django.http import HttpResponse from django.contrib.auth.decorators import login_required + +from xmodule.modulestore import Location +from xmodule.modulestore.django import modulestore +from xmodule.modulestore.inheritance import own_metadata + from util.json_request import expect_json -from mitxmako.shortcuts import render_to_response +from contentstore.utils import get_modulestore +from access import has_access +from requests import _xmodule_recurse + +# cdodge: these are categories which should not be parented, they are detached from the hierarchy +DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info'] @login_required diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index f473b962c5..a3fc816730 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -1,3 +1,10 @@ +import logging, sys +import static_replace +from xmodule_modifiers import replace_static_urls +from xmodule.error_module import ErrorDescriptor +from xmodule.errortracker import exc_info_to_str +from django.core.urlresolvers import reverse +from mitxmako.shortcuts import render_to_response from django.contrib.auth.decorators import login_required from xblock.runtime import DbModel from xmodule.x_module import ModuleSystem @@ -6,6 +13,14 @@ from xmodule_modifiers import wrap_xmodule from session_kv_store import SessionKeyValueStore from requests import render_from_lms from functools import partial +from xmodule.modulestore import Location +from access import has_access +from xmodule.modulestore.django import modulestore +from xmodule.exceptions import NotFoundError, ProcessingError +from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden + + +log = logging.getLogger(__name__) @login_required def preview_dispatch(request, preview_id, location, dispatch=None): diff --git a/cms/djangoapps/contentstore/views/public.py b/cms/djangoapps/contentstore/views/public.py index 7c207b7893..fe26fbec7c 100644 --- a/cms/djangoapps/contentstore/views/public.py +++ b/cms/djangoapps/contentstore/views/public.py @@ -1,7 +1,11 @@ from external_auth.views import ssl_login_shortcut from mitxmako.shortcuts import render_to_response from django_future.csrf import ensure_csrf_cookie -from requests import index +from django.core.context_processors import csrf +from django.shortcuts import redirect +from django.conf import settings + +from user import index """ Public views diff --git a/cms/djangoapps/contentstore/views/requests.py b/cms/djangoapps/contentstore/views/requests.py index 131068768a..58a3275527 100644 --- a/cms/djangoapps/contentstore/views/requests.py +++ b/cms/djangoapps/contentstore/views/requests.py @@ -1,42 +1,7 @@ -from django.contrib.auth.decorators import login_required -from django_future.csrf import ensure_csrf_cookie -from mitxmako.shortcuts import render_to_response -from xmodule.modulestore import Location -from xmodule.modulestore.django import modulestore -from access import has_access -from contentstore.utils import get_url_reverse, get_lms_link_for_item -from django.conf import settings - -@login_required -@ensure_csrf_cookie -def index(request): - """ - List all courses available to the logged in user - """ - courses = modulestore('direct').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.display_name, - get_url_reverse('CourseOutline', course), - get_lms_link_for_item(course.location, course_id=course.location.course_id)) - 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================================ +import json +from django.http import HttpResponse +from mitxmako.shortcuts import render_to_string, render_to_response # points to the temporary course landing page with log in and sign up def landing(request, org, course, coursename): diff --git a/cms/djangoapps/contentstore/views/session_kv_store.py b/cms/djangoapps/contentstore/views/session_kv_store.py index 2f6868ee81..7bfb14351d 100644 --- a/cms/djangoapps/contentstore/views/session_kv_store.py +++ b/cms/djangoapps/contentstore/views/session_kv_store.py @@ -1,4 +1,4 @@ -from xblock.runtime import KeyValueStore +from xblock.runtime import KeyValueStore, InvalidScopeError class SessionKeyValueStore(KeyValueStore): def __init__(self, request, model_data): diff --git a/cms/djangoapps/contentstore/views/tabs.py b/cms/djangoapps/contentstore/views/tabs.py new file mode 100644 index 0000000000..9a6d8736bf --- /dev/null +++ b/cms/djangoapps/contentstore/views/tabs.py @@ -0,0 +1,110 @@ +from access import has_access +from util.json_request import expect_json + +from django.http import HttpResponse, HttpResponseBadRequest +from django.contrib.auth.decorators import login_required +from django.core.exceptions import PermissionDenied +from django_future.csrf import ensure_csrf_cookie +from mitxmako.shortcuts import render_to_response + +from xmodule.modulestore import Location +from xmodule.modulestore.inheritance import own_metadata +from xmodule.modulestore.django import modulestore +from contentstore.utils import get_course_for_item + + +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(), own_metadata(course)) + + +@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].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, own_metadata(course)) + 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) + + # 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 + }) diff --git a/cms/djangoapps/contentstore/views/user.py b/cms/djangoapps/contentstore/views/user.py index 5be78a0c37..0ead03257b 100644 --- a/cms/djangoapps/contentstore/views/user.py +++ b/cms/djangoapps/contentstore/views/user.py @@ -1,9 +1,23 @@ +from django.conf import settings +from django.core.exceptions import PermissionDenied +from django.core.urlresolvers import reverse from django.contrib.auth.decorators import login_required from django_future.csrf import ensure_csrf_cookie -from util.json_request import expect_json from mitxmako.shortcuts import render_to_response +from xmodule.modulestore import Location +from xmodule.modulestore.django import modulestore +from contentstore.utils import get_url_reverse, get_lms_link_for_item + +from access import has_access +from requests import create_json_response +from util.json_request import expect_json + +from auth.authz import STAFF_ROLE_NAME, INSTRUCTOR_ROLE_NAME, 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 + + def user_author_string(user): '''Get an author string for commits by this user. Format: first last . @@ -20,6 +34,33 @@ def user_author_string(user): email=user.email) +@login_required +@ensure_csrf_cookie +def index(request): + """ + List all courses available to the logged in user + """ + courses = modulestore('direct').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.display_name, + get_url_reverse('CourseOutline', course), + get_lms_link_for_item(course.location, course_id=course.location.course_id)) + for course in courses], + 'user': request.user, + 'disable_course_creation': settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff + }) + @login_required @ensure_csrf_cookie def manage_users(request, location): From 01d4fbeb7c9915d7ad3c31dbdb36594d48d49ff3 Mon Sep 17 00:00:00 2001 From: Steve Strassmann Date: Fri, 10 May 2013 14:14:36 -0400 Subject: [PATCH 03/10] reorder imports --- cms/djangoapps/contentstore/views/preview.py | 27 ++++++++++---------- cms/djangoapps/contentstore/views/public.py | 5 ++-- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index a3fc816730..36ca01ec86 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -1,24 +1,25 @@ import logging, sys import static_replace -from xmodule_modifiers import replace_static_urls +from functools import partial + +from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden +from django.core.urlresolvers import reverse +from django.contrib.auth.decorators import login_required +from mitxmako.shortcuts import render_to_response + +from xmodule_modifiers import replace_static_urls, wrap_xmodule from xmodule.error_module import ErrorDescriptor from xmodule.errortracker import exc_info_to_str -from django.core.urlresolvers import reverse -from mitxmako.shortcuts import render_to_response -from django.contrib.auth.decorators import login_required -from xblock.runtime import DbModel -from xmodule.x_module import ModuleSystem +from xmodule.exceptions import NotFoundError, ProcessingError +from xmodule.modulestore import Location +from xmodule.modulestore.django import modulestore from xmodule.modulestore.mongo import MongoUsage -from xmodule_modifiers import wrap_xmodule +from xmodule.x_module import ModuleSystem +from xblock.runtime import DbModel + from session_kv_store import SessionKeyValueStore from requests import render_from_lms -from functools import partial -from xmodule.modulestore import Location from access import has_access -from xmodule.modulestore.django import modulestore -from xmodule.exceptions import NotFoundError, ProcessingError -from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden - log = logging.getLogger(__name__) diff --git a/cms/djangoapps/contentstore/views/public.py b/cms/djangoapps/contentstore/views/public.py index fe26fbec7c..3ab9e4e5a0 100644 --- a/cms/djangoapps/contentstore/views/public.py +++ b/cms/djangoapps/contentstore/views/public.py @@ -1,10 +1,11 @@ -from external_auth.views import ssl_login_shortcut -from mitxmako.shortcuts import render_to_response from django_future.csrf import ensure_csrf_cookie from django.core.context_processors import csrf from django.shortcuts import redirect from django.conf import settings +from mitxmako.shortcuts import render_to_response + +from external_auth.views import ssl_login_shortcut from user import index """ From 70cca0540dbccf086a121d973c1dd7ba30f46b5d Mon Sep 17 00:00:00 2001 From: Steve Strassmann Date: Fri, 10 May 2013 15:23:28 -0400 Subject: [PATCH 04/10] reorder imports for pep8 --- cms/djangoapps/contentstore/views/preview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index 36ca01ec86..bbee9f621c 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -1,5 +1,4 @@ import logging, sys -import static_replace from functools import partial from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden @@ -17,6 +16,7 @@ from xmodule.modulestore.mongo import MongoUsage from xmodule.x_module import ModuleSystem from xblock.runtime import DbModel +import static_replace from session_kv_store import SessionKeyValueStore from requests import render_from_lms from access import has_access From 571a9af0c087eae413001db91ea5fdc25d01eb46 Mon Sep 17 00:00:00 2001 From: Steve Strassmann Date: Fri, 10 May 2013 16:37:28 -0400 Subject: [PATCH 05/10] declared exports --- cms/djangoapps/contentstore/views/__init__.py | 19 ++++++++++--------- cms/djangoapps/contentstore/views/assets.py | 2 ++ .../contentstore/views/checklist.py | 2 ++ cms/djangoapps/contentstore/views/error.py | 2 ++ cms/djangoapps/contentstore/views/item.py | 2 ++ cms/djangoapps/contentstore/views/preview.py | 2 ++ cms/djangoapps/contentstore/views/public.py | 2 ++ cms/djangoapps/contentstore/views/requests.py | 2 ++ cms/djangoapps/contentstore/views/tabs.py | 1 + 9 files changed, 25 insertions(+), 9 deletions(-) diff --git a/cms/djangoapps/contentstore/views/__init__.py b/cms/djangoapps/contentstore/views/__init__.py index 37f786ac3c..e37e4bae37 100644 --- a/cms/djangoapps/contentstore/views/__init__.py +++ b/cms/djangoapps/contentstore/views/__init__.py @@ -1,13 +1,14 @@ # TODO: replace asterisks, should explicitly enumerate imports instead -from assets import asset_index, upload_asset, import_course, generate_export_course, export_course -from checklist import get_checklists, update_checklist +from assets import * +from checklist import * from component import * from course import * -from error import not_found, server_error, render_404, render_500 -from item import save_item, clone_item, delete_item -from preview import preview_dispatch, preview_component -from public import signup, old_login_redirect, login_page, howitworks, ux_alerts -from user import index, add_user, remove_user, manage_users -from tabs import edit_tabs, reorder_static_tabs -from requests import edge, event, landing +from error import * +from item import * +from preview import * +from public import * +from user import * +from tabs import * +from requests import * + diff --git a/cms/djangoapps/contentstore/views/assets.py b/cms/djangoapps/contentstore/views/assets.py index c2aa52b1e1..04eb0c0ed6 100644 --- a/cms/djangoapps/contentstore/views/assets.py +++ b/cms/djangoapps/contentstore/views/assets.py @@ -25,6 +25,8 @@ from xmodule.util.date_utils import get_default_time_display from access import get_location_and_verify_access from auth.authz import create_all_course_groups +__all__ = ['asset_index', 'upload_asset', 'import_course', 'generate_export_course', 'export_course'] + @login_required @ensure_csrf_cookie def asset_index(request, org, course, name): diff --git a/cms/djangoapps/contentstore/views/checklist.py b/cms/djangoapps/contentstore/views/checklist.py index 4a97ddc1df..a86c751c14 100644 --- a/cms/djangoapps/contentstore/views/checklist.py +++ b/cms/djangoapps/contentstore/views/checklist.py @@ -12,6 +12,8 @@ from contentstore.utils import get_modulestore, get_url_reverse from requests import get_request_method from access import get_location_and_verify_access +__all__ = ['get_checklists', 'update_checklist'] + @ensure_csrf_cookie @login_required def get_checklists(request, org, course, name): diff --git a/cms/djangoapps/contentstore/views/error.py b/cms/djangoapps/contentstore/views/error.py index 814af96104..64d1538d5d 100644 --- a/cms/djangoapps/contentstore/views/error.py +++ b/cms/djangoapps/contentstore/views/error.py @@ -2,6 +2,8 @@ from django.http import HttpResponseServerError, HttpResponseNotFound from mitxmako.shortcuts import render_to_string, render_to_response +__all__ = ['not_found', 'server_error', 'render_404', 'render_500'] + def not_found(request): return render_to_response('error.html', {'error': '404'}) diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py index b6d03e3f81..ac82e38577 100644 --- a/cms/djangoapps/contentstore/views/item.py +++ b/cms/djangoapps/contentstore/views/item.py @@ -14,6 +14,8 @@ from contentstore.utils import get_modulestore from access import has_access from requests import _xmodule_recurse +__all__ = ['save_item', 'clone_item', 'delete_item'] + # cdodge: these are categories which should not be parented, they are detached from the hierarchy DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info'] diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index bbee9f621c..5e8abb238c 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -21,6 +21,8 @@ from session_kv_store import SessionKeyValueStore from requests import render_from_lms from access import has_access +__all__ = ['preview_dispatch', 'preview_component'] + log = logging.getLogger(__name__) @login_required diff --git a/cms/djangoapps/contentstore/views/public.py b/cms/djangoapps/contentstore/views/public.py index 3ab9e4e5a0..fe8a2b7a91 100644 --- a/cms/djangoapps/contentstore/views/public.py +++ b/cms/djangoapps/contentstore/views/public.py @@ -8,6 +8,8 @@ from mitxmako.shortcuts import render_to_response from external_auth.views import ssl_login_shortcut from user import index +__all__ = ['signup', 'old_login_redirect', 'login_page', 'howitworks', 'ux_alerts'] + """ Public views """ diff --git a/cms/djangoapps/contentstore/views/requests.py b/cms/djangoapps/contentstore/views/requests.py index 58a3275527..46b42fad7a 100644 --- a/cms/djangoapps/contentstore/views/requests.py +++ b/cms/djangoapps/contentstore/views/requests.py @@ -3,6 +3,8 @@ import json from django.http import HttpResponse from mitxmako.shortcuts import render_to_string, render_to_response +__all__ = ['edge', 'event', 'landing'] + # 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', {}) diff --git a/cms/djangoapps/contentstore/views/tabs.py b/cms/djangoapps/contentstore/views/tabs.py index 9a6d8736bf..b947c163eb 100644 --- a/cms/djangoapps/contentstore/views/tabs.py +++ b/cms/djangoapps/contentstore/views/tabs.py @@ -12,6 +12,7 @@ from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.django import modulestore from contentstore.utils import get_course_for_item +__all__ = ['edit_tabs', 'reorder_static_tabs'] def initialize_course_tabs(course): # set up the default tabs From c071ee448f69aa409f3898ea19f842d05cc60881 Mon Sep 17 00:00:00 2001 From: Steve Strassmann Date: Fri, 10 May 2013 16:51:26 -0400 Subject: [PATCH 06/10] pep8 compliance --- cms/djangoapps/contentstore/views/__init__.py | 2 -- cms/djangoapps/contentstore/views/component.py | 2 ++ cms/djangoapps/contentstore/views/course.py | 4 +++- cms/djangoapps/contentstore/views/preview.py | 9 ++++++--- cms/djangoapps/contentstore/views/requests.py | 1 - 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/cms/djangoapps/contentstore/views/__init__.py b/cms/djangoapps/contentstore/views/__init__.py index e37e4bae37..e17b27e9b1 100644 --- a/cms/djangoapps/contentstore/views/__init__.py +++ b/cms/djangoapps/contentstore/views/__init__.py @@ -1,5 +1,3 @@ -# TODO: replace asterisks, should explicitly enumerate imports instead - from assets import * from checklist import * from component import * diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index 2dd7307976..f2a63c9b2c 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -25,6 +25,8 @@ from models.settings.course_grading import CourseGradingModel from requests import get_request_method, _xmodule_recurse from access import has_access, get_location_and_verify_access +# TODO: should explicitly enumerate exports with __all__ + # to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz' log = logging.getLogger(__name__) diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index c0f6acc808..336a4ad0fe 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -17,13 +17,15 @@ from contentstore.utils import get_lms_link_for_item, add_open_ended_panel_tab, from models.settings.course_details import CourseDetails, CourseSettingsEncoder from models.settings.course_grading import CourseGradingModel from models.settings.course_metadata import CourseMetadata -from component import OPEN_ENDED_COMPONENT_TYPES, ADVANCED_COMPONENT_POLICY_KEY from auth.authz import create_all_course_groups from util.json_request import expect_json + from access import has_access, get_location_and_verify_access from requests import get_request_method from tabs import initialize_course_tabs +from component import OPEN_ENDED_COMPONENT_TYPES, ADVANCED_COMPONENT_POLICY_KEY +# TODO: should explicitly enumerate exports with __all__ @login_required @ensure_csrf_cookie diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index 5e8abb238c..0b839a6754 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -1,4 +1,5 @@ -import logging, sys +import logging +import sys from functools import partial from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden @@ -25,6 +26,7 @@ __all__ = ['preview_dispatch', 'preview_component'] log = logging.getLogger(__name__) + @login_required def preview_dispatch(request, preview_id, location, dispatch=None): """ @@ -58,6 +60,7 @@ def preview_dispatch(request, preview_id, location, dispatch=None): return HttpResponse(ajax_return) + @login_required def preview_component(request, location): # TODO (vshnayder): change name from id to location in coffee+html as well. @@ -72,7 +75,6 @@ def preview_component(request, location): }) - def preview_module_system(request, preview_id, descriptor): """ Returns a ModuleSystem for the specified descriptor that is specialized for @@ -104,6 +106,7 @@ def preview_module_system(request, preview_id, descriptor): xblock_model_data=preview_model_data, ) + def get_preview_module(request, preview_id, descriptor): """ Returns a preview XModule at the specified location. The preview_data is chosen arbitrarily @@ -159,6 +162,7 @@ def load_preview_module(request, preview_id, descriptor): return module + def get_module_previews(request, descriptor): """ Returns a list of preview XModule html contents. One preview is returned for each @@ -171,4 +175,3 @@ def get_module_previews(request, descriptor): module = load_preview_module(request, str(idx), descriptor) preview_html.append(module.get_html()) return preview_html - diff --git a/cms/djangoapps/contentstore/views/requests.py b/cms/djangoapps/contentstore/views/requests.py index 46b42fad7a..07903637d3 100644 --- a/cms/djangoapps/contentstore/views/requests.py +++ b/cms/djangoapps/contentstore/views/requests.py @@ -40,7 +40,6 @@ def create_json_response(errmsg=None): resp = HttpResponse(json.dumps({'Status': 'Failed', 'ErrMsg': errmsg})) else: resp = HttpResponse(json.dumps({'Status': 'OK'})) - return resp def render_from_lms(template_name, dictionary, context=None, namespace='main'): From 979e3be1746180eccecf076c7a970f4a9187af71 Mon Sep 17 00:00:00 2001 From: Steve Strassmann Date: Mon, 13 May 2013 09:58:30 -0400 Subject: [PATCH 07/10] fix pep8: imports and blank lines --- cms/djangoapps/contentstore/views/__init__.py | 1 - cms/djangoapps/contentstore/views/access.py | 1 + cms/djangoapps/contentstore/views/assets.py | 10 ++++++-- .../contentstore/views/checklist.py | 5 +--- .../contentstore/views/component.py | 24 +++---------------- cms/djangoapps/contentstore/views/course.py | 7 +++--- cms/djangoapps/contentstore/views/error.py | 2 -- cms/djangoapps/contentstore/views/item.py | 1 - cms/djangoapps/contentstore/views/public.py | 3 ++- cms/djangoapps/contentstore/views/requests.py | 5 +++- .../contentstore/views/session_kv_store.py | 1 + cms/djangoapps/contentstore/views/tabs.py | 21 ++++++++++++++++ cms/djangoapps/contentstore/views/user.py | 4 +--- 13 files changed, 46 insertions(+), 39 deletions(-) diff --git a/cms/djangoapps/contentstore/views/__init__.py b/cms/djangoapps/contentstore/views/__init__.py index e17b27e9b1..d31102c58e 100644 --- a/cms/djangoapps/contentstore/views/__init__.py +++ b/cms/djangoapps/contentstore/views/__init__.py @@ -9,4 +9,3 @@ from public import * from user import * from tabs import * from requests import * - diff --git a/cms/djangoapps/contentstore/views/access.py b/cms/djangoapps/contentstore/views/access.py index 37f6fcb767..0e4985ff8a 100644 --- a/cms/djangoapps/contentstore/views/access.py +++ b/cms/djangoapps/contentstore/views/access.py @@ -3,6 +3,7 @@ from auth.authz import is_user_in_course_group_role from contentstore.utils import get_course_location_for_item from django.core.exceptions import PermissionDenied + def get_location_and_verify_access(request, org, course, name): """ Create the location tuple verify that the user has permissions diff --git a/cms/djangoapps/contentstore/views/assets.py b/cms/djangoapps/contentstore/views/assets.py index 04eb0c0ed6..edc1c977ab 100644 --- a/cms/djangoapps/contentstore/views/assets.py +++ b/cms/djangoapps/contentstore/views/assets.py @@ -1,4 +1,8 @@ -import logging, json, os, tarfile, shutil +import logging +import json +import os +import tarfile +import shutil from tempfile import mkdtemp from path import path @@ -27,6 +31,7 @@ from auth.authz import create_all_course_groups __all__ = ['asset_index', 'upload_asset', 'import_course', 'generate_export_course', 'export_course'] + @login_required @ensure_csrf_cookie def asset_index(request, org, course, name): @@ -135,6 +140,7 @@ def upload_asset(request, org, course, coursename): response['asset_url'] = StaticContent.get_url_path_from_location(content.location) return response + @ensure_csrf_cookie @login_required def import_course(request, org, course, name): @@ -240,6 +246,7 @@ def generate_export_course(request, org, course, name): response['Content-Length'] = os.path.getsize(export_file.name) return response + @ensure_csrf_cookie @login_required def export_course(request, org, course, name): @@ -253,4 +260,3 @@ def export_course(request, org, course, name): 'active_tab': 'export', 'successful_import_redirect_url': '' }) - diff --git a/cms/djangoapps/contentstore/views/checklist.py b/cms/djangoapps/contentstore/views/checklist.py index a86c751c14..d6efdefdc3 100644 --- a/cms/djangoapps/contentstore/views/checklist.py +++ b/cms/djangoapps/contentstore/views/checklist.py @@ -14,6 +14,7 @@ from access import get_location_and_verify_access __all__ = ['get_checklists', 'update_checklist'] + @ensure_csrf_cookie @login_required def get_checklists(request, org, course, name): @@ -101,7 +102,3 @@ def expand_checklist_action_urls(course_module): modified = True return checklists, modified - - - - diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index f2a63c9b2c..00fcce9f2b 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -1,4 +1,5 @@ -import json, logging +import json +import logging from collections import defaultdict from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden @@ -23,7 +24,7 @@ from contentstore.utils import get_modulestore, get_lms_link_for_item, \ from models.settings.course_grading import CourseGradingModel from requests import get_request_method, _xmodule_recurse -from access import has_access, get_location_and_verify_access +from access import has_access # TODO: should explicitly enumerate exports with __all__ @@ -269,23 +270,6 @@ def unpublish_unit(request): return HttpResponse() -@login_required -@ensure_csrf_cookie -def static_pages(request, org, course, coursename): - - location = get_location_and_verify_access(request, org, course, coursename) - - 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', {}) - @expect_json @login_required @ensure_csrf_cookie @@ -311,5 +295,3 @@ def module_info(request, module_location): return HttpResponse(json.dumps(set_module_info(get_modulestore(location), location, request.POST)), mimetype="application/json") else: return HttpResponseBadRequest() - - diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index 336a4ad0fe..f1414faf4e 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -1,4 +1,5 @@ -import json, time +import json +import time from django.contrib.auth.decorators import login_required from django_future.csrf import ensure_csrf_cookie @@ -27,6 +28,7 @@ from component import OPEN_ENDED_COMPONENT_TYPES, ADVANCED_COMPONENT_POLICY_KEY # TODO: should explicitly enumerate exports with __all__ + @login_required @ensure_csrf_cookie def course_index(request, org, course, name): @@ -188,6 +190,7 @@ def course_info_updates(request, org, course, provided_id=None): return HttpResponseBadRequest("Failed to save", content_type="text/plain") + @login_required @ensure_csrf_cookie def get_course_settings(request, org, course, name): @@ -362,5 +365,3 @@ def course_advanced_updates(request, org, course, name): filter_tabs = False response_json = json.dumps(CourseMetadata.update_from_json(location, request_body, filter_tabs=filter_tabs)) return HttpResponse(response_json, mimetype="application/json") - - diff --git a/cms/djangoapps/contentstore/views/error.py b/cms/djangoapps/contentstore/views/error.py index 64d1538d5d..0422452c47 100644 --- a/cms/djangoapps/contentstore/views/error.py +++ b/cms/djangoapps/contentstore/views/error.py @@ -19,5 +19,3 @@ def render_404(request): def render_500(request): return HttpResponseServerError(render_to_string('500.html', {})) - - diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py index ac82e38577..67f1da2710 100644 --- a/cms/djangoapps/contentstore/views/item.py +++ b/cms/djangoapps/contentstore/views/item.py @@ -136,4 +136,3 @@ def delete_item(request): modulestore('direct').update_children(parent.location, parent.children) return HttpResponse() - diff --git a/cms/djangoapps/contentstore/views/public.py b/cms/djangoapps/contentstore/views/public.py index fe8a2b7a91..1049b29a28 100644 --- a/cms/djangoapps/contentstore/views/public.py +++ b/cms/djangoapps/contentstore/views/public.py @@ -14,6 +14,7 @@ __all__ = ['signup', 'old_login_redirect', 'login_page', 'howitworks', 'ux_alert Public views """ + @ensure_csrf_cookie def signup(request): """ @@ -49,9 +50,9 @@ def howitworks(request): else: return render_to_response('howitworks.html', {}) + def ux_alerts(request): """ static/proof-of-concept views """ return render_to_response('ux-alerts.html', {}) - diff --git a/cms/djangoapps/contentstore/views/requests.py b/cms/djangoapps/contentstore/views/requests.py index 07903637d3..b02a13fe3f 100644 --- a/cms/djangoapps/contentstore/views/requests.py +++ b/cms/djangoapps/contentstore/views/requests.py @@ -5,10 +5,12 @@ from mitxmako.shortcuts import render_to_string, render_to_response __all__ = ['edge', 'event', 'landing'] + # 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', {}) + # points to the temporary edge page def edge(request): return render_to_response('university_profiles/edge.html', {}) @@ -35,6 +37,7 @@ def get_request_method(request): return real_method + def create_json_response(errmsg=None): if errmsg is not None: resp = HttpResponse(json.dumps({'Status': 'Failed', 'ErrMsg': errmsg})) @@ -42,6 +45,7 @@ def create_json_response(errmsg=None): resp = HttpResponse(json.dumps({'Status': 'OK'})) return resp + def render_from_lms(template_name, dictionary, context=None, namespace='main'): """ Render a template using the LMS MAKO_TEMPLATES @@ -54,4 +58,3 @@ def _xmodule_recurse(item, action): _xmodule_recurse(child, action) action(item) - diff --git a/cms/djangoapps/contentstore/views/session_kv_store.py b/cms/djangoapps/contentstore/views/session_kv_store.py index 7bfb14351d..309518c27d 100644 --- a/cms/djangoapps/contentstore/views/session_kv_store.py +++ b/cms/djangoapps/contentstore/views/session_kv_store.py @@ -1,5 +1,6 @@ from xblock.runtime import KeyValueStore, InvalidScopeError + class SessionKeyValueStore(KeyValueStore): def __init__(self, request, model_data): self._model_data = model_data diff --git a/cms/djangoapps/contentstore/views/tabs.py b/cms/djangoapps/contentstore/views/tabs.py index b947c163eb..8ecfc31602 100644 --- a/cms/djangoapps/contentstore/views/tabs.py +++ b/cms/djangoapps/contentstore/views/tabs.py @@ -11,9 +11,11 @@ from xmodule.modulestore import Location from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.django import modulestore from contentstore.utils import get_course_for_item +from access import get_location_and_verify_access __all__ = ['edit_tabs', 'reorder_static_tabs'] + 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 @@ -75,6 +77,7 @@ def reorder_static_tabs(request): modulestore('direct').update_metadata(course.location, own_metadata(course)) return HttpResponse() + @login_required @ensure_csrf_cookie def edit_tabs(request, org, course, coursename): @@ -109,3 +112,21 @@ def edit_tabs(request, org, course, coursename): 'context_course': course_item, 'components': components }) + + +@login_required +@ensure_csrf_cookie +def static_pages(request, org, course, coursename): + + location = get_location_and_verify_access(request, org, course, coursename) + + 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', {}) diff --git a/cms/djangoapps/contentstore/views/user.py b/cms/djangoapps/contentstore/views/user.py index 0ead03257b..6b1926bbae 100644 --- a/cms/djangoapps/contentstore/views/user.py +++ b/cms/djangoapps/contentstore/views/user.py @@ -61,6 +61,7 @@ def index(request): 'disable_course_creation': settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff }) + @login_required @ensure_csrf_cookie def manage_users(request, location): @@ -84,8 +85,6 @@ def manage_users(request, location): }) - - @expect_json @login_required @ensure_csrf_cookie @@ -145,4 +144,3 @@ def remove_user(request, location): remove_user_from_course_group(request.user, user, location, STAFF_ROLE_NAME) return create_json_response() - From b288a8e3bda5224724fe1cc989219894947cee9f Mon Sep 17 00:00:00 2001 From: Steve Strassmann Date: Mon, 13 May 2013 10:03:20 -0400 Subject: [PATCH 08/10] fix export from tabs.__all__ --- cms/djangoapps/contentstore/views/tabs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/views/tabs.py b/cms/djangoapps/contentstore/views/tabs.py index 8ecfc31602..672723a161 100644 --- a/cms/djangoapps/contentstore/views/tabs.py +++ b/cms/djangoapps/contentstore/views/tabs.py @@ -13,7 +13,7 @@ from xmodule.modulestore.django import modulestore from contentstore.utils import get_course_for_item from access import get_location_and_verify_access -__all__ = ['edit_tabs', 'reorder_static_tabs'] +__all__ = ['edit_tabs', 'reorder_static_tabs', 'static_pages', 'edit_static'] def initialize_course_tabs(course): From e03585d85c463cafbe1fcb6daa4fd3495230de8d Mon Sep 17 00:00:00 2001 From: Steve Strassmann Date: Mon, 13 May 2013 11:07:08 -0400 Subject: [PATCH 09/10] pylint: refactor imports --- cms/djangoapps/contentstore/views/__init__.py | 30 ++++++++----- cms/djangoapps/contentstore/views/course.py | 45 ++++++++++++------- 2 files changed, 48 insertions(+), 27 deletions(-) diff --git a/cms/djangoapps/contentstore/views/__init__.py b/cms/djangoapps/contentstore/views/__init__.py index d31102c58e..0da8a15707 100644 --- a/cms/djangoapps/contentstore/views/__init__.py +++ b/cms/djangoapps/contentstore/views/__init__.py @@ -1,11 +1,19 @@ -from assets import * -from checklist import * -from component import * -from course import * -from error import * -from item import * -from preview import * -from public import * -from user import * -from tabs import * -from requests import * +# pylint: disable=W0401, W0511 + +# TODO: component.py should explicitly enumerate exports with __all__ +from .component import * + +# TODO: course.py should explicitly enumerate exports with __all__ +from .course import * + +# Disable warnings about import from wildcard +# All files below declare exports with __all__ +from .assets import * +from .checklist import * +from .error import * +from .item import * +from .preview import * +from .public import * +from .user import * +from .tabs import * +from .requests import * diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index f1414faf4e..f71759e410 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -10,12 +10,16 @@ from django.core.urlresolvers import reverse from mitxmako.shortcuts import render_to_response from xmodule.modulestore.django import modulestore -from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError +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.utils import get_lms_link_for_item, add_open_ended_panel_tab, remove_open_ended_panel_tab -from models.settings.course_details import CourseDetails, CourseSettingsEncoder +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_open_ended_panel_tab, remove_open_ended_panel_tab +from models.settings.course_details import CourseDetails, \ + CourseSettingsEncoder from models.settings.course_grading import CourseGradingModel from models.settings.course_metadata import CourseMetadata from auth.authz import create_all_course_groups @@ -24,7 +28,8 @@ from util.json_request import expect_json from access import has_access, get_location_and_verify_access from requests import get_request_method from tabs import initialize_course_tabs -from component import OPEN_ENDED_COMPONENT_TYPES, ADVANCED_COMPONENT_POLICY_KEY +from component import OPEN_ENDED_COMPONENT_TYPES, \ + ADVANCED_COMPONENT_POLICY_KEY # TODO: should explicitly enumerate exports with __all__ @@ -325,18 +330,23 @@ def course_advanced_updates(request, org, course, name): real_method = get_request_method(request) if real_method == 'GET': - return HttpResponse(json.dumps(CourseMetadata.fetch(location)), mimetype="application/json") + return HttpResponse(json.dumps(CourseMetadata.fetch(location)), + mimetype="application/json") elif real_method == 'DELETE': - return HttpResponse(json.dumps(CourseMetadata.delete_key(location, json.loads(request.body))), + return HttpResponse(json.dumps(CourseMetadata.delete_key(location, + json.loads(request.body))), mimetype="application/json") elif real_method == 'POST' or real_method == 'PUT': - # NOTE: request.POST is messed up because expect_json cloned_request.POST.copy() is creating a defective entry w/ the whole payload as the key + # NOTE: request.POST is messed up because expect_json + # cloned_request.POST.copy() is creating a defective entry w/ the whole payload as the key request_body = json.loads(request.body) - #Whether or not to filter the tabs key out of the settings metadata + # Whether or not to filter the tabs key out of the settings metadata filter_tabs = True - #Check to see if the user instantiated any advanced components. This is a hack to add the open ended panel tab - #to a course automatically if the user has indicated that they want to edit the combinedopenended or peergrading - #module, and to remove it if they have removed the open ended elements. + # Check to see if the user instantiated any advanced components. + # This is a hack to add the open ended panel tab + # to a course automatically if the user has indicated that they want + # to edit the combinedopenended or peergrading + # module, and to remove it if they have removed the open ended elements. if ADVANCED_COMPONENT_POLICY_KEY in request_body: #Check to see if the user instantiated any open ended components found_oe_type = False @@ -346,7 +356,8 @@ def course_advanced_updates(request, org, course, name): if oe_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]: #Add an open ended tab to the course if needed changed, new_tabs = add_open_ended_panel_tab(course_module) - #If a tab has been added to the course, then send the metadata along to CourseMetadata.update_from_json + # If a tab has been added to the course, then send the + # metadata along to CourseMetadata.update_from_json if changed: request_body.update({'tabs': new_tabs}) #Indicate that tabs should not be filtered out of the metadata @@ -357,11 +368,13 @@ def course_advanced_updates(request, org, course, name): #If we did not find an open ended module type in the advanced settings, # we may need to remove the open ended tab from the course. if not found_oe_type: - #Remove open ended tab to the course if needed + # Remove open ended tab to the course if needed changed, new_tabs = remove_open_ended_panel_tab(course_module) if changed: request_body.update({'tabs': new_tabs}) - #Indicate that tabs should not be filtered out of the metadata + # Indicate that tabs should not be filtered out of the metadata filter_tabs = False - response_json = json.dumps(CourseMetadata.update_from_json(location, request_body, filter_tabs=filter_tabs)) + response_json = json.dumps(CourseMetadata.update_from_json(location, + request_body, + filter_tabs=filter_tabs)) return HttpResponse(response_json, mimetype="application/json") From 96c37089dc0174ee16a1417f307ec6680d4c009b Mon Sep 17 00:00:00 2001 From: Steve Strassmann Date: Mon, 13 May 2013 13:47:51 -0400 Subject: [PATCH 10/10] whitespace for pep8 --- cms/djangoapps/contentstore/views/component.py | 4 ++-- cms/djangoapps/contentstore/views/course.py | 14 +++++++------- cms/djangoapps/contentstore/views/user.py | 1 + 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index 33f8b2dc98..18253925e1 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -23,8 +23,8 @@ from contentstore.utils import get_modulestore, get_lms_link_for_item, \ from models.settings.course_grading import CourseGradingModel -from requests import get_request_method, _xmodule_recurse -from access import has_access +from .requests import get_request_method, _xmodule_recurse +from .access import has_access # TODO: should explicitly enumerate exports with __all__ diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index c85ebd5ade..ecc271926a 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -309,7 +309,7 @@ def course_grader_updates(request, org, course, name, grader_index=None): # ??? Should this return anything? Perhaps success fail? CourseGradingModel.delete_grader(Location(location), grader_index) return HttpResponse() - elif request.method == 'POST': # post or put, doesn't matter. + elif request.method == 'POST': # post or put, doesn't matter. return HttpResponse(json.dumps(CourseGradingModel.update_grader_from_json(Location(location), request.POST)), mimetype="application/json") @@ -347,24 +347,24 @@ def course_advanced_updates(request, org, course, name): # to edit the combinedopenended or peergrading # module, and to remove it if they have removed the open ended elements. if ADVANCED_COMPONENT_POLICY_KEY in request_body: - #Check to see if the user instantiated any open ended components + # Check to see if the user instantiated any open ended components found_oe_type = False - #Get the course so that we can scrape current tabs + # Get the course so that we can scrape current tabs course_module = modulestore().get_item(location) for oe_type in OPEN_ENDED_COMPONENT_TYPES: if oe_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]: - #Add an open ended tab to the course if needed + # Add an open ended tab to the course if needed changed, new_tabs = add_open_ended_panel_tab(course_module) # If a tab has been added to the course, then send the # metadata along to CourseMetadata.update_from_json if changed: request_body.update({'tabs': new_tabs}) - #Indicate that tabs should not be filtered out of the metadata + # Indicate that tabs should not be filtered out of the metadata filter_tabs = False - #Set this flag to avoid the open ended tab removal code below. + # Set this flag to avoid the open ended tab removal code below. found_oe_type = True break - #If we did not find an open ended module type in the advanced settings, + # If we did not find an open ended module type in the advanced settings, # we may need to remove the open ended tab from the course. if not found_oe_type: # Remove open ended tab to the course if needed diff --git a/cms/djangoapps/contentstore/views/user.py b/cms/djangoapps/contentstore/views/user.py index 70f633c5f1..462b184718 100644 --- a/cms/djangoapps/contentstore/views/user.py +++ b/cms/djangoapps/contentstore/views/user.py @@ -15,6 +15,7 @@ from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_ from .access import has_access from .requests import create_json_response + def user_author_string(user): '''Get an author string for commits by this user. Format: first last .