def update_parent_if_moved(self, original_parent_location, published_version, delete_draft_only, user_id): """ Update parent of an item if it has moved. Arguments: original_parent_location (BlockUsageLocator) : Original parent block locator. published_version (dict) : Published version of the block. delete_draft_only (function) : A callback function to delete draft children if it was moved. user_id (int) : User id """ for child_location in published_version.get('definition', {}).get('children', []): item_location = UsageKey.from_string( child_location).map_into_course( original_parent_location.course_key) try: source_item = self.get_item(item_location) except ItemNotFoundError: log.error('Unable to find the item %s', unicode(item_location)) return if source_item.parent and source_item.parent.block_id != original_parent_location.block_id: if self.update_item_parent(item_location, original_parent_location, source_item.parent, user_id): delete_draft_only(Location.from_string(child_location))
def convert_item(item, to_be_deleted): """ Convert the subtree """ # collect the children's ids for future processing next_tier = [] for child in item.get('definition', {}).get('children', []): child_loc = Location.from_string(child) next_tier.append(child_loc.to_deprecated_son()) # insert a new DRAFT version of the item item['_id']['revision'] = MongoRevisionKey.draft # ensure keys are in fixed and right order before inserting item['_id'] = self._id_dict_to_son(item['_id']) bulk_record = self._get_bulk_ops_record(location.course_key) bulk_record.dirty = True try: self.collection.insert(item) except pymongo.errors.DuplicateKeyError: # prevent re-creation of DRAFT versions, unless explicitly requested to ignore if not ignore_if_draft: raise DuplicateItemError(item['_id'], self, 'collection') # delete the old PUBLISHED version if requested if delete_published: item['_id']['revision'] = MongoRevisionKey.published to_be_deleted.append(item['_id']) return next_tier
def get_students_problem_grades(request, csv=False): """ Get a list of students and grades for a particular problem. If 'csv' is False, returns a dict of student's name: username: grade: percent. If 'csv' is True, returns a header array, and an array of arrays in the format: student names, usernames, grades, percents for CSV download. """ module_state_key = Location.from_string(request.GET.get('module_id')) csv = request.GET.get('csv') # Query for "problem grades" students students = models.StudentModule.objects.select_related('student').filter( module_state_key=module_state_key, module_type__exact='problem', grade__isnull=False, ).values('student__username', 'student__profile__name', 'grade', 'max_grade').order_by('student__profile__name') results = [] if not csv: # Restrict screen list length # Adding 1 so can tell if list is larger than MAX_SCREEN_LIST_LENGTH # without doing another select. for student in students[0:MAX_SCREEN_LIST_LENGTH + 1]: student_dict = { 'name': student['student__profile__name'], 'username': student['student__username'], 'grade': student['grade'], } student_dict['percent'] = 0 if student['max_grade'] > 0: student_dict['percent'] = round(student['grade'] * 100 / student['max_grade']) results.append(student_dict) max_exceeded = False if len(results) > MAX_SCREEN_LIST_LENGTH: # Remove the last item so list length is exactly MAX_SCREEN_LIST_LENGTH del results[-1] max_exceeded = True response_payload = { 'results': results, 'max_exceeded': max_exceeded, } return JsonResponse(response_payload) else: tooltip = request.GET.get('tooltip') filename = sanitize_filename(tooltip[:tooltip.rfind(' - ')]) header = [_("Name"), _("Username"), _("Grade"), _("Percent")] for student in students: percent = 0 if student['max_grade'] > 0: percent = round(student['grade'] * 100 / student['max_grade']) results.append([student['student__profile__name'], student['student__username'], student['grade'], percent]) response = create_csv_response(filename, header, results) return response
def problem_location(problem_url_name, course_key=None): """ Create an internal location for a test problem. """ if "i4x:" in problem_url_name: return Location.from_string(problem_url_name) elif course_key: return course_key.make_usage_key('problem', problem_url_name) else: return TEST_COURSE_KEY.make_usage_key('problem', problem_url_name)
def get_students_opened_subsection(request, csv=False): """ Get a list of students that opened a particular subsection. If 'csv' is False, returns a dict of student's name: username. If 'csv' is True, returns a header array, and an array of arrays in the format: student names, usernames for CSV download. """ module_state_key = Location.from_string(request.GET.get('module_id')) csv = request.GET.get('csv') # Query for "opened a subsection" students students = models.StudentModule.objects.select_related('student').filter( module_state_key__exact=module_state_key, module_type__exact='sequential', ).values('student__username', 'student__profile__name').order_by('student__profile__name') results = [] if not csv: # Restrict screen list length # Adding 1 so can tell if list is larger than MAX_SCREEN_LIST_LENGTH # without doing another select. for student in students[0:MAX_SCREEN_LIST_LENGTH + 1]: results.append({ 'name': student['student__profile__name'], 'username': student['student__username'], }) max_exceeded = False if len(results) > MAX_SCREEN_LIST_LENGTH: # Remove the last item so list length is exactly MAX_SCREEN_LIST_LENGTH del results[-1] max_exceeded = True response_payload = { 'results': results, 'max_exceeded': max_exceeded, } return JsonResponse(response_payload) else: tooltip = request.GET.get('tooltip') # Subsection name is everything after 3rd space in tooltip filename = sanitize_filename(' '.join(tooltip.split(' ')[3:])) header = [_("Name"), _("Username")] for student in students: results.append([ student['student__profile__name'], student['student__username'] ]) response = create_csv_response(filename, header, results) return response
def to_python(self, location): # lint-amnesty, pylint: disable=arguments-differ """ Deserialize to a UsageKey instance: for now it's a location missing the run """ assert isinstance(location, (type(None), str, UsageKey)) if location == '': return None if isinstance(location, str): location = super().to_python(location) return Location.from_string(location) else: return location
def to_python(self, location): """ Deserialize to a UsageKey instance: for now it's a location missing the run """ assert isinstance(location, (NoneType, basestring, UsageKey)) if location == '': return None if isinstance(location, basestring): location = super(UsageKeyField, self).to_python(location) return Location.from_string(location) else: return location
def get_course_child_key(content_id): """ Returns course child key """ try: content_id = UsageKey.from_string(content_id) except InvalidKeyError: try: content_id = Location.from_string(content_id) except (InvalidLocationError, InvalidKeyError): content_id = None return content_id
def get_students_opened_subsection(request, csv=False): """ Get a list of students that opened a particular subsection. If 'csv' is False, returns a dict of student's name: username. If 'csv' is True, returns a header array, and an array of arrays in the format: student names, usernames for CSV download. """ module_state_key = Location.from_string(request.GET.get('module_id')) csv = request.GET.get('csv') # Query for "opened a subsection" students students = models.StudentModule.objects.select_related('student').filter( module_state_key__exact=module_state_key, module_type__exact='sequential', ).values('student__username', 'student__profile__name').order_by('student__profile__name') results = [] if not csv: # Restrict screen list length # Adding 1 so can tell if list is larger than MAX_SCREEN_LIST_LENGTH # without doing another select. for student in students[0:MAX_SCREEN_LIST_LENGTH + 1]: results.append({ 'name': student['student__profile__name'], 'username': student['student__username'], }) max_exceeded = False if len(results) > MAX_SCREEN_LIST_LENGTH: # Remove the last item so list length is exactly MAX_SCREEN_LIST_LENGTH del results[-1] max_exceeded = True response_payload = { 'results': results, 'max_exceeded': max_exceeded, } return JsonResponse(response_payload) else: tooltip = request.GET.get('tooltip') # Subsection name is everything after 3rd space in tooltip filename = sanitize_filename(' '.join(tooltip.split(' ')[3:])) header = [_("Name"), _("Username")] for student in students: results.append([student['student__profile__name'], student['student__username']]) response = create_csv_response(filename, header, results) return response
def delete_draft_only(root_location): """ Helper function that calls delete on the specified location if a draft version of the item exists. If no draft exists, this function recursively calls itself on the children of the item. """ query = root_location.to_deprecated_son(prefix='_id.') del query['_id.revision'] versions_found = self.collection.find( query, { '_id': True, 'definition.children': True }, sort=[SORT_REVISION_FAVOR_DRAFT]) # If 2 versions versions exist, we can assume one is a published version. Go ahead and do the delete # of the draft version. if versions_found.count() > 1: # Moving a child from published parent creates a draft of the parent and moved child. published_version = [ version for version in versions_found if version.get( '_id').get('revision') != MongoRevisionKey.draft ] if len(published_version) > 0: # This change makes sure that parents are updated too i.e. an item will have only one parent. self.update_parent_if_moved(root_location, published_version[0], delete_draft_only, user_id) self._delete_subtree(root_location, [as_draft], draft_only=True) elif versions_found.count() == 1: # Since this method cannot be called on something in DIRECT_ONLY_CATEGORIES and we call # delete_subtree as soon as we find an item with a draft version, if there is only 1 version # it must be published (since adding a child to a published item creates a draft of the parent). item = versions_found[0] assert item.get('_id').get( 'revision') != MongoRevisionKey.draft for child in item.get('definition', {}).get('children', []): child_loc = Location.from_string(child) delete_draft_only(child_loc)
class TestAsideKeys(TestCase): """Test of Aside keys.""" @ddt.data(*itertools.product([ AsideUsageKeyV1, AsideUsageKeyV2, ], [ Location.from_string('i4x://org/course/cat/name'), BlockUsageLocator(CourseLocator('org', 'course', 'run'), 'block_type', 'block_id'), ], ['aside', 'aside_b'])) @ddt.unpack def test_usage_round_trip_deserialized(self, key_class, usage_key, aside_type): key = key_class(usage_key, aside_type) serialized = text_type(key) deserialized = AsideUsageKey.from_string(serialized) self.assertEqual(key, deserialized) self.assertEqual(usage_key, key.usage_key, usage_key) self.assertEqual(usage_key, deserialized.usage_key) self.assertEqual(aside_type, key.aside_type) self.assertEqual(aside_type, deserialized.aside_type) @ddt.data( 'aside-usage-v1:i4x://org/course/cat/name::aside', 'aside-usage-v1:block-v1:org+course+cat+type@block_type+block@name::aside', 'aside-usage-v2:lib-block-v1$:$:+-+branch@-+version@000000000000000000000000+type@-+block@-::0', 'aside-usage-v2:i4x$://-/-/-/$:$:-::0', 'aside-usage-v2:i4x$://-/-/-/$:$:$:-::0', 'aside-usage-v2:i4x$://-/-/$:$:$:$:$:/-::0', ) def test_usage_round_trip_serialized(self, aside_key): deserialized = AsideUsageKey.from_string(aside_key) serialized = text_type(deserialized) self.assertEqual(aside_key, serialized) @ddt.data(*itertools.product([ AsideDefinitionKeyV1, AsideDefinitionKeyV2, ], [ DefinitionLocator('block_type', 'abcd1234abcd1234abcd1234'), ], ['aside', 'aside_b'])) @ddt.unpack def test_definition_round_trip_deserialized(self, key_class, definition_key, aside_type): key = key_class(definition_key, aside_type) serialized = text_type(key) deserialized = AsideDefinitionKey.from_string(serialized) self.assertEqual(key, deserialized) self.assertEqual(definition_key, key.definition_key, definition_key) self.assertEqual(definition_key, deserialized.definition_key) self.assertEqual(aside_type, key.aside_type) self.assertEqual(aside_type, deserialized.aside_type) @ddt.data( 'aside-def-v1:def-v1:abcd1234abcd1234abcd1234+type@block_type::aside', 'aside-def-v2:def-v1$:abcd1234abcd1234abcd1234+type@block_type::aside') def test_definition_round_trip_serialized(self, aside_key): deserialized = AsideDefinitionKey.from_string(aside_key) serialized = text_type(deserialized) self.assertEqual(aside_key, serialized) @ddt.data(*itertools.product( [ AsideUsageKeyV1, AsideUsageKeyV2, ], [ ('aside_type', 'bside'), ('usage_key', BlockUsageLocator(CourseLocator('borg', 'horse', 'gun'), 'lock_type', 'lock_id')), ('block_id', 'lock_id'), ('block_type', 'lock_type'), # BlockUsageLocator can't `replace` a definition_key, so skip for now # ('definition_key', DefinitionLocator('block_type', 'abcd1234abcd1234abcd1234')), ('course_key', CourseLocator('borg', 'horse', 'gun')), ])) @ddt.unpack def test_usage_key_replace(self, key_class, attr_value): attr, value = attr_value key = key_class( BlockUsageLocator(CourseLocator('org', 'course', 'run'), 'block_type', 'block_id'), 'aside') new_key = key.replace(**{attr: value}) self.assertEqual(getattr(new_key, attr), value) @ddt.data(*itertools.product([ AsideDefinitionKeyV1, AsideDefinitionKeyV2, ], [ ('aside_type', 'bside'), ('definition_key', DefinitionLocator('block_type', 'abcd1234abcd1234abcd1234')), ('block_type', 'lock_type'), ])) @ddt.unpack def test_definition_key_replace(self, key_class, attr_value): attr, value = attr_value key = key_class( DefinitionLocator('block_type', 'abcd1234abcd1234abcd1234'), 'aside') new_key = key.replace(**{attr: value}) self.assertEqual(getattr(new_key, attr), value)
class TestAsideKeys(TestCase): """Test of Aside keys.""" @ddt.data( (Location.from_string('i4x://org/course/cat/name'), 'aside'), (BlockUsageLocator(CourseLocator('org', 'course', 'run'), 'block_type', 'block_id'), 'aside'), ) @ddt.unpack def test_usage_round_trip_deserialized(self, usage_key, aside_type): key = AsideUsageKeyV1(usage_key, aside_type) serialized = unicode(key) deserialized = AsideUsageKey.from_string(serialized) self.assertEquals(key, deserialized) self.assertEquals(usage_key, key.usage_key, usage_key) self.assertEquals(usage_key, deserialized.usage_key) self.assertEquals(aside_type, key.aside_type) self.assertEquals(aside_type, deserialized.aside_type) @ddt.data( 'aside-usage-v1:i4x://org/course/cat/name::aside', 'aside-usage-v1:block-v1:org+course+cat+type@block_type+block@name::aside', ) def test_usage_round_trip_serialized(self, aside_key): deserialized = AsideUsageKey.from_string(aside_key) serialized = unicode(deserialized) self.assertEquals(aside_key, serialized) @ddt.data( (DefinitionLocator('block_type', 'abcd1234abcd1234abcd1234'), 'aside'), ) @ddt.unpack def test_definition_round_trip_deserialized(self, definition_key, aside_type): key = AsideDefinitionKeyV1(definition_key, aside_type) serialized = unicode(key) deserialized = AsideDefinitionKey.from_string(serialized) self.assertEquals(key, deserialized) self.assertEquals(definition_key, key.definition_key, definition_key) self.assertEquals(definition_key, deserialized.definition_key) self.assertEquals(aside_type, key.aside_type) self.assertEquals(aside_type, deserialized.aside_type) @ddt.data( 'aside-def-v1:def-v1:abcd1234abcd1234abcd1234+type@block_type::aside') def test_definition_round_trip_serialized(self, aside_key): deserialized = AsideDefinitionKey.from_string(aside_key) serialized = unicode(deserialized) self.assertEquals(aside_key, serialized) @ddt.data( ('aside_type', 'bside'), ('usage_key', BlockUsageLocator(CourseLocator('borg', 'horse', 'gun'), 'lock_type', 'lock_id')), ('block_id', 'lock_id'), ('block_type', 'lock_type'), # BlockUsageLocator can't `replace` a definition_key, so skip for now # ('definition_key', DefinitionLocator('block_type', 'abcd1234abcd1234abcd1234')), ('course_key', CourseLocator('borg', 'horse', 'gun')), ) @ddt.unpack def test_usage_key_replace(self, attr, value): key = AsideUsageKeyV1( BlockUsageLocator(CourseLocator('org', 'course', 'run'), 'block_type', 'block_id'), 'aside') new_key = key.replace(**{attr: value}) self.assertEquals(getattr(new_key, attr), value) @ddt.data( ('aside_type', 'bside'), ('definition_key', DefinitionLocator('block_type', 'abcd1234abcd1234abcd1234')), ('block_type', 'lock_type'), ) @ddt.unpack def test_definition_key_replace(self, attr, value): key = AsideDefinitionKeyV1( DefinitionLocator('block_type', 'abcd1234abcd1234abcd1234'), 'aside') new_key = key.replace(**{attr: value}) self.assertEquals(getattr(new_key, attr), value)