def test_post_course_update(self): """ Test that a user can successfully post on course updates and handouts of a course whose location in not in loc_mapper """ # create a course via the view handler course_location = Location(['i4x', 'Org_1', 'Course_1', 'course', 'Run_1']) course_locator = loc_mapper().translate_location( course_location.course_id, course_location, False, True ) self.client.ajax_post( course_locator.url_reverse('course'), { 'org': course_location.org, 'number': course_location.course, 'display_name': 'test course', 'run': course_location.name, } ) branch = u'draft' version = None block = u'updates' updates_locator = BlockUsageLocator( package_id=course_location.course_id.replace('/', '.'), branch=branch, version_guid=version, block_id=block ) content = u"Sample update" payload = {'content': content, 'date': 'January 8, 2013'} course_update_url = updates_locator.url_reverse('course_info_update') resp = self.client.ajax_post(course_update_url, payload) # check that response status is 200 not 400 self.assertEqual(resp.status_code, 200) payload = json.loads(resp.content) self.assertHTMLEqual(payload['content'], content) # now test that calling translate_location returns a locator whose block_id is 'updates' updates_location = course_location.replace(category='course_info', name=block) updates_locator = loc_mapper().translate_location(course_location.course_id, updates_location) self.assertTrue(isinstance(updates_locator, BlockUsageLocator)) self.assertEqual(updates_locator.block_id, block) # check posting on handouts block = u'handouts' handouts_locator = BlockUsageLocator( package_id=updates_locator.package_id, branch=updates_locator.branch, version_guid=version, block_id=block ) course_handouts_url = handouts_locator.url_reverse('xblock') content = u"Sample handout" payload = {"data": content} resp = self.client.ajax_post(course_handouts_url, payload) # check that response status is 200 not 500 self.assertEqual(resp.status_code, 200) payload = json.loads(resp.content) self.assertHTMLEqual(payload['data'], content)
def settings_handler(request, tag=None, course_id=None, branch=None, version_guid=None, block=None): """ Course settings for dates and about pages GET html: get the page json: get the CourseDetails model PUT json: update the Course and About xblocks through the CourseDetails model """ locator = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block) if not has_access(request.user, locator): raise PermissionDenied() if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET': course_old_location = loc_mapper().translate_locator_to_location(locator) course_module = modulestore().get_item(course_old_location) upload_asset_url = locator.url_reverse('assets/') return render_to_response('settings.html', { 'context_course': course_module, 'course_locator': locator, 'lms_link_for_about_page': utils.get_lms_link_for_about_page(course_old_location), 'course_image_url': utils.course_image_url(course_module), 'details_url': locator.url_reverse('/settings/details/'), 'about_page_editable': not settings.FEATURES.get( 'ENABLE_MKTG_SITE', False ), 'upload_asset_url': upload_asset_url }) elif 'application/json' in request.META.get('HTTP_ACCEPT', ''): if request.method == 'GET': return JsonResponse( CourseDetails.fetch(locator), # encoder serializes dates, old locations, and instances encoder=CourseSettingsEncoder ) else: # post or put, doesn't matter. return JsonResponse( CourseDetails.update_from_json(locator, request.json), encoder=CourseSettingsEncoder )
def grading_handler(request, tag=None, course_id=None, branch=None, version_guid=None, block=None, grader_index=None): """ Course Grading policy configuration GET html: get the page json no grader_index: get the CourseGrading model (graceperiod, cutoffs, and graders) json w/ grader_index: get the specific grader PUT json no grader_index: update the Course through the CourseGrading model json w/ grader_index: create or update the specific grader (create if index out of range) """ locator = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block) if not has_access(request.user, locator): raise PermissionDenied() if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET': course_old_location = loc_mapper().translate_locator_to_location(locator) course_module = modulestore().get_item(course_old_location) course_details = CourseGradingModel.fetch(locator) return render_to_response('settings_graders.html', { 'context_course': course_module, 'course_locator': locator, 'course_details': json.dumps(course_details, cls=CourseSettingsEncoder), 'grading_url': locator.url_reverse('/settings/grading/'), }) elif 'application/json' in request.META.get('HTTP_ACCEPT', ''): if request.method == 'GET': if grader_index is None: return JsonResponse( CourseGradingModel.fetch(locator), # encoder serializes dates, old locations, and instances encoder=CourseSettingsEncoder ) else: return JsonResponse(CourseGradingModel.fetch_grader(locator, grader_index)) elif request.method in ('POST', 'PUT'): # post or put, doesn't matter. # None implies update the whole model (cutoffs, graceperiod, and graders) not a specific grader if grader_index is None: return JsonResponse( CourseGradingModel.update_from_json(locator, request.json), encoder=CourseSettingsEncoder ) else: return JsonResponse( CourseGradingModel.update_grader_from_json(locator, request.json) ) elif request.method == "DELETE" and grader_index is not None: CourseGradingModel.delete_grader(locator, grader_index) return JsonResponse()
def advanced_settings_handler(request, course_id=None, branch=None, version_guid=None, block=None, tag=None): """ Course settings configuration GET html: get the page json: get the model PUT, POST json: update the Course's settings. The payload is a json rep of the metadata dicts. The dict can include a "unsetKeys" entry which is a list of keys whose values to unset: i.e., revert to default """ locator = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block) if not has_access(request.user, locator): raise PermissionDenied() course_old_location = loc_mapper().translate_locator_to_location(locator) course_module = modulestore().get_item(course_old_location) if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET': return render_to_response('settings_advanced.html', { 'context_course': course_module, 'advanced_dict': json.dumps(CourseMetadata.fetch(course_module)), 'advanced_settings_url': locator.url_reverse('settings/advanced') }) elif 'application/json' in request.META.get('HTTP_ACCEPT', ''): if request.method == 'GET': return JsonResponse(CourseMetadata.fetch(course_module)) else: # Whether or not to filter the tabs key out of the settings metadata filter_tabs = _config_course_advanced_components(request, course_module) try: return JsonResponse(CourseMetadata.update_from_json( course_module, request.json, filter_tabs=filter_tabs )) except (TypeError, ValueError) as err: return HttpResponseBadRequest( "Incorrect setting format. {}".format(err), content_type="text/plain" )
def import_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None): """ The restful handler for importing a course. GET html: return html page for import page json: not supported POST or PUT json: import a course via the .tar.gz file specified in request.FILES """ location = BlockUsageLocator(package_id=package_id, branch=branch, version_guid=version_guid, block_id=block) if not has_course_access(request.user, location): raise PermissionDenied() old_location = loc_mapper().translate_locator_to_location(location) if 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'): if request.method == 'GET': raise NotImplementedError('coming soon') else: data_root = path(settings.GITHUB_REPO_ROOT) course_subdir = "{0}-{1}-{2}".format(old_location.org, old_location.course, old_location.name) course_dir = data_root / course_subdir filename = request.FILES['course-data'].name if not filename.endswith('.tar.gz'): return JsonResponse( { 'ErrMsg': _('We only support uploading a .tar.gz file.'), 'Stage': 1 }, status=415 ) temp_filepath = course_dir / filename if not course_dir.isdir(): os.mkdir(course_dir) logging.debug('importing course to {0}'.format(temp_filepath)) # Get upload chunks byte ranges try: matches = CONTENT_RE.search(request.META["HTTP_CONTENT_RANGE"]) content_range = matches.groupdict() except KeyError: # Single chunk # no Content-Range header, so make one that will work content_range = {'start': 0, 'stop': 1, 'end': 2} # stream out the uploaded files in chunks to disk if int(content_range['start']) == 0: mode = "wb+" else: mode = "ab+" size = os.path.getsize(temp_filepath) # Check to make sure we haven't missed a chunk # This shouldn't happen, even if different instances are handling # the same session, but it's always better to catch errors earlier. if size < int(content_range['start']): log.warning( "Reported range %s does not match size downloaded so far %s", content_range['start'], size ) return JsonResponse( { 'ErrMsg': _('File upload corrupted. Please try again'), 'Stage': 1 }, status=409 ) # The last request sometimes comes twice. This happens because # nginx sends a 499 error code when the response takes too long. elif size > int(content_range['stop']) and size == int(content_range['end']): return JsonResponse({'ImportStatus': 1}) with open(temp_filepath, mode) as temp_file: for chunk in request.FILES['course-data'].chunks(): temp_file.write(chunk) size = os.path.getsize(temp_filepath) if int(content_range['stop']) != int(content_range['end']) - 1: # More chunks coming return JsonResponse({ "files": [{ "name": filename, "size": size, "deleteUrl": "", "deleteType": "", "url": location.url_reverse('import'), "thumbnailUrl": "" }] }) else: # This was the last chunk. # Use sessions to keep info about import progress session_status = request.session.setdefault("import_status", {}) key = location.package_id + filename session_status[key] = 1 request.session.modified = True # Do everything from now on in a try-finally block to make sure # everything is properly cleaned up. try: tar_file = tarfile.open(temp_filepath) try: safetar_extractall(tar_file, (course_dir + '/').encode('utf-8')) except SuspiciousOperation as exc: return JsonResponse( { 'ErrMsg': 'Unsafe tar file. Aborting import.', 'SuspiciousFileOperationMsg': exc.args[0], 'Stage': 1 }, status=400 ) finally: tar_file.close() session_status[key] = 2 request.session.modified = True # find the 'course.xml' file def get_all_files(directory): """ For each file in the directory, yield a 2-tuple of (file-name, directory-path) """ for dirpath, _dirnames, filenames in os.walk(directory): for filename in filenames: yield (filename, dirpath) def get_dir_for_fname(directory, filename): """ Returns the dirpath for the first file found in the directory with the given name. If there is no file in the directory with the specified name, return None. """ for fname, dirpath in get_all_files(directory): if fname == filename: return dirpath return None fname = "course.xml" dirpath = get_dir_for_fname(course_dir, fname) if not dirpath: return JsonResponse( { 'ErrMsg': _('Could not find the course.xml file in the package.'), 'Stage': 2 }, status=415 ) logging.debug('found course.xml at {0}'.format(dirpath)) if dirpath != course_dir: for fname in os.listdir(dirpath): shutil.move(dirpath / 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=old_location, draft_store=modulestore() ) new_location = course_items[0].location logging.debug('new course at {0}'.format(new_location)) session_status[key] = 3 request.session.modified = True auth.add_users(request.user, CourseInstructorRole(new_location), request.user) auth.add_users(request.user, CourseStaffRole(new_location), request.user) logging.debug('created all course groups at {0}'.format(new_location)) # Send errors to client with stage at which error occurred. except Exception as exception: # pylint: disable=W0703 return JsonResponse( { 'ErrMsg': str(exception), 'Stage': session_status[key] }, status=400 ) finally: shutil.rmtree(course_dir) return JsonResponse({'Status': 'OK'}) elif request.method == 'GET': # assume html course_module = modulestore().get_item(old_location) return render_to_response('import.html', { 'context_course': course_module, 'successful_import_redirect_url': location.url_reverse("course"), 'import_status_url': location.url_reverse("import_status", "fillerName"), }) else: return HttpResponseNotFound()
def export_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None): """ The restful handler for exporting a course. GET html: return html page for import page application/x-tgz: return tar.gz file containing exported course json: not supported Note that there are 2 ways to request the tar.gz file. The request header can specify application/x-tgz via HTTP_ACCEPT, or a query parameter can be used (?_accept=application/x-tgz). If the tar.gz file has been requested but the export operation fails, an HTML page will be returned which describes the error. """ location = BlockUsageLocator(package_id=package_id, branch=branch, version_guid=version_guid, block_id=block) if not has_course_access(request.user, location): raise PermissionDenied() old_location = loc_mapper().translate_locator_to_location(location) course_module = modulestore().get_item(old_location) # an _accept URL parameter will be preferred over HTTP_ACCEPT in the header. requested_format = request.REQUEST.get('_accept', request.META.get('HTTP_ACCEPT', 'text/html')) export_url = location.url_reverse('export') + '?_accept=application/x-tgz' if 'application/x-tgz' in requested_format: name = old_location.name export_file = NamedTemporaryFile(prefix=name + '.', suffix=".tar.gz") root_dir = path(mkdtemp()) try: export_to_xml(modulestore('direct'), contentstore(), old_location, root_dir, name, modulestore()) logging.debug('tar file being generated at {0}'.format(export_file.name)) with tarfile.open(name=export_file.name, mode='w:gz') as tar_file: tar_file.add(root_dir / name, arcname=name) except SerializationError, e: logging.exception('There was an error exporting course {0}. {1}'.format(course_module.location, unicode(e))) unit = None failed_item = None parent = None try: failed_item = modulestore().get_instance(course_module.location.course_id, e.location) parent_locs = modulestore().get_parent_locations(failed_item.location, course_module.location.course_id) if len(parent_locs) > 0: parent = modulestore().get_item(parent_locs[0]) if parent.location.category == 'vertical': unit = parent except: # if we have a nested exception, then we'll show the more generic error message pass unit_locator = loc_mapper().translate_location(old_location.course_id, parent.location, False, True) return render_to_response('export.html', { 'context_course': course_module, 'in_err': True, 'raw_err_msg': str(e), 'failed_module': failed_item, 'unit': unit, 'edit_unit_url': unit_locator.url_reverse("unit") if parent else "", 'course_home_url': location.url_reverse("course"), 'export_url': export_url }) except Exception, e: logging.exception('There was an error exporting course {0}. {1}'.format(course_module.location, unicode(e))) return render_to_response('export.html', { 'context_course': course_module, 'in_err': True, 'unit': None, 'raw_err_msg': str(e), 'course_home_url': location.url_reverse("course"), 'export_url': export_url })
def checklists_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None, checklist_index=None): """ The restful handler for checklists. GET html: return html page for all checklists json: return json representing all checklists. checklist_index is not supported for GET at this time. POST or PUT json: updates the checked state for items within a particular checklist. checklist_index is required. """ location = BlockUsageLocator(package_id=package_id, branch=branch, version_guid=version_guid, block_id=block) if not has_course_access(request.user, location): raise PermissionDenied() old_location = loc_mapper().translate_locator_to_location(location) modulestore = get_modulestore(old_location) course_module = modulestore.get_item(old_location) json_request = 'application/json' in request.META.get( 'HTTP_ACCEPT', 'application/json') if request.method == 'GET': # If course was created before checklists were introduced, copy them over # from the template. if not course_module.checklists: course_module.checklists = CourseDescriptor.checklists.default modulestore.update_item(course_module, request.user.id) expanded_checklists = expand_all_action_urls(course_module) if json_request: return JsonResponse(expanded_checklists[0:2]) else: handler_url = location.url_reverse('checklists/', '') return render_to_response( 'checklists.html', { 'handler_url': handler_url, # context_course is used by analytics 'context_course': course_module, 'checklists': expanded_checklists[0:2] }) elif json_request: # Can now assume POST or PUT because GET handled above. if checklist_index is not None and 0 <= int(checklist_index) < len( course_module.checklists): index = int(checklist_index) persisted_checklist = course_module.checklists[index] modified_checklist = json.loads(request.body) # Only thing the user can modify is the "checked" state. # We don't want to persist what comes back from the client because it will # include the expanded action URLs (which are non-portable). for item_index, item in enumerate(modified_checklist.get('items')): persisted_checklist['items'][item_index]['is_checked'] = item[ 'is_checked'] # seeming noop which triggers kvs to record that the metadata is # not default course_module.checklists = course_module.checklists course_module.save() modulestore.update_item(course_module, request.user.id) expanded_checklist = expand_checklist_action_url( course_module, persisted_checklist) return JsonResponse(expanded_checklist) else: return HttpResponseBadRequest( ("Could not save checklist state because the checklist index " "was out of range or unspecified."), content_type="text/plain") else: return HttpResponseNotFound()
def checklists_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None, checklist_index=None): """ The restful handler for checklists. GET html: return html page for all checklists json: return json representing all checklists. checklist_index is not supported for GET at this time. POST or PUT json: updates the checked state for items within a particular checklist. checklist_index is required. """ location = BlockUsageLocator(package_id=package_id, branch=branch, version_guid=version_guid, block_id=block) if not has_course_access(request.user, location): raise PermissionDenied() old_location = loc_mapper().translate_locator_to_location(location) modulestore = get_modulestore(old_location) course_module = modulestore.get_item(old_location) json_request = 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json') if request.method == 'GET': # If course was created before checklists were introduced, copy them over # from the template. if not course_module.checklists: course_module.checklists = CourseDescriptor.checklists.default modulestore.update_item(course_module, request.user.id) expanded_checklists = expand_all_action_urls(course_module) if json_request: return JsonResponse(expanded_checklists) else: handler_url = location.url_reverse('checklists/', '') return render_to_response('checklists.html', { 'handler_url': handler_url, # context_course is used by analytics 'context_course': course_module, 'checklists': expanded_checklists }) elif json_request: # Can now assume POST or PUT because GET handled above. if checklist_index is not None and 0 <= int(checklist_index) < len(course_module.checklists): index = int(checklist_index) persisted_checklist = course_module.checklists[index] modified_checklist = json.loads(request.body) # Only thing the user can modify is the "checked" state. # We don't want to persist what comes back from the client because it will # include the expanded action URLs (which are non-portable). for item_index, item in enumerate(modified_checklist.get('items')): persisted_checklist['items'][item_index]['is_checked'] = item['is_checked'] # seeming noop which triggers kvs to record that the metadata is # not default course_module.checklists = course_module.checklists course_module.save() modulestore.update_item(course_module, request.user.id) expanded_checklist = expand_checklist_action_url(course_module, persisted_checklist) return JsonResponse(expanded_checklist) else: return HttpResponseBadRequest( ("Could not save checklist state because the checklist index " "was out of range or unspecified."), content_type="text/plain" ) else: return HttpResponseNotFound()
def export_handler(request, tag=None, course_id=None, branch=None, version_guid=None, block=None): """ The restful handler for exporting a course. GET html: return html page for import page application/x-tgz: return tar.gz file containing exported course json: not supported Note that there are 2 ways to request the tar.gz file. The request header can specify application/x-tgz via HTTP_ACCEPT, or a query parameter can be used (?_accept=application/x-tgz). If the tar.gz file has been requested but the export operation fails, an HTML page will be returned which describes the error. """ location = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block) if not has_access(request.user, location): raise PermissionDenied() old_location = loc_mapper().translate_locator_to_location(location) course_module = modulestore().get_item(old_location) # an _accept URL parameter will be preferred over HTTP_ACCEPT in the header. requested_format = request.REQUEST.get("_accept", request.META.get("HTTP_ACCEPT", "text/html")) export_url = location.url_reverse("export") + "?_accept=application/x-tgz" if "application/x-tgz" in requested_format: name = old_location.name export_file = NamedTemporaryFile(prefix=name + ".", suffix=".tar.gz") root_dir = path(mkdtemp()) try: export_to_xml(modulestore("direct"), contentstore(), old_location, root_dir, name, modulestore()) except SerializationError, e: logging.exception("There was an error exporting course {0}. {1}".format(course_module.location, unicode(e))) unit = None failed_item = None parent = None try: failed_item = modulestore().get_instance(course_module.location.course_id, e.location) parent_locs = modulestore().get_parent_locations(failed_item.location, course_module.location.course_id) if len(parent_locs) > 0: parent = modulestore().get_item(parent_locs[0]) if parent.location.category == "vertical": unit = parent except: # if we have a nested exception, then we'll show the more generic error message pass unit_locator = loc_mapper().translate_location(old_location.course_id, parent.location, False, True) return render_to_response( "export.html", { "context_course": course_module, "in_err": True, "raw_err_msg": str(e), "failed_module": failed_item, "unit": unit, "edit_unit_url": unit_locator.url_reverse("unit") if parent else "", "course_home_url": location.url_reverse("course"), "export_url": export_url, }, ) except Exception, e: logging.exception("There was an error exporting course {0}. {1}".format(course_module.location, unicode(e))) return render_to_response( "export.html", { "context_course": course_module, "in_err": True, "unit": None, "raw_err_msg": str(e), "course_home_url": location.url_reverse("course"), "export_url": export_url, }, )