From 9350a2c0674eee804ba7aae96c6d358a5156b291 Mon Sep 17 00:00:00 2001 From: Steve Strassmann Date: Fri, 10 May 2013 13:56:30 -0400 Subject: [PATCH] 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):