def handle(self, *args, **options): "Execute the command" if len(args) != 2: raise CommandError("clone requires 2 arguments: <source-course_id> <dest-course_id>") source_course_id = args[0] dest_course_id = args[1] mstore = modulestore('direct') cstore = contentstore() course_id_dict = Location.parse_course_id(dest_course_id) mstore.ignore_write_events_on_courses.append('{org}/{course}'.format(**course_id_dict)) print("Cloning course {0} to {1}".format(source_course_id, dest_course_id)) source_location = CourseDescriptor.id_to_location(source_course_id) dest_location = CourseDescriptor.id_to_location(dest_course_id) if clone_course(mstore, cstore, source_location, dest_location): # be sure to recompute metadata inheritance after all those updates mstore.refresh_cached_metadata_inheritance_tree(dest_location) print("copying User permissions...") # purposely avoids auth.add_user b/c it doesn't have a caller to authorize CourseInstructorRole(dest_location).add_users( *CourseInstructorRole(source_location).users_with_role() ) CourseStaffRole(dest_location).add_users( *CourseStaffRole(source_location).users_with_role() )
def handle(self, *args, **options): "Execute the command" if len(args) != 2: raise CommandError("clone requires two arguments: <source-course_id> <dest-course_id>") source_course_id = args[0] dest_course_id = args[1] mstore = modulestore('direct') cstore = contentstore() org, course_num, run = dest_course_id.split("/") mstore.ignore_write_events_on_courses.append('{0}/{1}'.format(org, course_num)) print("Cloning course {0} to {1}".format(source_course_id, dest_course_id)) source_location = CourseDescriptor.id_to_location(source_course_id) dest_location = CourseDescriptor.id_to_location(dest_course_id) if clone_course(mstore, cstore, source_location, dest_location): # be sure to recompute metadata inheritance after all those updates mstore.refresh_cached_metadata_inheritance_tree(dest_location) print("copying User permissions...") _copy_course_group(source_location, dest_location)
def test_clone_course(self): course_data = { 'template': 'i4x://edx/templates/course/Empty', 'org': 'MITx', 'number': '999', 'display_name': 'Robot Super Course', } module_store = modulestore('direct') import_from_xml(module_store, 'common/test/data/', ['full']) resp = self.client.post(reverse('create_new_course'), course_data) self.assertEqual(resp.status_code, 200) data = parse_json(resp) self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course') content_store = contentstore() source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012') dest_location = CourseDescriptor.id_to_location('MITx/999/Robot_Super_Course') clone_course(module_store, content_store, source_location, dest_location) # now loop through all the units in the course and verify that the clone can render them, which # means the objects are at least present items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None])) self.assertGreater(len(items), 0) clone_items = module_store.get_items(Location(['i4x', 'MITx', '999', 'vertical', None])) self.assertGreater(len(clone_items), 0) for descriptor in items: new_loc = descriptor.location.replace(org='MITx', course='999') print "Checking {0} should now also be at {1}".format(descriptor.location.url(), new_loc.url()) resp = self.client.get(reverse('edit_unit', kwargs={'location': new_loc.url()})) self.assertEqual(resp.status_code, 200)
def test_clone_course(self): course_data = { "template": "i4x://edx/templates/course/Empty", "org": "MITx", "number": "999", "display_name": "Robot Super Course", } module_store = modulestore("direct") import_from_xml(module_store, "common/test/data/", ["full"]) resp = self.client.post(reverse("create_new_course"), course_data) self.assertEqual(resp.status_code, 200) data = parse_json(resp) self.assertEqual(data["id"], "i4x://MITx/999/course/Robot_Super_Course") content_store = contentstore() source_location = CourseDescriptor.id_to_location("edX/full/6.002_Spring_2012") dest_location = CourseDescriptor.id_to_location("MITx/999/Robot_Super_Course") clone_course(module_store, content_store, source_location, dest_location) # now loop through all the units in the course and verify that the clone can render them, which # means the objects are at least present items = module_store.get_items(Location(["i4x", "edX", "full", "vertical", None])) self.assertGreater(len(items), 0) clone_items = module_store.get_items(Location(["i4x", "MITx", "999", "vertical", None])) self.assertGreater(len(clone_items), 0) for descriptor in items: new_loc = descriptor.location.replace(org="MITx", course="999") print "Checking {0} should now also be at {1}".format(descriptor.location.url(), new_loc.url()) resp = self.client.get(reverse("edit_unit", kwargs={"location": new_loc.url()})) self.assertEqual(resp.status_code, 200)
def get_array_section_has_problem(course_id): """ Returns an array of true/false whether each section has problems. `course_id` the course ID for the course interested in The ith value in the array is true if the ith section in the course contains problems and false otherwise. """ course = modulestore().get_instance(course_id, CourseDescriptor.id_to_location(course_id), depth=4) b_section_has_problem = [False] * len(course.get_children()) i = 0 for section in course.get_children(): for subsection in section.get_children(): for unit in subsection.get_children(): for child in unit.get_children(): if child.location.category == 'problem': b_section_has_problem[i] = True break # out of child loop if b_section_has_problem[i]: break # out of unit loop if b_section_has_problem[i]: break # out of subsection loop i += 1 return b_section_has_problem
def handle(self, *args, **options): user = options['username'] course_id = options['course'] if not (course_id and user): raise CommandError('both course id and student username are required') student = None print "Fetching enrollment for student {0} in {1}".format(user, course_id) if '@' in user: student = User.objects.get(email=user, courseenrollment__course_id=course_id) else: student = User.objects.get(username=user, courseenrollment__course_id=course_id) print "Fetching course data for {0}".format(course_id) course = modulestore().get_instance(course_id, CourseDescriptor.id_to_location(course_id), depth=2) if not options['noop']: # Add the certificate request to the queue xq = XQueueCertInterface() if options['insecure']: xq.use_https = False ret = xq.regen_cert(student, course_id, course=course) print '{0} - {1}'.format(student, ret) else: print "noop option given, skipping work queueing..."
def test_prefetch_children(self): module_store = modulestore('direct') import_from_xml(module_store, 'common/test/data/', ['full']) location = CourseDescriptor.id_to_location( 'edX/full/6.002_Spring_2012') wrapper = MongoCollectionFindWrapper(module_store.collection.find) module_store.collection.find = wrapper.find course = module_store.get_item(location, depth=2) # make sure we haven't done too many round trips to DB # note we say 4 round trips here for 1) the course, 2 & 3) for the chapters and sequentials, and # 4) because of the RT due to calculating the inherited metadata self.assertEqual(wrapper.counter, 4) # make sure we pre-fetched a known sequential which should be at # depth=2 self.assertTrue(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None]) in course.system.module_data) # make sure we don't have a specific vertical which should be at # depth=3 self.assertFalse(Location(['i4x', 'edX', 'full', 'vertical', 'vertical_58', None]) in course.system.module_data)
def handle(self, *args, **options): "Execute the command" if len(args) != 1: raise CommandError("export requires one argument: <output path>") output_path = args[0] cs = contentstore() ms = modulestore('direct') root_dir = output_path courses = ms.get_courses() print("%d courses to export:" % len(courses)) cids = [x.id for x in courses] print(cids) for course_id in cids: print("-"*77) print("Exporting course id = {0} to {1}".format(course_id, output_path)) if 1: try: location = CourseDescriptor.id_to_location(course_id) course_dir = course_id.replace('/', '...') export_to_xml(ms, cs, location, root_dir, course_dir, modulestore()) except Exception as err: print("="*30 + "> Oops, failed to export %s" % course_id) print("Error:") print(err)
def handle(self, *args, **options): if len(args) != 1 and len(args) != 2: raise CommandError("delete_course requires one or more arguments: <location> |commit|") course_id = args[0] commit = False if len(args) == 2: commit = args[1] == 'commit' if commit: print 'Actually going to delete the course from DB....' ms = modulestore('direct') cs = contentstore() org, course_num, run = course_id.split("/") ms.ignore_write_events_on_courses.append('{0}/{1}'.format(org, course_num)) if query_yes_no("Deleting course {0}. Confirm?".format(course_id), default="no"): if query_yes_no("Are you sure. This action cannot be undone!", default="no"): loc = CourseDescriptor.id_to_location(course_id) if delete_course(ms, cs, loc, commit): print 'removing User permissions from course....' # in the django layer, we need to remove all the user permissions groups associated with this course if commit: try: _delete_course_group(loc) except Exception as err: print("Error in deleting course groups for {0}: {1}".format(loc, err))
def test_remove_hide_progress_tab(self): module_store = modulestore('direct') import_from_xml(module_store, 'common/test/data/', ['full']) source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012') course = module_store.get_item(source_location) self.assertFalse(course.hide_progress_tab)
def purchased_callback(self): """ When purchased, this should enroll the user in the course. We are assuming that course settings for enrollment date are configured such that only if the (user.email, course_id) pair is found in CourseEnrollmentAllowed will the user be allowed to enroll. Otherwise requiring payment would in fact be quite silly since there's a clear back door. """ try: course_loc = CourseDescriptor.id_to_location(self.course_id) course_exists = modulestore().has_item(self.course_id, course_loc) except ValueError: raise PurchasedCallbackException( "The customer purchased Course {0}, but that course doesn't exist!".format(self.course_id) ) if not course_exists: raise PurchasedCallbackException( "The customer purchased Course {0}, but that course doesn't exist!".format(self.course_id) ) CourseEnrollment.enroll(user=self.user, course_id=self.course_id, mode=self.mode) log.info( "Enrolled {0} in paid course {1}, paid ${2}".format(self.user.email, self.course_id, self.line_cost) ) # pylint: disable=E1101
def handle(self, *args, **options): if len(args) != 1 and len(args) != 2: raise CommandError( "delete_course requires one or more arguments: <location> |commit|") loc_str = args[0] commit = False if len(args) == 2: commit = args[1] == 'commit' if commit: print 'Actually going to delete the course from DB....' ms = modulestore('direct') cs = contentstore() if query_yes_no("Deleting course {0}. Confirm?".format(loc_str), default="no"): if query_yes_no("Are you sure. This action cannot be undone!", default="no"): loc = CourseDescriptor.id_to_location(loc_str) if delete_course(ms, cs, loc, commit): print 'removing User permissions from course....' # in the django layer, we need to remove all the user # permissions groups associated with this course if commit: _delete_course_group(loc)
def jump_to_id(request, course_id, module_id): """ This entry point allows for a shorter version of a jump to where just the id of the element is passed in. This assumes that id is unique within the course_id namespace """ course_location = CourseDescriptor.id_to_location(course_id) items = modulestore().get_items( Location("i4x", course_location.org, course_location.course, None, module_id), course_id=course_id ) if len(items) == 0: raise Http404( "Could not find id = {0} in course_id = {1}. Referer = {2}".format( module_id, course_id, request.META.get("HTTP_REFERER", "") ) ) if len(items) > 1: log.warning( "Multiple items found with id = {0} in course_id = {1}. Referer = {2}. Using first found {3}...".format( module_id, course_id, request.META.get("HTTP_REFERER", ""), items[0].location.url() ) ) return jump_to(request, course_id, items[0].location.url())
def get_course(self): """ Return course by course id. """ course_location = CourseDescriptor.id_to_location(self.course_id) course = self.descriptor.runtime.modulestore.get_item(course_location) return course
def get_static_transcript(self, request): """ Courses that are imported with the --nostatic flag do not show transcripts/captions properly even if those captions are stored inside their static folder. This adds a last resort method of redirecting to the static asset path of the course if the transcript can't be found inside the contentstore and the course has the static_asset_path field set. """ response = Response(status=404) # Only do redirect for English if not self.transcript_language == "en": return response video_id = request.GET.get("videoId", None) if video_id: transcript_name = video_id else: transcript_name = self.sub if transcript_name: course_location = CourseDescriptor.id_to_location(self.course_id) course = self.descriptor.runtime.modulestore.get_item(course_location) if course.static_asset_path: response = Response( status=307, location="/static/{0}/{1}".format( course.static_asset_path, subs_filename(transcript_name, self.transcript_language) ), ) return response
def test_remove_hide_progress_tab(self): module_store = modulestore("direct") import_from_xml(module_store, "common/test/data/", ["full"]) source_location = CourseDescriptor.id_to_location("edX/full/6.002_Spring_2012") course = module_store.get_item(source_location) self.assertFalse(course.hide_progress_tab)
def course_context_from_course_id(course_id): """ Creates a course context from a `course_id`. Example Returned Context:: { 'course_id': 'org/course/run', 'org_id': 'org' } """ course_id = course_id or '' context = { 'course_id': course_id, 'org_id': '' } if course_id: try: location = CourseDescriptor.id_to_location(course_id) context['org_id'] = location.org except ValueError: log.warning( 'Unable to parse course_id "{course_id}"'.format( course_id=course_id ), exc_info=True ) return context
def purchased_callback(self): """ When purchased, this should enroll the user in the course. We are assuming that course settings for enrollment date are configured such that only if the (user.email, course_id) pair is found in CourseEnrollmentAllowed will the user be allowed to enroll. Otherwise requiring payment would in fact be quite silly since there's a clear back door. """ try: course_loc = CourseDescriptor.id_to_location(self.course_id) course_exists = modulestore().has_item(self.course_id, course_loc) except ValueError: raise PurchasedCallbackException( "The customer purchased Course {0}, but that course doesn't exist!".format(self.course_id)) if not course_exists: raise PurchasedCallbackException( "The customer purchased Course {0}, but that course doesn't exist!".format(self.course_id)) CourseEnrollment.enroll(user=self.user, course_id=self.course_id, mode=self.mode) log.info("Enrolled {0} in paid course {1}, paid ${2}".format(self.user.email, self.course_id, self.line_cost)) org, course_num, run = self.course_id.split("/") dog_stats_api.increment( "shoppingcart.PaidCourseRegistration.purchased_callback.enrollment", tags=["org:{0}".format(org), "course:{0}".format(course_num), "run:{0}".format(run)] )
def delete_course_and_groups(course_id, commit=False): """ This deletes the courseware associated with a course_id as well as cleaning update_item the various user table stuff (groups, permissions, etc.) """ module_store = modulestore('direct') content_store = contentstore() org, course_num, _ = course_id.split("/") module_store.ignore_write_events_on_courses.append('{0}/{1}'.format( org, course_num)) loc = CourseDescriptor.id_to_location(course_id) if delete_course(module_store, content_store, loc, commit): print 'removing forums permissions and roles...' unseed_permissions_roles(course_id) print 'removing User permissions from course....' # in the django layer, we need to remove all the user permissions groups associated with this course if commit: try: staff_role = CourseStaffRole(loc) staff_role.remove_users(*staff_role.users_with_role()) instructor_role = CourseInstructorRole(loc) instructor_role.remove_users( *instructor_role.users_with_role()) except Exception as err: log.error( "Error in deleting course groups for {0}: {1}".format( loc, err))
def delete_course_and_groups(course_id, commit=False): """ This deletes the courseware associated with a course_id as well as cleaning update_item the various user table stuff (groups, permissions, etc.) """ module_store = modulestore('direct') content_store = contentstore() course_id_dict = Location.parse_course_id(course_id) module_store.ignore_write_events_on_courses.append('{org}/{course}'.format(**course_id_dict)) loc = CourseDescriptor.id_to_location(course_id) if delete_course(module_store, content_store, loc, commit): print 'removing User permissions from course....' # in the django layer, we need to remove all the user permissions groups associated with this course if commit: try: staff_role = CourseStaffRole(loc) staff_role.remove_users(*staff_role.users_with_role()) instructor_role = CourseInstructorRole(loc) instructor_role.remove_users(*instructor_role.users_with_role()) except Exception as err: log.error("Error in deleting course groups for {0}: {1}".format(loc, err)) # remove location of this course from loc_mapper and cache loc_mapper().delete_course_mapping(loc)
def handle(self, *args, **options): if len(args) != 1: raise CommandError("check_course requires one argument: <location>") loc_str = args[0] loc = CourseDescriptor.id_to_location(loc_str) store = modulestore() # setup a request cache so we don't throttle the DB with all the metadata inheritance requests store.set_modulestore_configuration({ 'metadata_inheritance_cache_subsystem': CACHE, 'request_cache': RequestCache.get_request_cache() }) course = store.get_item(loc, 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.url()) 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.url()] 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(['i4x', course.location.org, course.location.course, 'discussion', None, None]) for item in queried_discussion_items: if item.location.url() not in discussion_items: print 'Found dangling discussion module = {0}'.format(item.location.url())
def get_d3_sequential_open_distrib(course_id): """ Returns how many students opened a sequential/subsection for each section, data already in format for d3 function. `course_id` the course ID for the course interested in Returns an array in the order of the sections and each dict has: 'display_name' - display name for the section 'data' - data for the d3_stacked_bar_graph function of how many students opened each sequential/subsection """ sequential_open_distrib = get_sequential_open_distrib(course_id) d3_data = [] # Retrieve course object down to subsection course = modulestore().get_instance(course_id, CourseDescriptor.id_to_location(course_id), depth=2) # Iterate through sections, subsections for section in course.get_children(): curr_section = {} curr_section['display_name'] = own_metadata(section).get('display_name', '') data = [] c_subsection = 0 # Construct data for each subsection to be sent to d3 for subsection in section.get_children(): c_subsection += 1 subsection_name = own_metadata(subsection).get('display_name', '') num_students = 0 if subsection.location.url() in sequential_open_distrib: num_students = sequential_open_distrib[subsection.location.url()] stack_data = [] # Tooltip parameters for subsection in open_distribution view tooltip = { 'type': 'subsection', 'num_students': num_students, 'subsection_num': c_subsection, 'subsection_name': subsection_name } stack_data.append({ 'color': 0, 'value': num_students, 'tooltip': tooltip, 'module_url': subsection.location.url(), }) subsection = { 'xValue': "SS {0}".format(c_subsection), 'stackData': stack_data, } data.append(subsection) curr_section['data'] = data d3_data.append(curr_section) return d3_data
def _ended_courses(self): for course_id in [course # all courses in COURSE_LISTINGS for sub in settings.COURSE_LISTINGS for course in settings.COURSE_LISTINGS[sub]]: course_loc = CourseDescriptor.id_to_location(course_id) course = modulestore().get_instance(course_id, course_loc) if course.has_ended(): yield course_id
def handle(self, *args, **options): if len(args) != 2: raise CommandError("clone requires two arguments: <source-location> <dest-location>") source_location_str = args[0] dest_location_str = args[1] ms = modulestore('direct') cs = contentstore() print "Cloning course {0} to {1}".format(source_location_str, dest_location_str) source_location = CourseDescriptor.id_to_location(source_location_str) dest_location = CourseDescriptor.id_to_location(dest_location_str) if clone_course(ms, cs, source_location, dest_location): print "copying User permissions..." _copy_course_group(source_location, dest_location)
def has_permission(self, permission): course_loc = CourseDescriptor.id_to_location(self.course_id) course = modulestore().get_instance(self.course_id, course_loc) if self.name == FORUM_ROLE_STUDENT and \ (permission.startswith('edit') or permission.startswith('update') or permission.startswith('create')) and \ (not course.forum_posts_allowed): return False return self.permissions.filter(name=permission).exists()
def _check_access(user, course_id): """ Raise 404 if user doesn't have staff access to course_id """ course_location = CourseDescriptor.id_to_location(course_id) if not has_access(user, course_location, 'staff'): raise Http404 return
def get_course_by_id(course_id, depth=0): """ Given a course id, return the corresponding course descriptor. If course_id is not valid, raises a 404. depth: The number of levels of children for the modulestore to cache. None means infinite depth """ try: course_loc = CourseDescriptor.id_to_location(course_id) return modulestore().get_instance(course_id, course_loc, depth=depth) except (KeyError, ItemNotFoundError): raise Http404("Course not found.")
def test_delete_course(self): module_store = modulestore('direct') import_from_xml(module_store, 'common/test/data/', ['full']) content_store = contentstore() location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012') delete_course(module_store, content_store, location, commit=True) items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None])) self.assertEqual(len(items), 0)
def load_test_import_course(self): ''' Load the standard course used to test imports (for do_import_static=False behavior). ''' content_store = contentstore() module_store = modulestore('direct') import_from_xml(module_store, 'common/test/data/', ['test_import_course'], static_content_store=content_store, do_import_static=False, verbose=True) course_location = CourseDescriptor.id_to_location('edX/test_import_course/2012_Fall') course = module_store.get_item(course_location) self.assertIsNotNone(course) return module_store, content_store, course, course_location
def test_delete_course(self): module_store = modulestore("direct") import_from_xml(module_store, "common/test/data/", ["full"]) content_store = contentstore() location = CourseDescriptor.id_to_location("edX/full/6.002_Spring_2012") delete_course(module_store, content_store, location, commit=True) items = module_store.get_items(Location(["i4x", "edX", "full", "vertical", None])) self.assertEqual(len(items), 0)
def handle(self, *args, **options): username = args[0] course_id = args[1] print username, course_id our_options = dict((k, v) for k, v in options.items() if Command.is_valid_option(k) and v is not None) try: student = User.objects.get(username=username) except User.DoesNotExist: raise CommandError("User \"{}\" does not exist".format(username)) try: testcenter_user = TestCenterUser.objects.get(user=student) except TestCenterUser.DoesNotExist: raise CommandError( "User \"{}\" does not have an existing demographics record". format(username)) # get an "exam" object. Check to see if a course_id was specified, and use information from that: exam = None create_dummy_exam = 'create_dummy_exam' in our_options and our_options[ 'create_dummy_exam'] if not create_dummy_exam: try: course = course_from_id(course_id) if 'ignore_registration_dates' in our_options: examlist = [ exam for exam in course.test_center_exams if exam.exam_series_code == our_options.get( 'exam_series_code') ] exam = examlist[0] if len(examlist) > 0 else None else: exam = course.current_test_center_exam except ItemNotFoundError: pass else: # otherwise use explicit values (so we don't have to define a course): exam_name = "Dummy Placeholder Name" exam_info = { 'Exam_Series_Code': our_options['exam_series_code'], 'First_Eligible_Appointment_Date': our_options['eligibility_appointment_date_first'], 'Last_Eligible_Appointment_Date': our_options['eligibility_appointment_date_last'], } exam = CourseDescriptor.TestCenterExam(course_id, exam_name, exam_info) # update option values for date_first and date_last to use YYYY-MM-DD format # instead of YYYY-MM-DDTHH:MM our_options[ 'eligibility_appointment_date_first'] = exam.first_eligible_appointment_date.strftime( "%Y-%m-%d") our_options[ 'eligibility_appointment_date_last'] = exam.last_eligible_appointment_date.strftime( "%Y-%m-%d") if exam is None: raise CommandError( "Exam for course_id {} does not exist".format(course_id)) exam_code = exam.exam_series_code UPDATE_FIELDS = ( 'accommodation_request', 'accommodation_code', 'client_authorization_id', 'exam_series_code', 'eligibility_appointment_date_first', 'eligibility_appointment_date_last', ) # create and save the registration: needs_updating = False registrations = get_testcenter_registration(student, course_id, exam_code) if len(registrations) > 0: registration = registrations[0] for fieldname in UPDATE_FIELDS: if fieldname in our_options and registration.__getattribute__( fieldname) != our_options[fieldname]: needs_updating = True else: accommodation_request = our_options.get('accommodation_request', '') registration = TestCenterRegistration.create( testcenter_user, exam, accommodation_request) needs_updating = True if needs_updating: # first update the record with the new values, if any: for fieldname in UPDATE_FIELDS: if fieldname in our_options and fieldname not in TestCenterRegistrationForm.Meta.fields: registration.__setattr__(fieldname, our_options[fieldname]) # the registration form normally populates the data dict with # the accommodation request (if any). But here we want to # specify only those values that might change, so update the dict with existing # values. form_options = dict(our_options) for propname in TestCenterRegistrationForm.Meta.fields: if propname not in form_options: form_options[propname] = registration.__getattribute__( propname) form = TestCenterRegistrationForm(instance=registration, data=form_options) if form.is_valid(): form.update_and_save() print "Updated registration information for user's registration: username \"{}\" course \"{}\", examcode \"{}\"".format( student.username, course_id, exam_code) else: if (len(form.errors) > 0): print "Field Form errors encountered:" for fielderror in form.errors: for msg in form.errors[fielderror]: print "Field Form Error: {} -- {}".format( fielderror, msg) if (len(form.non_field_errors()) > 0): print "Non-field Form errors encountered:" for nonfielderror in form.non_field_errors: print "Non-field Form Error: %s" % nonfielderror else: print "No changes necessary to make to existing user's registration." # override internal values: change_internal = False if 'exam_series_code' in our_options: exam_code = our_options['exam_series_code'] registration = get_testcenter_registration(student, course_id, exam_code)[0] for internal_field in [ 'upload_error_message', 'upload_status', 'authorization_id' ]: if internal_field in our_options: registration.__setattr__(internal_field, our_options[internal_field]) change_internal = True if change_internal: print "Updated confirmation information in existing user's registration." registration.save() else: print "No changes necessary to make to confirmation information in existing user's registration."
def path_to_location(modulestore, course_id, location): ''' Try to find a course_id/chapter/section[/position] path to location in modulestore. The courseware insists that the first level in the course is chapter, but any kind of module can be a "section". location: something that can be passed to Location course_id: Search for paths in this course. raise ItemNotFoundError if the location doesn't exist. raise NoPathToItem if the location exists, but isn't accessible via a chapter/section path in the course(s) being searched. Return a tuple (course_id, chapter, section, position) suitable for the courseware index view. A location may be accessible via many paths. This method may return any valid path. If the section is a sequential or vertical, position will be the position of this location in that sequence. Otherwise, position will be None. TODO (vshnayder): Not true yet. ''' def flatten(xs): '''Convert lisp-style (a, (b, (c, ()))) list into a python list. Not a general flatten function. ''' p = [] while xs != (): p.append(xs[0]) xs = xs[1] return p def find_path_to_course(): '''Find a path up the location graph to a node with the specified category. If no path exists, return None. If a path exists, return it as a list with target location first, and the starting location last. ''' # Standard DFS # To keep track of where we came from, the work queue has # tuples (location, path-so-far). To avoid lots of # copying, the path-so-far is stored as a lisp-style # list--nested hd::tl tuples, and flattened at the end. queue = [(location, ())] while len(queue) > 0: (loc, path) = queue.pop() # Takes from the end loc = Location(loc) # get_parent_locations should raise ItemNotFoundError if location # isn't found so we don't have to do it explicitly. Call this # first to make sure the location is there (even if it's a course, and # we would otherwise immediately exit). parents = modulestore.get_parent_locations(loc, course_id) # print 'Processing loc={0}, path={1}'.format(loc, path) if loc.category == "course": # confirm that this is the right course if course_id == CourseDescriptor.location_to_id(loc): # Found it! path = (loc, path) return flatten(path) # otherwise, add parent locations at the end newpath = (loc, path) queue.extend(zip(parents, repeat(newpath))) # If we're here, there is no path return None if not modulestore.has_item(location): raise ItemNotFoundError path = find_path_to_course() if path is None: raise NoPathToItem(location) n = len(path) course_id = CourseDescriptor.location_to_id(path[0]) # pull out the location names chapter = path[1].name if n > 1 else None section = path[2].name if n > 2 else None # Figure out the position position = None # This block of code will find the position of a module within a nested tree # of modules. If a problem is on tab 2 of a sequence that's on tab 3 of a # sequence, the resulting position is 3_2. However, no positional modules # (e.g. sequential and videosequence) currently deal with this form of # representing nested positions. This needs to happen before jumping to a # module nested in more than one positional module will work. if n > 3: position_list = [] for path_index in range(2, n - 1): category = path[path_index].category if category == 'sequential' or category == 'videosequence': section_desc = modulestore.get_instance(course_id, path[path_index]) child_locs = [c.location for c in section_desc.get_children()] # positions are 1-indexed, and should be strings to be consistent with # url parsing. position_list.append(str(child_locs.index(path[path_index + 1]) + 1)) position = "_".join(position_list) return (course_id, chapter, section, position)
def get_d3_problem_grade_distrib(course_id): """ Returns problem grade distribution information for each section, data already in format for d3 function. `course_id` the course ID for the course interested in Returns an array of dicts in the order of the sections. Each dict has: 'display_name' - display name for the section 'data' - data for the d3_stacked_bar_graph function of the grade distribution for that problem """ prob_grade_distrib = get_problem_grade_distribution(course_id) d3_data = [] # Retrieve course object down to problems course = modulestore().get_instance( course_id, CourseDescriptor.id_to_location(course_id), depth=4) # Iterate through sections, subsections, units, problems for section in course.get_children(): curr_section = {} curr_section['display_name'] = own_metadata(section).get( 'display_name', '') data = [] c_subsection = 0 for subsection in section.get_children(): c_subsection += 1 c_unit = 0 for unit in subsection.get_children(): c_unit += 1 c_problem = 0 for child in unit.get_children(): # Student data is at the problem level if child.location.category == 'problem': c_problem += 1 stack_data = [] # Construct label to display for this problem label = "P{0}.{1}.{2}".format(c_subsection, c_unit, c_problem) # Only problems in prob_grade_distrib have had a student submission. if child.location.url() in prob_grade_distrib: # Get max_grade, grade_distribution for this problem problem_info = prob_grade_distrib[ child.location.url()] # Get problem_name for tooltip problem_name = own_metadata(child).get( 'display_name', '') # Compute percent of this grade over max_grade max_grade = float(problem_info['max_grade']) for (grade, count_grade) in problem_info['grade_distrib']: percent = 0.0 if max_grade > 0: percent = (grade * 100.0) / max_grade # Construct tooltip for problem in grade distibution view tooltip = _( "{label} {problem_name} - {count_grade} {students} ({percent:.0f}%: {grade:.0f}/{max_grade:.0f} {questions})" ).format( label=label, problem_name=problem_name, count_grade=count_grade, students=_("students"), percent=percent, grade=grade, max_grade=max_grade, questions=_("questions"), ) # Construct data to be sent to d3 stack_data.append({ 'color': percent, 'value': count_grade, 'tooltip': tooltip, }) problem = { 'xValue': label, 'stackData': stack_data, } data.append(problem) curr_section['data'] = data d3_data.append(curr_section) return d3_data
def get_d3_section_grade_distrib(course_id, section): """ Returns the grade distribution for the problems in the `section` section in a format for the d3 code. `course_id` a string that is the course's ID. `section` an int that is a zero-based index into the course's list of sections. Navigates to the section specified to find all the problems associated with that section and then finds the grade distribution for those problems. Finally returns an object formated the way the d3_stacked_bar_graph.js expects its data object to be in. If this is requested multiple times quickly for the same course, it is better to call get_d3_problem_grade_distrib and pick out the sections of interest. Returns an array of dicts with the following keys (taken from d3_stacked_bar_graph.js's documentation) 'xValue' - Corresponding value for the x-axis 'stackData' - Array of objects with key, value pairs that represent a bar: 'color' - Defines what "color" the bar will map to 'value' - Maps to the height of the bar, along the y-axis 'tooltip' - (Optional) Text to display on mouse hover """ # Retrieve course object down to problems course = modulestore().get_instance( course_id, CourseDescriptor.id_to_location(course_id), depth=4) problem_set = [] problem_info = {} c_subsection = 0 for subsection in course.get_children()[section].get_children(): c_subsection += 1 c_unit = 0 for unit in subsection.get_children(): c_unit += 1 c_problem = 0 for child in unit.get_children(): if (child.location.category == 'problem'): c_problem += 1 problem_set.append(child.location.url()) problem_info[child.location.url()] = { 'id': child.location.url(), 'x_value': "P{0}.{1}.{2}".format(c_subsection, c_unit, c_problem), 'display_name': own_metadata(child).get('display_name', ''), } # Retrieve grade distribution for these problems grade_distrib = get_problem_set_grade_distrib(course_id, problem_set) d3_data = [] # Construct data for each problem to be sent to d3 for problem in problem_set: stack_data = [] if problem in grade_distrib: # Some problems have no data because students have not tried them yet. max_grade = float(grade_distrib[problem]['max_grade']) for (grade, count_grade) in grade_distrib[problem]['grade_distrib']: percent = 0.0 if max_grade > 0: percent = (grade * 100.0) / max_grade # Construct tooltip for problem in grade distibution view tooltip = _( "{problem_info_x} {problem_info_n} - {count_grade} {students} ({percent:.0f}%: {grade:.0f}/{max_grade:.0f} {questions})" ).format( problem_info_x=problem_info[problem]['x_value'], count_grade=count_grade, students=_("students"), percent=percent, problem_info_n=problem_info[problem]['display_name'], grade=grade, max_grade=max_grade, questions=_("questions"), ) stack_data.append({ 'color': percent, 'value': count_grade, 'tooltip': tooltip, }) d3_data.append({ 'xValue': problem_info[problem]['x_value'], 'stackData': stack_data, }) return d3_data
def handle(self, *args, **options): if len(args) != 1: raise CommandError( "check_course requires one argument: <location>") loc_str = args[0] loc = CourseDescriptor.id_to_location(loc_str) store = modulestore() # setup a request cache so we don't throttle the DB with all the metadata inheritance requests store.set_modulestore_configuration({ 'metadata_inheritance_cache_subsystem': CACHE, 'request_cache': RequestCache.get_request_cache() }) course = store.get_item(loc, 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.url()) 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.url()] 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([ 'i4x', course.location.org, course.location.course, 'discussion', None, None ]) for item in queried_discussion_items: if item.location.url() not in discussion_items: print 'Found dangling discussion module = {0}'.format( item.location.url())
def test_export_course(self): module_store = modulestore('direct') draft_store = modulestore('draft') content_store = contentstore() import_from_xml(module_store, 'common/test/data/', ['full']) location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012') # get a vertical (and components in it) to put into 'draft' vertical = module_store.get_item(Location(['i4x', 'edX', 'full', 'vertical', 'vertical_66', None]), depth=1) draft_store.clone_item(vertical.location, vertical.location) # We had a bug where orphaned draft nodes caused export to fail. This is here to cover that case. draft_store.clone_item(vertical.location, Location(['i4x', 'edX', 'full', 'vertical', 'no_references', 'draft'])) for child in vertical.get_children(): draft_store.clone_item(child.location, child.location) root_dir = path(mkdtemp_clean()) # now create a private vertical private_vertical = draft_store.clone_item(vertical.location, Location(['i4x', 'edX', 'full', 'vertical', 'a_private_vertical', None])) # add private to list of children sequential = module_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None])) private_location_no_draft = private_vertical.location.replace(revision=None) module_store.update_children(sequential.location, sequential.children + [private_location_no_draft.url()]) # read back the sequential, to make sure we have a pointer to sequential = module_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None])) self.assertIn(private_location_no_draft.url(), sequential.children) print 'Exporting to tempdir = {0}'.format(root_dir) # export out to a tempdir export_to_xml(module_store, content_store, location, root_dir, 'test_export', draft_modulestore=draft_store) # check for static tabs self.verify_content_existence(module_store, root_dir, location, 'tabs', 'static_tab', '.html') # check for custom_tags self.verify_content_existence(module_store, root_dir, location, 'info', 'course_info', '.html') # check for custom_tags self.verify_content_existence(module_store, root_dir, location, 'custom_tags', 'custom_tag_template') # check for graiding_policy.json filesystem = OSFS(root_dir / 'test_export/policies/6.002_Spring_2012') self.assertTrue(filesystem.exists('grading_policy.json')) course = module_store.get_item(location) # compare what's on disk compared to what we have in our course with filesystem.open('grading_policy.json', 'r') as grading_policy: on_disk = loads(grading_policy.read()) self.assertEqual(on_disk, course.grading_policy) #check for policy.json self.assertTrue(filesystem.exists('policy.json')) # compare what's on disk to what we have in the course module with filesystem.open('policy.json', 'r') as course_policy: on_disk = loads(course_policy.read()) self.assertIn('course/6.002_Spring_2012', on_disk) self.assertEqual(on_disk['course/6.002_Spring_2012'], own_metadata(course)) # remove old course delete_course(module_store, content_store, location) # reimport import_from_xml(module_store, root_dir, ['test_export'], draft_store=draft_store) items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None])) self.assertGreater(len(items), 0) for descriptor in items: # don't try to look at private verticals. Right now we're running # the service in non-draft aware if getattr(descriptor, 'is_draft', False): print "Checking {0}....".format(descriptor.location.url()) resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()})) self.assertEqual(resp.status_code, 200) # verify that we have the content in the draft store as well vertical = draft_store.get_item(Location(['i4x', 'edX', 'full', 'vertical', 'vertical_66', None]), depth=1) self.assertTrue(getattr(vertical, 'is_draft', False)) for child in vertical.get_children(): self.assertTrue(getattr(child, 'is_draft', False)) # make sure that we don't have a sequential that is in draft mode sequential = draft_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None])) self.assertFalse(getattr(sequential, 'is_draft', False)) # verify that we have the private vertical test_private_vertical = draft_store.get_item(Location(['i4x', 'edX', 'full', 'vertical', 'vertical_66', None])) self.assertTrue(getattr(test_private_vertical, 'is_draft', False)) # make sure the textbook survived the export/import course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None])) self.assertGreater(len(course.textbooks), 0) shutil.rmtree(root_dir)
def _has_staff_access_to_course_id(user, course_id): """Helper method that takes a course_id instead of a course name""" loc = CourseDescriptor.id_to_location(course_id) return _has_staff_access_to_location(user, loc, course_id)
def handle(self, *args, **options): # Will only generate a certificate if the current # status is in the unavailable state, can be set # to something else with the force flag if options['force']: valid_statuses = getattr(CertificateStatuses, options['force']) else: valid_statuses = [CertificateStatuses.unavailable] # Print update after this many students STATUS_INTERVAL = 500 if options['course']: ended_courses = [options['course']] else: # Find all courses that have ended ended_courses = [] for course_id in [ course # all courses in COURSE_LISTINGS for sub in settings.COURSE_LISTINGS for course in settings.COURSE_LISTINGS[sub] ]: course_loc = CourseDescriptor.id_to_location(course_id) course = modulestore().get_instance(course_id, course_loc) if course.has_ended(): ended_courses.append(course_id) for course_id in ended_courses: # prefetch all chapters/sequentials by saying depth=2 course = modulestore().get_instance( course_id, CourseDescriptor.id_to_location(course_id), depth=2) print "Fetching enrolled students for {0}".format(course_id) enrolled_students = User.objects.filter( courseenrollment__course_id=course_id).prefetch_related( "groups").order_by('username') xq = XQueueCertInterface() total = enrolled_students.count() count = 0 start = datetime.datetime.now(UTC) for student in enrolled_students: count += 1 if count % STATUS_INTERVAL == 0: # Print a status update with an approximation of # how much time is left based on how long the last # interval took diff = datetime.datetime.now(UTC) - start timeleft = diff * (total - count) / STATUS_INTERVAL hours, remainder = divmod(timeleft.seconds, 3600) minutes, seconds = divmod(remainder, 60) print "{0}/{1} completed ~{2:02}:{3:02}m remaining".format( count, total, hours, minutes) start = datetime.datetime.now(UTC) if certificate_status_for_student( student, course_id)['status'] in valid_statuses: if not options['noop']: # Add the certificate request to the queue ret = xq.add_cert(student, course_id, course=course) if ret == 'generating': print '{0} - {1}'.format(student, ret)
def load_course(self, course_dir, course_ids, tracker): """ Load a course into this module store course_path: Course directory name returns a CourseDescriptor for the course """ log.debug( '========> Starting course import from {0}'.format(course_dir)) with open(self.data_dir / course_dir / "course.xml") as course_file: # VS[compat] # TODO (cpennington): Remove this once all fall 2012 courses have # been imported into the cms from xml course_file = StringIO( clean_out_mako_templating(course_file.read())) course_data = etree.parse(course_file, parser=edx_xml_parser).getroot() org = course_data.get('org') if org is None: msg = ("No 'org' attribute set for course in {dir}. " "Using default 'edx'".format(dir=course_dir)) log.warning(msg) tracker(msg) org = 'edx' course = course_data.get('course') if course is None: msg = ("No 'course' attribute set for course in {dir}." " Using default '{default}'".format(dir=course_dir, default=course_dir)) log.warning(msg) tracker(msg) course = course_dir url_name = course_data.get('url_name', course_data.get('slug')) policy_dir = None if url_name: policy_dir = self.data_dir / course_dir / 'policies' / url_name policy_path = policy_dir / 'policy.json' policy = self.load_policy(policy_path, tracker) # VS[compat]: remove once courses use the policy dirs. if policy == {}: old_policy_path = self.data_dir / course_dir / 'policies' / '{0}.json'.format( url_name) policy = self.load_policy(old_policy_path, tracker) else: policy = {} # VS[compat] : 'name' is deprecated, but support it for now... if course_data.get('name'): url_name = Location.clean(course_data.get('name')) tracker("'name' is deprecated for module xml. Please use " "display_name and url_name.") else: raise ValueError( "Can't load a course without a 'url_name' " "(or 'name') set. Set url_name.") course_id = CourseDescriptor.make_id(org, course, url_name) if course_ids is not None and course_id not in course_ids: return None def get_policy(usage_id): """ Return the policy dictionary to be applied to the specified XBlock usage """ return policy.get(policy_key(usage_id), {}) system = ImportSystem( xmlstore=self, course_id=course_id, course_dir=course_dir, error_tracker=tracker, parent_tracker=self.parent_trackers[course_id], load_error_modules=self.load_error_modules, get_policy=get_policy, mixins=self.xblock_mixins, default_class=self.default_class, select=self.xblock_select, field_data=self.field_data, ) course_descriptor = system.process_xml( etree.tostring(course_data, encoding='unicode')) # If we fail to load the course, then skip the rest of the loading steps if isinstance(course_descriptor, ErrorDescriptor): return course_descriptor # NOTE: The descriptors end up loading somewhat bottom up, which # breaks metadata inheritance via get_children(). Instead # (actually, in addition to, for now), we do a final inheritance pass # after we have the course descriptor. compute_inherited_metadata(course_descriptor) # now import all pieces of course_info which is expected to be stored # in <content_dir>/info or <content_dir>/info/<url_name> self.load_extra_content(system, course_descriptor, 'course_info', self.data_dir / course_dir / 'info', course_dir, url_name) # now import all static tabs which are expected to be stored in # in <content_dir>/tabs or <content_dir>/tabs/<url_name> self.load_extra_content(system, course_descriptor, 'static_tab', self.data_dir / course_dir / 'tabs', course_dir, url_name) self.load_extra_content(system, course_descriptor, 'custom_tag_template', self.data_dir / course_dir / 'custom_tags', course_dir, url_name) self.load_extra_content(system, course_descriptor, 'about', self.data_dir / course_dir / 'about', course_dir, url_name) log.debug('========> Done with course import from {0}'.format( course_dir)) return course_descriptor
def course_from_id(course_id): """Return the CourseDescriptor corresponding to this course_id""" course_loc = CourseDescriptor.id_to_location(course_id) return modulestore().get_instance(course_id, course_loc)
def get_html(self): """ Renders parameters to template. """ # LTI provides a list of default parameters that might be passed as # part of the POST data. These parameters should not be prefixed. # Likewise, The creator of an LTI link can add custom key/value parameters # to a launch which are to be included with the launch of the LTI link. # In this case, we will automatically add `custom_` prefix before this parameters. # See http://www.imsglobal.org/LTI/v1p1p1/ltiIMGv1p1p1.html#_Toc316828520 PARAMETERS = [ "lti_message_type", "lti_version", "resource_link_id", "resource_link_title", "resource_link_description", "user_id", "user_image", "roles", "lis_person_name_given", "lis_person_name_family", "lis_person_name_full", "lis_person_contact_email_primary", "lis_person_sourcedid", "role_scope_mentor", "context_id", "context_type", "context_title", "context_label", "launch_presentation_locale", "launch_presentation_document_target", "launch_presentation_css_url", "launch_presentation_width", "launch_presentation_height", "launch_presentation_return_url", "tool_consumer_info_product_family_code", "tool_consumer_info_version", "tool_consumer_instance_guid", "tool_consumer_instance_name", "tool_consumer_instance_description", "tool_consumer_instance_url", "tool_consumer_instance_contact_email", ] # Obtains client_key and client_secret credentials from current course: course_id = self.course_id course_location = CourseDescriptor.id_to_location(course_id) course = self.descriptor.runtime.modulestore.get_item(course_location) client_key = client_secret = '' for lti_passport in course.lti_passports: try: lti_id, key, secret = [ i.strip() for i in lti_passport.split(':') ] except ValueError: raise LTIError('Could not parse LTI passport: {0!r}. \ Should be "id:key:secret" string.'.format(lti_passport)) if lti_id == self.lti_id.strip(): client_key, client_secret = key, secret break # parsing custom parameters to dict custom_parameters = {} for custom_parameter in self.custom_parameters: try: param_name, param_value = [ p.strip() for p in custom_parameter.split('=', 1) ] except ValueError: raise LTIError('Could not parse custom parameter: {0!r}. \ Should be "x=y" string.'.format(custom_parameter)) # LTI specs: 'custom_' should be prepended before each custom parameter, as pointed in link above. if param_name not in PARAMETERS: param_name = 'custom_' + param_name custom_parameters[unicode(param_name)] = unicode(param_value) input_fields = self.oauth_params(custom_parameters, client_key, client_secret) context = { 'input_fields': input_fields, # these params do not participate in oauth signing 'launch_url': self.launch_url.strip(), 'element_id': self.location.html_id(), 'element_class': self.category, 'open_in_a_new_page': self.open_in_a_new_page, 'display_name': self.display_name, } return self.system.render_template('lti.html', context)
def get_d3_sequential_open_distrib(course_id): """ Returns how many students opened a sequential/subsection for each section, data already in format for d3 function. `course_id` the course ID for the course interested in Returns an array in the order of the sections and each dict has: 'display_name' - display name for the section 'data' - data for the d3_stacked_bar_graph function of how many students opened each sequential/subsection """ sequential_open_distrib = get_sequential_open_distrib(course_id) d3_data = [] # Retrieve course object down to subsection course = modulestore().get_instance( course_id, CourseDescriptor.id_to_location(course_id), depth=2) # Iterate through sections, subsections for section in course.get_children(): curr_section = {} curr_section['display_name'] = own_metadata(section).get( 'display_name', '') data = [] c_subsection = 0 # Construct data for each subsection to be sent to d3 for subsection in section.get_children(): c_subsection += 1 subsection_name = own_metadata(subsection).get('display_name', '') num_students = 0 if subsection.location.url() in sequential_open_distrib: num_students = sequential_open_distrib[ subsection.location.url()] stack_data = [] # Tooltip parameters for subsection in open_distribution view tooltip = { 'type': 'subsection', 'num_students': num_students, 'subsection_num': c_subsection, 'subsection_name': subsection_name } stack_data.append({ 'color': 0, 'value': num_students, 'tooltip': tooltip, 'module_url': subsection.location.url(), }) subsection = { 'xValue': "SS {0}".format(c_subsection), 'stackData': stack_data, } data.append(subsection) curr_section['data'] = data d3_data.append(curr_section) return d3_data