def test_course_constructor_bad_package_id(self, bad_id): """ Test all sorts of badly-formed package_ids (and urls with those package_ids) """ with self.assertRaises(InvalidKeyError): CourseLocator(org=bad_id, offering='test') with self.assertRaises(InvalidKeyError): CourseLocator(org='test', offering=bad_id) with self.assertRaises(InvalidKeyError): CourseKey.from_string('course-locator:test+{}'.format(bad_id))
def course_handler(request, course_key_string=None): """ The restful handler for course specific requests. It provides the course tree with the necessary information for identifying and labeling the parts. The root will typically be a 'course' object but may not be especially as we support modules. GET html: return course listing page if not given a course id html: return html page overview for the given course if given a course id json: return json representing the course branch's index entry as well as dag w/ all of the children replaced w/ json docs where each doc has {'_id': , 'display_name': , 'children': } POST json: create a course, return resulting json descriptor (same as in GET course/...). Leaving off /branch/draft would imply create the course w/ default branches. Cannot change the structure contents ('_id', 'display_name', 'children') but can change the index entry. PUT json: update this course (index entry not xblock) such as repointing head, changing display name, org, offering. Return same json as above. DELETE json: delete this branch from this course (leaving off /branch/draft would imply delete the course) """ response_format = request.REQUEST.get('format', 'html') if response_format == 'json' or 'application/json' in request.META.get( 'HTTP_ACCEPT', 'application/json'): if request.method == 'GET': return JsonResponse( _course_json(request, CourseKey.from_string(course_key_string))) elif request.method == 'POST': # not sure if this is only post. If one will have ids, it goes after access return create_new_course(request) elif not has_course_access(request.user, CourseKey.from_string(course_key_string)): raise PermissionDenied() elif request.method == 'PUT': raise NotImplementedError() elif request.method == 'DELETE': raise NotImplementedError() else: return HttpResponseBadRequest() elif request.method == 'GET': # assume html if course_key_string is None: return course_listing(request) else: return course_index(request, CourseKey.from_string(course_key_string)) else: return HttpResponseNotFound()
def clean_course_id(self): """Validate the course id""" cleaned_id = self.cleaned_data["course_id"] try: course_key = CourseKey.from_string(cleaned_id) except InvalidKeyError: try: course_key = SlashSeparatedCourseKey.from_deprecated_string(cleaned_id) except InvalidKeyError: msg = u'Course id invalid.' msg += u' --- Entered course id was: "{0}". '.format(cleaned_id) msg += 'Please recheck that you have supplied a valid course id.' raise forms.ValidationError(msg) if not modulestore().has_course(course_key): msg = u'COURSE NOT FOUND' msg += u' --- Entered course id was: "{0}". '.format(course_key.to_deprecated_string()) msg += 'Please recheck that you have supplied a valid course id.' raise forms.ValidationError(msg) # Now, try and discern if it is a Studio course - HTML editor doesn't work with XML courses is_studio_course = modulestore().get_modulestore_type(course_key) != XML_MODULESTORE_TYPE if not is_studio_course: msg = "Course Email feature is only available for courses authored in Studio. " msg += '"{0}" appears to be an XML backed course.'.format(course_key.to_deprecated_string()) raise forms.ValidationError(msg) return course_key
def clean_course_id(self): """Validate the course id""" cleaned_id = self.cleaned_data["course_id"] try: course_key = CourseKey.from_string(cleaned_id) except InvalidKeyError: try: course_key = SlashSeparatedCourseKey.from_deprecated_string( cleaned_id) except InvalidKeyError: msg = u'Course id invalid.' msg += u' --- Entered course id was: "{0}". '.format( cleaned_id) msg += 'Please recheck that you have supplied a valid course id.' raise forms.ValidationError(msg) if not modulestore().has_course(course_key): msg = u'COURSE NOT FOUND' msg += u' --- Entered course id was: "{0}". '.format( course_key.to_deprecated_string()) msg += 'Please recheck that you have supplied a valid course id.' raise forms.ValidationError(msg) # Now, try and discern if it is a Studio course - HTML editor doesn't work with XML courses is_studio_course = modulestore().get_modulestore_type( course_key) != XML_MODULESTORE_TYPE if not is_studio_course: msg = "Course Email feature is only available for courses authored in Studio. " msg += '"{0}" appears to be an XML backed course.'.format( course_key.to_deprecated_string()) raise forms.ValidationError(msg) return course_key
def export_git(request, course_key_string): """ This method serves up the 'Export to Git' page """ course_key = CourseKey.from_string(course_key_string) if not has_course_access(request.user, course_key): raise PermissionDenied() course_module = modulestore().get_course(course_key) failed = False log.debug('export_git course_module=%s', course_module) msg = "" if 'action' in request.GET and course_module.giturl: if request.GET['action'] == 'push': try: git_export_utils.export_to_git( course_module.id, course_module.giturl, request.user, ) msg = _('Course successfully exported to git repository') except git_export_utils.GitExportError as ex: failed = True msg = unicode(ex) return render_to_response('export_git.html', { 'context_course': course_module, 'msg': msg, 'failed': failed, })
def orphan_handler(request, course_key_string): """ View for handling orphan related requests. GET gets all of the current orphans. DELETE removes all orphans (requires is_staff access) An orphan is a block whose category is not in the DETACHED_CATEGORY list, is not the root, and is not reachable from the root via children """ course_usage_key = CourseKey.from_string(course_key_string) if request.method == 'GET': if has_course_access(request.user, course_usage_key): return JsonResponse(modulestore().get_orphans(course_usage_key)) else: raise PermissionDenied() if request.method == 'DELETE': if request.user.is_staff: items = modulestore().get_orphans(course_usage_key) for itemloc in items: # get_orphans returns the deprecated string format usage_key = course_usage_key.make_usage_key_from_deprecated_string( itemloc) modulestore('draft').delete_item(usage_key, delete_all_versions=True) return JsonResponse({'deleted': items}) else: raise PermissionDenied()
def parse_args(self, *args): """ Return a 4-tuple of (course_key, user, org, offering). If the user didn't specify an org & offering, those will be None. """ if len(args) < 2: raise CommandError( "migrate_to_split requires at least two arguments: " "a course_key and a user identifier (email or ID)" ) try: course_key = CourseKey.from_string(args[0]) except InvalidKeyError: course_key = SlashSeparatedCourseKey.from_deprecated_string(args[0]) try: user = user_from_str(args[1]) except User.DoesNotExist: raise CommandError("No user found identified by {}".format(args[1])) try: org = args[2] offering = args[3] except IndexError: org = offering = None return course_key, user, org, offering
def handle(self, *args, **options): if not options["course"]: raise CommandError(Command.course_option.help) try: course_key = CourseKey.from_string(options["course"]) except InvalidKeyError: course_key = SlashSeparatedCourseKey.from_deprecated_string(options["course"]) course = get_course_by_id(course_key) print "Warning: this command directly edits the list of course tabs in mongo." print "Tabs before any changes:" print_course(course) try: if options["delete"]: if len(args) != 1: raise CommandError(Command.delete_option.help) num = int(args[0]) if query_yes_no("Deleting tab {0} Confirm?".format(num), default="no"): tabs.primitive_delete(course, num - 1) # -1 for 0-based indexing elif options["insert"]: if len(args) != 3: raise CommandError(Command.insert_option.help) num = int(args[0]) tab_type = args[1] name = args[2] if query_yes_no('Inserting tab {0} "{1}" "{2}" Confirm?'.format(num, tab_type, name), default="no"): tabs.primitive_insert(course, num - 1, tab_type, name) # -1 as above except ValueError as e: # Cute: translate to CommandError so the CLI error prints nicely. raise CommandError(e)
def handle(self, *args, **options): """ Checks arguments and runs export function if they are good """ if len(args) != 2: raise CommandError('This script requires exactly two arguments: ' 'course_loc and git_url') # Rethrow GitExportError as CommandError for SystemExit try: course_key = CourseKey.from_string(args[0]) except InvalidKeyError: try: course_key = SlashSeparatedCourseKey.from_deprecated_string(args[0]) except InvalidKeyError: raise CommandError(GitExportError.BAD_COURSE) try: git_export_utils.export_to_git( course_key, args[1], options.get('user', ''), options.get('rdir', None) ) except git_export_utils.GitExportError as ex: raise CommandError(str(ex))
def handle(self, *args, **options): """ Checks arguments and runs export function if they are good """ if len(args) != 2: raise CommandError('This script requires exactly two arguments: ' 'course_loc and git_url') # Rethrow GitExportError as CommandError for SystemExit try: course_key = CourseKey.from_string(args[0]) except InvalidKeyError: try: course_key = SlashSeparatedCourseKey.from_deprecated_string( args[0]) except InvalidKeyError: raise CommandError(GitExportError.BAD_COURSE) try: git_export_utils.export_to_git(course_key, args[1], options.get('user', ''), options.get('rdir', None)) except git_export_utils.GitExportError as ex: raise CommandError(str(ex))
def handle(self, *args, **options): print "args = ", args if len(args) > 0: course_id = args[0] else: print self.help return course_key = None # parse out the course id into a coursekey try: course_key = CourseKey.from_string(course_id) # if it's not a new-style course key, parse it from an old-style # course key except InvalidKeyError: course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) try: course = get_course_by_id(course_key) except Exception as err: print "-----------------------------------------------------------------------------" print "Sorry, cannot find course with id {}".format(course_id) print "Got exception {}".format(err) print "Please provide a course ID or course data directory name, eg content-mit-801rq" return print "-----------------------------------------------------------------------------" print "Computing grades for {}".format(course_id) offline_grade_calculation(course_key)
def handle(self, *args, **options): if not options['course_id']: raise CommandError("You must specify a course id for this command") if not options['from_mode'] or not options['to_mode']: raise CommandError('You must specify a "to" and "from" mode as parameters') try: course_key = CourseKey.from_string(options['course_id']) except InvalidKeyError: course_key = SlashSeparatedCourseKey.from_deprecated_string(options['course_id']) filter_args = dict( course_id=course_key, mode=options['from_mode'] ) if options['user']: if '@' in options['user']: user = User.objects.get(email=options['user']) else: user = User.objects.get(username=options['user']) filter_args['user'] = user enrollments = CourseEnrollment.objects.filter(**filter_args) if options['noop']: print "Would have changed {num_enrollments} students from {from_mode} to {to_mode}".format( num_enrollments=enrollments.count(), from_mode=options['from_mode'], to_mode=options['to_mode'] ) else: for enrollment in enrollments: enrollment.update_enrollment(mode=options['to_mode']) enrollment.save()
def handle(self, *args, **options): print "args = ", args if len(args) > 0: course_id = args[0] else: print self.help return course_key = None # parse out the course id into a coursekey try: course_key = CourseKey.from_string(course_id) # if it's not a new-style course key, parse it from an old-style # course key except InvalidKeyError: course_key = SlashSeparatedCourseKey.from_deprecated_string( course_id) try: _course = get_course_by_id(course_key) except Exception as err: print "-----------------------------------------------------------------------------" print "Sorry, cannot find course with id {}".format(course_id) print "Got exception {}".format(err) print "Please provide a course ID or course data directory name, eg content-mit-801rq" return print "-----------------------------------------------------------------------------" print "Computing grades for {}".format(course_id) offline_grade_calculation(course_key)
def course_key_from_arg(self, arg): """ Convert the command line arg into a course key """ try: return CourseKey.from_string(arg) except InvalidKeyError: return SlashSeparatedCourseKey.from_deprecated_string(arg)
def handle(self, *args, **options): # current grading logic and data schema doesn't handle dates # datetime.strptime("21/11/06 16:30", "%m/%d/%y %H:%M") print "args = ", args course_id = 'MITx/8.01rq_MW/Classical_Mechanics_Reading_Questions_Fall_2012_MW_Section' fn = "grades.csv" get_raw_scores = False if len(args) > 0: course_id = args[0] if len(args) > 1: fn = args[1] if len(args) > 2: get_raw_scores = args[2].lower() == 'raw' request = DummyRequest() # parse out the course into a coursekey try: course_key = CourseKey.from_string(course_id) # if it's not a new-style course key, parse it from an old-style # course key except InvalidKeyError: course_key = SlashSeparatedCourseKey.from_deprecated_string( course_id) try: course = get_course_by_id(course_key) # Ok with catching general exception here because this is run as a management command # and the exception is exposed right away to the user. except Exception as err: # pylint: disable=broad-except print "-----------------------------------------------------------------------------" print "Sorry, cannot find course with id {}".format(course_id) print "Got exception {}".format(err) print "Please provide a course ID or course data directory name, eg content-mit-801rq" return print "-----------------------------------------------------------------------------" print "Dumping grades from {} to file {} (get_raw_scores={})".format( course.id, fn, get_raw_scores) datatable = get_student_grade_summary_data( request, course, get_raw_scores=get_raw_scores) fp = open(fn, 'w') writer = csv.writer(fp, dialect='excel', quotechar='"', quoting=csv.QUOTE_ALL) writer.writerow(datatable['header']) for datarow in datatable['data']: encoded_row = [unicode(s).encode('utf-8') for s in datarow] writer.writerow(encoded_row) fp.close() print "Done: {} records dumped".format(len(datatable['data']))
def tabs_handler(request, course_key_string): """ The restful handler for static tabs. GET html: return page for editing static tabs json: not supported PUT or POST json: update the tab order. It is expected that the request body contains a JSON-encoded dict with entry "tabs". The value for "tabs" is an array of tab locators, indicating the desired order of the tabs. Creating a tab, deleting a tab, or changing its contents is not supported through this method. Instead use the general xblock URL (see item.xblock_handler). """ course_key = CourseKey.from_string(course_key_string) if not has_course_access(request.user, course_key): raise PermissionDenied() course_item = modulestore().get_course(course_key) if 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'): if request.method == 'GET': raise NotImplementedError('coming soon') else: if 'tabs' in request.json: return reorder_tabs_handler(course_item, request) elif 'tab_id_locator' in request.json: return edit_tab_handler(course_item, request) else: raise NotImplementedError( 'Creating or changing tab content is not supported.') elif request.method == 'GET': # assume html # get all tabs from the tabs list: static tabs (a.k.a. user-created tabs) and built-in tabs # present in the same order they are displayed in LMS tabs_to_render = [] for tab in CourseTabList.iterate_displayable_cms( course_item, settings, ): if isinstance(tab, StaticTab): # static tab needs its locator information to render itself as an xmodule static_tab_loc = course_key.make_usage_key( 'static_tab', tab.url_slug) tab.locator = static_tab_loc tabs_to_render.append(tab) return render_to_response( 'edit-tabs.html', { 'context_course': course_item, 'tabs_to_render': tabs_to_render, 'lms_link': get_lms_link_for_item(course_item.location), }) else: return HttpResponseNotFound()
def test_course_constructor_url_package_id_and_version_guid(self): test_id_loc = '519665f6223ebd6980884f2b' testobj = CourseKey.from_string( 'course-locator:mit.eecs+honors.6002x+{}+{}'.format( CourseLocator.VERSION_PREFIX, test_id_loc)) self.check_course_locn_fields(testobj, org='mit.eecs', offering='honors.6002x', version_guid=ObjectId(test_id_loc))
def test_course_constructor_url(self): # Test parsing a url when it starts with a version ID and there is also a block ID. # This hits the parsers parse_guid method. test_id_loc = '519665f6223ebd6980884f2b' testobj = CourseKey.from_string("course-locator:{}+{}+{}+hw3".format( CourseLocator.VERSION_PREFIX, test_id_loc, CourseLocator.BLOCK_PREFIX)) self.check_course_locn_fields(testobj, version_guid=ObjectId(test_id_loc))
def course_handler(request, course_key_string=None): """ The restful handler for course specific requests. It provides the course tree with the necessary information for identifying and labeling the parts. The root will typically be a 'course' object but may not be especially as we support modules. GET html: return course listing page if not given a course id html: return html page overview for the given course if given a course id json: return json representing the course branch's index entry as well as dag w/ all of the children replaced w/ json docs where each doc has {'_id': , 'display_name': , 'children': } POST json: create a course, return resulting json descriptor (same as in GET course/...). Leaving off /branch/draft would imply create the course w/ default branches. Cannot change the structure contents ('_id', 'display_name', 'children') but can change the index entry. PUT json: update this course (index entry not xblock) such as repointing head, changing display name, org, offering. Return same json as above. DELETE json: delete this branch from this course (leaving off /branch/draft would imply delete the course) """ response_format = request.REQUEST.get("format", "html") if response_format == "json" or "application/json" in request.META.get("HTTP_ACCEPT", "application/json"): if request.method == "GET": return JsonResponse(_course_json(request, CourseKey.from_string(course_key_string))) elif request.method == "POST": # not sure if this is only post. If one will have ids, it goes after access return create_new_course(request) elif not has_course_access(request.user, CourseKey.from_string(course_key_string)): raise PermissionDenied() elif request.method == "PUT": raise NotImplementedError() elif request.method == "DELETE": raise NotImplementedError() else: return HttpResponseBadRequest() elif request.method == "GET": # assume html if course_key_string is None: return course_listing(request) else: return course_index(request, CourseKey.from_string(course_key_string)) else: return HttpResponseNotFound()
def test_course_constructor_url_package_id_and_version_guid(self): test_id_loc = '519665f6223ebd6980884f2b' testobj = CourseKey.from_string( 'course-locator:mit.eecs+honors.6002x+{}+{}'.format(CourseLocator.VERSION_PREFIX, test_id_loc) ) self.check_course_locn_fields( testobj, org='mit.eecs', offering='honors.6002x', version_guid=ObjectId(test_id_loc) )
def test_course_constructor_url(self): # Test parsing a url when it starts with a version ID and there is also a block ID. # This hits the parsers parse_guid method. test_id_loc = '519665f6223ebd6980884f2b' testobj = CourseKey.from_string("course-locator:{}+{}+{}+hw3".format( CourseLocator.VERSION_PREFIX, test_id_loc, CourseLocator.BLOCK_PREFIX )) self.check_course_locn_fields( testobj, version_guid=ObjectId(test_id_loc) )
def textbooks_detail_handler(request, course_key_string, textbook_id): """ JSON API endpoint for manipulating a textbook via its internal ID. Used by the Backbone application. GET json: return JSON representation of textbook POST or PUT json: update textbook based on provided information DELETE json: remove textbook """ course_key = CourseKey.from_string(course_key_string) course_module = _get_course_module(course_key, request.user) store = get_modulestore(course_module.location) matching_id = [ tb for tb in course_module.pdf_textbooks if unicode(tb.get("id")) == unicode(textbook_id) ] if matching_id: textbook = matching_id[0] else: textbook = None if request.method == 'GET': if not textbook: return JsonResponse(status=404) return JsonResponse(textbook) elif request.method in ('POST', 'PUT'): # can be either and sometimes # django is rewriting one to the other try: new_textbook = validate_textbook_json(request.body) except TextbookValidationError as err: return JsonResponse({"error": err.message}, status=400) new_textbook["id"] = textbook_id if textbook: i = course_module.pdf_textbooks.index(textbook) new_textbooks = course_module.pdf_textbooks[0:i] new_textbooks.append(new_textbook) new_textbooks.extend(course_module.pdf_textbooks[i + 1:]) course_module.pdf_textbooks = new_textbooks else: course_module.pdf_textbooks.append(new_textbook) store.update_item(course_module, request.user.id) return JsonResponse(new_textbook, status=201) elif request.method == 'DELETE': if not textbook: return JsonResponse(status=404) i = course_module.pdf_textbooks.index(textbook) remaining_textbooks = course_module.pdf_textbooks[0:i] remaining_textbooks.extend(course_module.pdf_textbooks[i + 1:]) course_module.pdf_textbooks = remaining_textbooks store.update_item(course_module, request.user.id) return JsonResponse()
def handle(self, *args, **options): if len(args) != 1: raise CommandError("check_course requires one argument: <course_id>") try: course_key = CourseKey.from_string(args[0]) except InvalidKeyError: course_key = SlashSeparatedCourseKey.from_deprecated_string(args[0]) store = modulestore() course = store.get_course(course_key, depth=3) err_cnt = 0 def _xlint_metadata(module): err_cnt = check_module_metadata_editability(module) for child in module.get_children(): err_cnt = err_cnt + _xlint_metadata(child) return err_cnt err_cnt = err_cnt + _xlint_metadata(course) # we've had a bug where the xml_attributes field can we rewritten as a string rather than a dict def _check_xml_attributes_field(module): err_cnt = 0 if hasattr(module, 'xml_attributes') and isinstance(module.xml_attributes, basestring): print 'module = {0} has xml_attributes as a string. It should be a dict'.format(module.location) err_cnt = err_cnt + 1 for child in module.get_children(): err_cnt = err_cnt + _check_xml_attributes_field(child) return err_cnt err_cnt = err_cnt + _check_xml_attributes_field(course) # check for dangling discussion items, this can cause errors in the forums def _get_discussion_items(module): discussion_items = [] if module.location.category == 'discussion': discussion_items = discussion_items + [module.location] for child in module.get_children(): discussion_items = discussion_items + _get_discussion_items(child) return discussion_items discussion_items = _get_discussion_items(course) # now query all discussion items via get_items() and compare with the tree-traversal queried_discussion_items = store.get_items(course_key=course_key, category='discussion',) for item in queried_discussion_items: if item.location not in discussion_items: print 'Found dangling discussion module = {0}'.format(item.location)
def settings_handler(request, course_key_string): """ 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 """ course_key = CourseKey.from_string(course_key_string) course_module = _get_course_module(course_key, request.user) if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET': upload_asset_url = reverse_course_url('assets_handler', course_key) # see if the ORG of this course can be attributed to a 'Microsite'. In that case, the # course about page should be editable in Studio about_page_editable = not microsite.get_value_for_org( course_module.location.org, 'ENABLE_MKTG_SITE', settings.FEATURES.get('ENABLE_MKTG_SITE', False)) short_description_editable = settings.FEATURES.get( 'EDITABLE_SHORT_DESCRIPTION', True) return render_to_response( 'settings.html', { 'context_course': course_module, 'course_locator': course_key, 'lms_link_for_about_page': utils.get_lms_link_for_about_page(course_key), 'course_image_url': utils.course_image_url(course_module), 'details_url': reverse_course_url('settings_handler', course_key), 'about_page_editable': about_page_editable, 'short_description_editable': short_description_editable, 'upload_asset_url': upload_asset_url }) elif 'application/json' in request.META.get('HTTP_ACCEPT', ''): if request.method == 'GET': return JsonResponse( CourseDetails.fetch(course_key), # encoder serializes dates, old locations, and instances encoder=CourseSettingsEncoder) else: # post or put, doesn't matter. return JsonResponse(CourseDetails.update_from_json( course_key, request.json, request.user), encoder=CourseSettingsEncoder)
def grading_handler(request, course_key_string, 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) """ course_key = CourseKey.from_string(course_key_string) course_module = _get_course_module(course_key, request.user) if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET': course_details = CourseGradingModel.fetch(course_key) return render_to_response( 'settings_graders.html', { 'context_course': course_module, 'course_locator': course_key, 'course_details': json.dumps(course_details, cls=CourseSettingsEncoder), 'grading_url': reverse_course_url('grading_handler', course_key), }) elif 'application/json' in request.META.get('HTTP_ACCEPT', ''): if request.method == 'GET': if grader_index is None: return JsonResponse( CourseGradingModel.fetch(course_key), # encoder serializes dates, old locations, and instances encoder=CourseSettingsEncoder) else: return JsonResponse( CourseGradingModel.fetch_grader(course_key, 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( course_key, request.json, request.user), encoder=CourseSettingsEncoder) else: return JsonResponse( CourseGradingModel.update_grader_from_json( course_key, request.json, request.user)) elif request.method == "DELETE" and grader_index is not None: CourseGradingModel.delete_grader(course_key, grader_index, request.user) return JsonResponse()
def tabs_handler(request, course_key_string): """ The restful handler for static tabs. GET html: return page for editing static tabs json: not supported PUT or POST json: update the tab order. It is expected that the request body contains a JSON-encoded dict with entry "tabs". The value for "tabs" is an array of tab locators, indicating the desired order of the tabs. Creating a tab, deleting a tab, or changing its contents is not supported through this method. Instead use the general xblock URL (see item.xblock_handler). """ course_key = CourseKey.from_string(course_key_string) if not has_course_access(request.user, course_key): raise PermissionDenied() course_item = modulestore().get_course(course_key) if 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'): if request.method == 'GET': raise NotImplementedError('coming soon') else: if 'tabs' in request.json: return reorder_tabs_handler(course_item, request) elif 'tab_id_locator' in request.json: return edit_tab_handler(course_item, request) else: raise NotImplementedError('Creating or changing tab content is not supported.') elif request.method == 'GET': # assume html # get all tabs from the tabs list: static tabs (a.k.a. user-created tabs) and built-in tabs # present in the same order they are displayed in LMS tabs_to_render = [] for tab in CourseTabList.iterate_displayable_cms( course_item, settings, ): if isinstance(tab, StaticTab): # static tab needs its locator information to render itself as an xmodule static_tab_loc = course_key.make_usage_key('static_tab', tab.url_slug) tab.locator = static_tab_loc tabs_to_render.append(tab) return render_to_response('edit-tabs.html', { 'context_course': course_item, 'tabs_to_render': tabs_to_render, 'lms_link': get_lms_link_for_item(course_item.location), }) else: return HttpResponseNotFound()
def textbooks_detail_handler(request, course_key_string, textbook_id): """ JSON API endpoint for manipulating a textbook via its internal ID. Used by the Backbone application. GET json: return JSON representation of textbook POST or PUT json: update textbook based on provided information DELETE json: remove textbook """ course_key = CourseKey.from_string(course_key_string) course_module = _get_course_module(course_key, request.user) store = get_modulestore(course_module.location) matching_id = [tb for tb in course_module.pdf_textbooks if unicode(tb.get("id")) == unicode(textbook_id)] if matching_id: textbook = matching_id[0] else: textbook = None if request.method == 'GET': if not textbook: return JsonResponse(status=404) return JsonResponse(textbook) elif request.method in ('POST', 'PUT'): # can be either and sometimes # django is rewriting one to the other try: new_textbook = validate_textbook_json(request.body) except TextbookValidationError as err: return JsonResponse({"error": err.message}, status=400) new_textbook["id"] = textbook_id if textbook: i = course_module.pdf_textbooks.index(textbook) new_textbooks = course_module.pdf_textbooks[0:i] new_textbooks.append(new_textbook) new_textbooks.extend(course_module.pdf_textbooks[i + 1:]) course_module.pdf_textbooks = new_textbooks else: course_module.pdf_textbooks.append(new_textbook) store.update_item(course_module, request.user.id) return JsonResponse(new_textbook, status=201) elif request.method == 'DELETE': if not textbook: return JsonResponse(status=404) i = course_module.pdf_textbooks.index(textbook) remaining_textbooks = course_module.pdf_textbooks[0:i] remaining_textbooks.extend(course_module.pdf_textbooks[i + 1:]) course_module.pdf_textbooks = remaining_textbooks store.update_item(course_module, request.user.id) return JsonResponse()
def handle(self, *args, **options): # current grading logic and data schema doesn't handle dates # datetime.strptime("21/11/06 16:30", "%m/%d/%y %H:%M") print "args = ", args course_id = 'MITx/8.01rq_MW/Classical_Mechanics_Reading_Questions_Fall_2012_MW_Section' fn = "grades.csv" get_raw_scores = False if len(args) > 0: course_id = args[0] if len(args) > 1: fn = args[1] if len(args) > 2: get_raw_scores = args[2].lower() == 'raw' request = DummyRequest() # parse out the course into a coursekey try: course_key = CourseKey.from_string(course_id) # if it's not a new-style course key, parse it from an old-style # course key except InvalidKeyError: course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) try: course = get_course_by_id(course_key) # Ok with catching general exception here because this is run as a management command # and the exception is exposed right away to the user. except Exception as err: # pylint: disable=broad-except print "-----------------------------------------------------------------------------" print "Sorry, cannot find course with id {}".format(course_id) print "Got exception {}".format(err) print "Please provide a course ID or course data directory name, eg content-mit-801rq" return print "-----------------------------------------------------------------------------" print "Dumping grades from {} to file {} (get_raw_scores={})".format(course.id, fn, get_raw_scores) datatable = get_student_grade_summary_data(request, course, get_raw_scores=get_raw_scores) fp = open(fn, 'w') writer = csv.writer(fp, dialect='excel', quotechar='"', quoting=csv.QUOTE_ALL) writer.writerow(datatable['header']) for datarow in datatable['data']: encoded_row = [unicode(s).encode('utf-8') for s in datarow] writer.writerow(encoded_row) fp.close() print "Done: {} records dumped".format(len(datatable['data']))
def test_course_constructor_url_package_id_branch_and_version_guid(self): test_id_loc = '519665f6223ebd6980884f2b' org = 'mit.eecs' offering = '~6002x' testobj = CourseKey.from_string( 'course-locator:{}+{}+{}+draft-1+{}+{}'.format( org, offering, CourseLocator.BRANCH_PREFIX, CourseLocator.VERSION_PREFIX, test_id_loc)) self.check_course_locn_fields(testobj, org=org, offering=offering, branch='draft-1', version_guid=ObjectId(test_id_loc))
def test_course_constructor_url_package_id_branch_and_version_guid(self): test_id_loc = '519665f6223ebd6980884f2b' org = 'mit.eecs' offering = '~6002x' testobj = CourseKey.from_string('course-locator:{}+{}+{}+draft-1+{}+{}'.format( org, offering, CourseLocator.BRANCH_PREFIX, CourseLocator.VERSION_PREFIX, test_id_loc )) self.check_course_locn_fields( testobj, org=org, offering=offering, branch='draft-1', version_guid=ObjectId(test_id_loc) )
def clean_course_id(self): course_id = self.cleaned_data['course_id'] try: course_key = CourseKey.from_string(course_id) except InvalidKeyError: try: course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) except InvalidKeyError: raise forms.ValidationError("Cannot make a valid CourseKey from id {}!".format(course_id)) if not modulestore().has_course(course_key): raise forms.ValidationError("Cannot find course with id {} in the modulestore".format(course_id)) return course_key
def course_info_update_handler(request, course_key_string, provided_id=None): """ restful CRUD operations on course_info updates. provided_id should be none if it's new (create) and index otherwise. GET json: return the course info update models POST json: create an update PUT or DELETE json: change an existing update """ if 'application/json' not in request.META.get('HTTP_ACCEPT', 'application/json'): return HttpResponseBadRequest("Only supports json requests") course_key = CourseKey.from_string(course_key_string) usage_key = course_key.make_usage_key('course_info', 'updates') if provided_id == '': provided_id = None # check that logged in user has permissions to this item (GET shouldn't require this level?) if not has_course_access(request.user, usage_key.course_key): raise PermissionDenied() if request.method == 'GET': course_updates = get_course_updates(usage_key, provided_id) if isinstance(course_updates, dict) and course_updates.get('error'): return JsonResponse(course_updates, course_updates.get('status', 400)) else: return JsonResponse(course_updates) elif request.method == 'DELETE': try: return JsonResponse( delete_course_update(usage_key, request.json, provided_id, request.user)) except: return HttpResponseBadRequest("Failed to delete", content_type="text/plain") # can be either and sometimes django is rewriting one to the other: elif request.method in ('POST', 'PUT'): try: return JsonResponse( update_course_updates(usage_key, request.json, provided_id, request.user)) except: return HttpResponseBadRequest("Failed to save", content_type="text/plain")
def grading_handler(request, course_key_string, 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) """ course_key = CourseKey.from_string(course_key_string) course_module = _get_course_module(course_key, request.user) if "text/html" in request.META.get("HTTP_ACCEPT", "") and request.method == "GET": course_details = CourseGradingModel.fetch(course_key) return render_to_response( "settings_graders.html", { "context_course": course_module, "course_locator": course_key, "course_details": json.dumps(course_details, cls=CourseSettingsEncoder), "grading_url": reverse_course_url("grading_handler", course_key), }, ) elif "application/json" in request.META.get("HTTP_ACCEPT", ""): if request.method == "GET": if grader_index is None: return JsonResponse( CourseGradingModel.fetch(course_key), # encoder serializes dates, old locations, and instances encoder=CourseSettingsEncoder, ) else: return JsonResponse(CourseGradingModel.fetch_grader(course_key, 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(course_key, request.json, request.user), encoder=CourseSettingsEncoder, ) else: return JsonResponse(CourseGradingModel.update_grader_from_json(course_key, request.json, request.user)) elif request.method == "DELETE" and grader_index is not None: CourseGradingModel.delete_grader(course_key, grader_index, request.user) return JsonResponse()
def handle(self, *args, **options): if len(args) < 1 or len(args) > 2: print Command.help return num = int(args[0]) if len(args) == 2: try: course_key = CourseKey.from_string(args[1]) except InvalidKeyError: course_key = SlashSeparatedCourseKey.from_deprecated_string(args[1]) else: course_key = None create(num, course_key)
def handle(self, *args, **options): if len(args) != 1 and len(args) != 0: raise CommandError("empty_asset_trashcan requires one or no arguments: |<course_id>|") if len(args) == 1: try: course_key = CourseKey.from_string(args[0]) except InvalidKeyError: course_key = SlashSeparatedCourseKey.from_deprecated_string(args[0]) course_ids = [course_key] else: course_ids = [course.id for course in modulestore('direct').get_courses()] if query_yes_no("Emptying trashcan. Confirm?", default="no"): empty_asset_trashcan(course_ids)
def settings_handler(request, course_key_string): """ 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 """ course_key = CourseKey.from_string(course_key_string) course_module = _get_course_module(course_key, request.user) if "text/html" in request.META.get("HTTP_ACCEPT", "") and request.method == "GET": upload_asset_url = reverse_course_url("assets_handler", course_key) # see if the ORG of this course can be attributed to a 'Microsite'. In that case, the # course about page should be editable in Studio about_page_editable = not microsite.get_value_for_org( course_module.location.org, "ENABLE_MKTG_SITE", settings.FEATURES.get("ENABLE_MKTG_SITE", False) ) short_description_editable = settings.FEATURES.get("EDITABLE_SHORT_DESCRIPTION", True) return render_to_response( "settings.html", { "context_course": course_module, "course_locator": course_key, "lms_link_for_about_page": utils.get_lms_link_for_about_page(course_key), "course_image_url": utils.course_image_url(course_module), "details_url": reverse_course_url("settings_handler", course_key), "about_page_editable": about_page_editable, "short_description_editable": short_description_editable, "upload_asset_url": upload_asset_url, }, ) elif "application/json" in request.META.get("HTTP_ACCEPT", ""): if request.method == "GET": return JsonResponse( CourseDetails.fetch(course_key), # encoder serializes dates, old locations, and instances encoder=CourseSettingsEncoder, ) else: # post or put, doesn't matter. return JsonResponse( CourseDetails.update_from_json(course_key, request.json, request.user), encoder=CourseSettingsEncoder )
def course_info_update_handler(request, course_key_string, provided_id=None): """ restful CRUD operations on course_info updates. provided_id should be none if it's new (create) and index otherwise. GET json: return the course info update models POST json: create an update PUT or DELETE json: change an existing update """ if 'application/json' not in request.META.get('HTTP_ACCEPT', 'application/json'): return HttpResponseBadRequest("Only supports json requests") course_key = CourseKey.from_string(course_key_string) usage_key = course_key.make_usage_key('course_info', 'updates') if provided_id == '': provided_id = None # check that logged in user has permissions to this item (GET shouldn't require this level?) if not has_course_access(request.user, usage_key.course_key): raise PermissionDenied() if request.method == 'GET': course_updates = get_course_updates(usage_key, provided_id) if isinstance(course_updates, dict) and course_updates.get('error'): return JsonResponse(course_updates, course_updates.get('status', 400)) else: return JsonResponse(course_updates) elif request.method == 'DELETE': try: return JsonResponse(delete_course_update(usage_key, request.json, provided_id, request.user)) except: return HttpResponseBadRequest( "Failed to delete", content_type="text/plain" ) # can be either and sometimes django is rewriting one to the other: elif request.method in ('POST', 'PUT'): try: return JsonResponse(update_course_updates(usage_key, request.json, provided_id, request.user)) except: return HttpResponseBadRequest( "Failed to save", content_type="text/plain" )
def settings_handler(request, course_key_string): """ 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 """ course_key = CourseKey.from_string(course_key_string) course_module = _get_course_module(course_key, request.user) if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET': upload_asset_url = reverse_course_url('assets_handler', course_key) # see if the ORG of this course can be attributed to a 'Microsite'. In that case, the # course about page should be editable in Studio about_page_editable = not microsite.get_value_for_org( course_module.location.org, 'ENABLE_MKTG_SITE', settings.FEATURES.get('ENABLE_MKTG_SITE', False) ) short_description_editable = settings.FEATURES.get('EDITABLE_SHORT_DESCRIPTION', True) return render_to_response('settings.html', { 'context_course': course_module, 'course_locator': course_key, 'lms_link_for_about_page': utils.get_lms_link_for_about_page(course_key), 'course_image_url': utils.course_image_url(course_module), 'details_url': reverse_course_url('settings_handler', course_key), 'about_page_editable': about_page_editable, 'short_description_editable': short_description_editable, 'upload_asset_url': upload_asset_url }) elif 'application/json' in request.META.get('HTTP_ACCEPT', ''): if request.method == 'GET': return JsonResponse( CourseDetails.fetch(course_key), # encoder serializes dates, old locations, and instances encoder=CourseSettingsEncoder ) else: # post or put, doesn't matter. return JsonResponse( CourseDetails.update_from_json(course_key, request.json, request.user), encoder=CourseSettingsEncoder )
def handle(self, *args, **options): if len(args) < 1 or len(args) > 2: print Command.help return num = int(args[0]) if len(args) == 2: try: course_key = CourseKey.from_string(args[1]) except InvalidKeyError: course_key = SlashSeparatedCourseKey.from_deprecated_string( args[1]) else: course_key = None create(num, course_key)
def handle(self, *args, **options): username = options['username'] name = options['name'] if not username: username = options['email'].split('@')[0] if not name: name = options['email'].split('@')[0] # parse out the course into a coursekey if options['course']: try: course = CourseKey.from_string(options['course']) # if it's not a new-style course key, parse it from an old-style # course key except InvalidKeyError: course = SlashSeparatedCourseKey.from_deprecated_string( options['course']) post_data = { 'username': username, 'email': options['email'], 'password': options['password'], 'name': name, 'honor_code': u'true', 'terms_of_service': u'true', } # django.utils.translation.get_language() will be used to set the new # user's preferred language. This line ensures that the result will # match this installation's default locale. Otherwise, inside a # management command, it will always return "en-us". translation.activate(settings.LANGUAGE_CODE) try: user, profile, reg = _do_create_account(post_data) if options['staff']: user.is_staff = True user.save() reg.activate() reg.save() create_comments_service_user(user) except AccountValidationError as e: print e.message user = User.objects.get(email=options['email']) if options['course']: CourseEnrollment.enroll(user, course, mode=options['mode']) translation.deactivate()
def handle(self, *args, **options): "Execute the command" if len(args) != 2: raise CommandError("export requires two arguments: <course id> <output path>") try: course_key = CourseKey.from_string(args[0]) except InvalidKeyError: course_key = SlashSeparatedCourseKey.from_deprecated_string(args[0]) output_path = args[1] print("Exporting course id = {0} to {1}".format(course_key, output_path)) root_dir = os.path.dirname(output_path) course_dir = os.path.splitext(os.path.basename(output_path))[0] export_to_xml(modulestore('direct'), contentstore(), course_key, root_dir, course_dir, modulestore())
def advanced_settings_handler(request, course_key_string): """ 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 """ course_key = CourseKey.from_string(course_key_string) course_module = _get_course_module(course_key, request.user) 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': reverse_course_url('advanced_settings_handler', course_key) }) 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, user=request.user, )) except (TypeError, ValueError) as err: return HttpResponseBadRequest( "Incorrect setting format. {}".format(err), content_type="text/plain")
def handle(self, *args, **options): username = options['username'] name = options['name'] if not username: username = options['email'].split('@')[0] if not name: name = options['email'].split('@')[0] # parse out the course into a coursekey if options['course']: try: course = CourseKey.from_string(options['course']) # if it's not a new-style course key, parse it from an old-style # course key except InvalidKeyError: course = SlashSeparatedCourseKey.from_deprecated_string(options['course']) post_data = { 'username': username, 'email': options['email'], 'password': options['password'], 'name': name, 'honor_code': u'true', 'terms_of_service': u'true', } # django.utils.translation.get_language() will be used to set the new # user's preferred language. This line ensures that the result will # match this installation's default locale. Otherwise, inside a # management command, it will always return "en-us". translation.activate(settings.LANGUAGE_CODE) try: user, profile, reg = _do_create_account(post_data) if options['staff']: user.is_staff = True user.save() reg.activate() reg.save() create_comments_service_user(user) except AccountValidationError as e: print e.message user = User.objects.get(email=options['email']) if options['course']: CourseEnrollment.enroll(user, course, mode=options['mode']) translation.deactivate()
def handle(self, *args, **options): if len(args) != 1 and len(args) != 2: raise CommandError("delete_course requires one or more arguments: <course_id> |commit|") try: course_key = CourseKey.from_string(args[0]) except InvalidKeyError: course_key = SlashSeparatedCourseKey.from_deprecated_string(args[0]) commit = False if len(args) == 2: commit = args[1] == 'commit' if commit: print('Actually going to delete the course from DB....') if query_yes_no("Deleting course {0}. Confirm?".format(course_key), default="no"): if query_yes_no("Are you sure. This action cannot be undone!", default="no"): delete_course_and_groups(course_key, commit)
def handle(self, *args, **options): if not args: raise CommandError("Course id not specified") if len(args) > 1: raise CommandError("Only one course id may be specifiied") course_id = args[0] try: course_key = CourseKey.from_string(course_id) except InvalidKeyError: course_key = SlashSeparatedCourseKey.from_deprecated_string( course_id) course = get_course(course_key) if not course: raise CommandError("Invalid course id: {}".format(course_id)) if course.discussion_link: self.stdout.write(course.discussion_link)
def course_info_handler(request, course_key_string): """ GET html: return html for editing the course info handouts and updates. """ course_key = CourseKey.from_string(course_key_string) course_module = _get_course_module(course_key, request.user) if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'): return render_to_response( 'course_info.html', { 'context_course': course_module, 'updates_url': reverse_course_url('course_info_update_handler', course_key), 'handouts_locator': course_key.make_usage_key('course_info', 'handouts'), 'base_asset_url': StaticContent.get_base_url_path_for_course_assets(course_module.id) } ) else: return HttpResponseBadRequest("Only supports html requests")
def course_info_handler(request, course_key_string): """ GET html: return html for editing the course info handouts and updates. """ course_key = CourseKey.from_string(course_key_string) course_module = _get_course_module(course_key, request.user) if "text/html" in request.META.get("HTTP_ACCEPT", "text/html"): return render_to_response( "course_info.html", { "context_course": course_module, "updates_url": reverse_course_url("course_info_update_handler", course_key), "handouts_locator": course_key.make_usage_key("course_info", "handouts"), "base_asset_url": StaticContent.get_base_url_path_for_course_assets(course_module.id), }, ) else: return HttpResponseBadRequest("Only supports html requests")
def import_status_handler(request, course_key_string, filename=None): """ Returns an integer corresponding to the status of a file import. These are: 0 : No status info found (import done or upload still in progress) 1 : Extracting file 2 : Validating. 3 : Importing to mongo """ course_key = CourseKey.from_string(course_key_string) if not has_course_access(request.user, course_key): raise PermissionDenied() try: session_status = request.session["import_status"] status = session_status[course_key_string + filename] except KeyError: status = 0 return JsonResponse({"ImportStatus": status})
def handle(self, *args, **options): if len(args) != 1 and len(args) != 0: raise CommandError( "empty_asset_trashcan requires one or no arguments: |<course_id>|" ) if len(args) == 1: try: course_key = CourseKey.from_string(args[0]) except InvalidKeyError: course_key = SlashSeparatedCourseKey.from_deprecated_string( args[0]) course_ids = [course_key] else: course_ids = [ course.id for course in modulestore('direct').get_courses() ] if query_yes_no("Emptying trashcan. Confirm?", default="no"): empty_asset_trashcan(course_ids)
def handle(self, *args, **options): if not options['course']: raise CommandError(Command.course_option.help) try: course_key = CourseKey.from_string(options['course']) except InvalidKeyError: course_key = SlashSeparatedCourseKey.from_deprecated_string( options['course']) course = get_course_by_id(course_key) print 'Warning: this command directly edits the list of course tabs in mongo.' print 'Tabs before any changes:' print_course(course) try: if options['delete']: if len(args) != 1: raise CommandError(Command.delete_option.help) num = int(args[0]) if query_yes_no('Deleting tab {0} Confirm?'.format(num), default='no'): tabs.primitive_delete(course, num - 1) # -1 for 0-based indexing elif options['insert']: if len(args) != 3: raise CommandError(Command.insert_option.help) num = int(args[0]) tab_type = args[1] name = args[2] if query_yes_no( 'Inserting tab {0} "{1}" "{2}" Confirm?'.format( num, tab_type, name), default='no'): tabs.primitive_insert(course, num - 1, tab_type, name) # -1 as above except ValueError as e: # Cute: translate to CommandError so the CLI error prints nicely. raise CommandError(e)
def handle(self, *args, **options): "Execute the command" if len(args) != 2: raise CommandError( "export requires two arguments: <course id> <output path>") try: course_key = CourseKey.from_string(args[0]) except InvalidKeyError: course_key = SlashSeparatedCourseKey.from_deprecated_string( args[0]) output_path = args[1] print("Exporting course id = {0} to {1}".format( course_key, output_path)) root_dir = os.path.dirname(output_path) course_dir = os.path.splitext(os.path.basename(output_path))[0] export_to_xml(modulestore('direct'), contentstore(), course_key, root_dir, course_dir, modulestore())