def test_update_section_grader_type(self): # Get the descriptor and the section_grader_type and assert they are the default values descriptor = get_modulestore(self.course.location).get_item( self.course.location) section_grader_type = CourseGradingModel.get_section_grader_type( self.course_locator) self.assertEqual('notgraded', section_grader_type['graderType']) self.assertEqual(None, descriptor.format) self.assertEqual(False, descriptor.graded) # Change the default grader type to Homework, which should also mark the section as graded CourseGradingModel.update_section_grader_type(self.course, 'Homework', self.user) descriptor = get_modulestore(self.course.location).get_item( self.course.location) section_grader_type = CourseGradingModel.get_section_grader_type( self.course_locator) self.assertEqual('Homework', section_grader_type['graderType']) self.assertEqual('Homework', descriptor.format) self.assertEqual(True, descriptor.graded) # Change the grader type back to notgraded, which should also unmark the section as graded CourseGradingModel.update_section_grader_type(self.course, 'notgraded', self.user) descriptor = get_modulestore(self.course.location).get_item( self.course.location) section_grader_type = CourseGradingModel.get_section_grader_type( self.course_locator) self.assertEqual('notgraded', section_grader_type['graderType']) self.assertEqual(None, descriptor.format) self.assertEqual(False, descriptor.graded)
def test_update_section_grader_type(self): # Get the descriptor and the section_grader_type and assert they are the default values descriptor = get_modulestore(self.course.location).get_item(self.course.location) section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location) self.assertEqual("Not Graded", section_grader_type["graderType"]) self.assertEqual(None, descriptor.lms.format) self.assertEqual(False, descriptor.lms.graded) # Change the default grader type to Homework, which should also mark the section as graded CourseGradingModel.update_section_grader_type(self.course.location, {"graderType": "Homework"}) descriptor = get_modulestore(self.course.location).get_item(self.course.location) section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location) self.assertEqual("Homework", section_grader_type["graderType"]) self.assertEqual("Homework", descriptor.lms.format) self.assertEqual(True, descriptor.lms.graded) # Change the grader type back to Not Graded, which should also unmark the section as graded CourseGradingModel.update_section_grader_type(self.course.location, {"graderType": "Not Graded"}) descriptor = get_modulestore(self.course.location).get_item(self.course.location) section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location) self.assertEqual("Not Graded", section_grader_type["graderType"]) self.assertEqual(None, descriptor.lms.format) self.assertEqual(False, descriptor.lms.graded)
def test_update_section_grader_type(self): # Get the descriptor and the section_grader_type and assert they are the default values descriptor = modulestore().get_item(self.course.location) section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location) self.assertEqual('notgraded', section_grader_type['graderType']) self.assertEqual(None, descriptor.format) self.assertEqual(False, descriptor.graded) # Change the default grader type to Homework, which should also mark the section as graded CourseGradingModel.update_section_grader_type(self.course, 'Homework', self.user) descriptor = modulestore().get_item(self.course.location) section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location) self.assertEqual('Homework', section_grader_type['graderType']) self.assertEqual('Homework', descriptor.format) self.assertEqual(True, descriptor.graded) # Change the grader type back to notgraded, which should also unmark the section as graded CourseGradingModel.update_section_grader_type(self.course, 'notgraded', self.user) descriptor = modulestore().get_item(self.course.location) section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location) self.assertEqual('notgraded', section_grader_type['graderType']) self.assertEqual(None, descriptor.format) self.assertEqual(False, descriptor.graded)
def assignment_type_update(request, org, course, category, name): ''' CRUD operations on assignment types for sections and subsections and anything else gradable. ''' location = Location(['i4x', org, course, category, name]) if not has_access(request.user, location): return HttpResponseForbidden() if request.method == 'GET': return JsonResponse(CourseGradingModel.get_section_grader_type(location)) elif request.method in ('POST', 'PUT'): # post or put, doesn't matter. return JsonResponse(CourseGradingModel.update_section_grader_type(location, request.POST))
def assignment_type_update(request, org, course, category, name): ''' CRUD operations on assignment types for sections and subsections and anything else gradable. ''' location = Location(['i4x', org, course, category, name]) if not has_access(request.user, location): return HttpResponseForbidden() if request.method == 'GET': return JsonResponse(CourseGradingModel.get_section_grader_type(location)) elif request.method in ('POST', 'PUT'): # post or put, doesn't matter. return JsonResponse(CourseGradingModel.update_section_grader_type(location, request.POST))
def assignment_type_update(request, org, course, category, name): """ CRUD operations on assignment types for sections and subsections and anything else gradable. """ location = Location(["i4x", org, course, category, name]) if not has_access(request.user, location): return HttpResponseForbidden() if request.method == "GET": rsp = CourseGradingModel.get_section_grader_type(location) elif request.method in ("POST", "PUT"): # post or put, doesn't matter. rsp = CourseGradingModel.update_section_grader_type(location, request.POST) return JsonResponse(rsp)
def _save_xblock(user, xblock, data=None, children_strings=None, metadata=None, nullout=None, grader_type=None, publish=None): """ Saves xblock w/ its fields. Has special processing for grader_type, publish, and nullout and Nones in metadata. nullout means to truly set the field to None whereas nones in metadata mean to unset them (so they revert to default). """ store = modulestore() # Perform all xblock changes within a (single-versioned) transaction with store.bulk_operations(xblock.location.course_key): # Don't allow updating an xblock and discarding changes in a single operation (unsupported by UI). if publish == "discard_changes": store.revert_to_published(xblock.location, user.id) # Returning the same sort of result that we do for other save operations. In the future, # we may want to return the full XBlockInfo. return JsonResponse({'id': unicode(xblock.location)}) old_metadata = own_metadata(xblock) old_content = xblock.get_explicitly_set_fields_by_scope(Scope.content) if data: # TODO Allow any scope.content fields not just "data" (exactly like the get below this) xblock.data = data else: data = old_content['data'] if 'data' in old_content else None if children_strings is not None: children = [] for child_string in children_strings: children.append(usage_key_with_run(child_string)) # if new children have been added, remove them from their old parents new_children = set(children) - set(xblock.children) for new_child in new_children: old_parent_location = store.get_parent_location(new_child) if old_parent_location: old_parent = store.get_item(old_parent_location) old_parent.children.remove(new_child) old_parent = _update_with_callback(old_parent, user) else: # the Studio UI currently doesn't present orphaned children, so assume this is an error return JsonResponse({"error": "Invalid data, possibly caused by concurrent authors."}, 400) # make sure there are no old children that became orphans # In a single-author (no-conflict) scenario, all children in the persisted list on the server should be # present in the updated list. If there are any children that have been dropped as part of this update, # then that would be an error. # # We can be even more restrictive in a multi-author (conflict), by returning an error whenever # len(old_children) > 0. However, that conflict can still be "merged" if the dropped child had been # re-parented. Hence, the check for the parent in the any statement below. # # Note that this multi-author conflict error should not occur in modulestores (such as Split) that support # atomic write transactions. In Split, if there was another author who moved one of the "old_children" # into another parent, then that child would have been deleted from this parent on the server. However, # this is error could occur in modulestores (such as Draft) that do not support atomic write-transactions old_children = set(xblock.children) - set(children) if any( store.get_parent_location(old_child) == xblock.location for old_child in old_children ): # since children are moved as part of a single transaction, orphans should not be created return JsonResponse({"error": "Invalid data, possibly caused by concurrent authors."}, 400) # set the children on the xblock xblock.children = children # also commit any metadata which might have been passed along if nullout is not None or metadata is not None: # the postback is not the complete metadata, as there's system metadata which is # not presented to the end-user for editing. So let's use the original (existing_item) and # 'apply' the submitted metadata, so we don't end up deleting system metadata. if nullout is not None: for metadata_key in nullout: setattr(xblock, metadata_key, None) # update existing metadata with submitted metadata (which can be partial) # IMPORTANT NOTE: if the client passed 'null' (None) for a piece of metadata that means 'remove it'. If # the intent is to make it None, use the nullout field if metadata is not None: for metadata_key, value in metadata.items(): field = xblock.fields[metadata_key] if value is None: field.delete_from(xblock) else: try: value = field.from_json(value) except ValueError as verr: reason = _("Invalid data") if verr.message: reason = _("Invalid data ({details})").format(details=verr.message) return JsonResponse({"error": reason}, 400) field.write_to(xblock, value) # update the xblock and call any xblock callbacks xblock = _update_with_callback(xblock, user, old_metadata, old_content) # for static tabs, their containing course also records their display name if xblock.location.category == 'static_tab': course = store.get_course(xblock.location.course_key) # find the course's reference to this tab and update the name. static_tab = CourseTabList.get_tab_by_slug(course.tabs, xblock.location.name) # only update if changed if static_tab and static_tab['name'] != xblock.display_name: static_tab['name'] = xblock.display_name store.update_item(course, user.id) result = { 'id': unicode(xblock.location), 'data': data, 'metadata': own_metadata(xblock) } if grader_type is not None: result.update(CourseGradingModel.update_section_grader_type(xblock, grader_type, user)) # If publish is set to 'republish' and this item is not in direct only categories and has previously been published, # then this item should be republished. This is used by staff locking to ensure that changing the draft # value of the staff lock will also update the published version, but only at the unit level. if publish == 'republish' and xblock.category not in DIRECT_ONLY_CATEGORIES: if modulestore().has_published_version(xblock): publish = 'make_public' # Make public after updating the xblock, in case the caller asked for both an update and a publish. # Used by Bok Choy tests and by republishing of staff locks. if publish == 'make_public': modulestore().publish(xblock.location, user.id) # Note that children aren't being returned until we have a use case. return JsonResponse(result, encoder=EdxJSONEncoder)
def _save_item(request, usage_loc, item_location, data=None, children=None, metadata=None, nullout=None, grader_type=None, publish=None): """ Saves xblock w/ its fields. Has special processing for grader_type, publish, and nullout and Nones in metadata. nullout means to truly set the field to None whereas nones in metadata mean to unset them (so they revert to default). The item_location is still the old-style location whereas usage_loc is a BlockUsageLocator """ store = get_modulestore(item_location) try: existing_item = store.get_item(item_location) except ItemNotFoundError: if item_location.category in CREATE_IF_NOT_FOUND: # New module at this location, for pages that are not pre-created. # Used for course info handouts. store.create_and_save_xmodule(item_location) existing_item = store.get_item(item_location) else: raise except InvalidLocationError: log.error("Can't find item by location.") return JsonResponse({"error": "Can't find item by location: " + str(item_location)}, 404) old_metadata = own_metadata(existing_item) if publish: if publish == 'make_private': _xmodule_recurse(existing_item, lambda i: modulestore().unpublish(i.location)) elif publish == 'create_draft': # This clones the existing item location to a draft location (the draft is # implicit, because modulestore is a Draft modulestore) modulestore().convert_to_draft(item_location) if data: # TODO Allow any scope.content fields not just "data" (exactly like the get below this) existing_item.data = data else: data = existing_item.get_explicitly_set_fields_by_scope(Scope.content) if children is not None: children_ids = [ loc_mapper().translate_locator_to_location(BlockUsageLocator(child_locator)).url() for child_locator in children ] existing_item.children = children_ids # also commit any metadata which might have been passed along if nullout is not None or metadata is not None: # the postback is not the complete metadata, as there's system metadata which is # not presented to the end-user for editing. So let's use the original (existing_item) and # 'apply' the submitted metadata, so we don't end up deleting system metadata. if nullout is not None: for metadata_key in nullout: setattr(existing_item, metadata_key, None) # update existing metadata with submitted metadata (which can be partial) # IMPORTANT NOTE: if the client passed 'null' (None) for a piece of metadata that means 'remove it'. If # the intent is to make it None, use the nullout field if metadata is not None: for metadata_key, value in metadata.items(): field = existing_item.fields[metadata_key] if value is None: field.delete_from(existing_item) else: try: value = field.from_json(value) except ValueError: return JsonResponse({"error": "Invalid data"}, 400) field.write_to(existing_item, value) if existing_item.category == 'video': manage_video_subtitles_save(existing_item, request.user, old_metadata, generate_translation=True) # commit to datastore store.update_item(existing_item, request.user.id) result = { 'id': unicode(usage_loc), 'data': data, 'metadata': own_metadata(existing_item) } if grader_type is not None: result.update(CourseGradingModel.update_section_grader_type(existing_item, grader_type, request.user)) # Make public after updating the xblock, in case the caller asked # for both an update and a publish. if publish and publish == 'make_public': def _publish(block): # This is super gross, but prevents us from publishing something that # we shouldn't. Ideally, all modulestores would have a consistant # interface for publishing. However, as of now, only the DraftMongoModulestore # does, so we have to check for the attribute explicitly. store = get_modulestore(block.location) if hasattr(store, 'publish'): store.publish(block.location, request.user.id) _xmodule_recurse( existing_item, _publish ) # Note that children aren't being returned until we have a use case. return JsonResponse(result)
def create_xblock(parent_locator, user, category, display_name, boilerplate=None, is_entrance_exam=False): """ Performs the actual grunt work of creating items/xblocks -- knows nothing about requests, views, etc. """ store = modulestore() usage_key = usage_key_with_run(parent_locator) with store.bulk_operations(usage_key.course_key): parent = store.get_item(usage_key) dest_usage_key = usage_key.replace(category=category, name=uuid4().hex) # get the metadata, display_name, and definition from the caller metadata = {} data = None template_id = boilerplate if template_id: clz = parent.runtime.load_block_type(category) if clz is not None: template = clz.get_template(template_id) if template is not None: metadata = template.get('metadata', {}) data = template.get('data') if display_name is not None: metadata['display_name'] = display_name # We should use the 'fields' kwarg for newer module settings/values (vs. metadata or data) fields = {} # Entrance Exams: Chapter module positioning child_position = None if is_entrance_exams_enabled(): if category == 'chapter' and is_entrance_exam: fields['is_entrance_exam'] = is_entrance_exam fields['in_entrance_exam'] = True # Inherited metadata, all children will have it child_position = 0 # TODO need to fix components that are sending definition_data as strings, instead of as dicts # For now, migrate them into dicts here. if isinstance(data, basestring): data = {'data': data} created_block = store.create_child( user.id, usage_key, dest_usage_key.block_type, block_id=dest_usage_key.block_id, fields=fields, definition_data=data, metadata=metadata, runtime=parent.runtime, position=child_position, ) # Entrance Exams: Grader assignment if is_entrance_exams_enabled(): course_key = usage_key.course_key course = store.get_course(course_key) if hasattr(course, 'entrance_exam_enabled') and course.entrance_exam_enabled: if category == 'sequential' and parent_locator == course.entrance_exam_id: # Clean up any pre-existing entrance exam graders remove_entrance_exam_graders(course_key, user) grader = { "type": GRADER_TYPES['ENTRANCE_EXAM'], "min_count": 0, "drop_count": 0, "short_label": "Entrance", "weight": 0 } grading_model = CourseGradingModel.update_grader_from_json( course.id, grader, user ) CourseGradingModel.update_section_grader_type( created_block, grading_model['type'], user ) # VS[compat] cdodge: This is a hack because static_tabs also have references from the course module, so # if we add one then we need to also add it to the policy information (i.e. metadata) # we should remove this once we can break this reference from the course to static tabs if category == 'static_tab': dog_stats_api.increment( DEPRECATION_VSCOMPAT_EVENT, tags=( "location:create_xblock_static_tab", u"course:{}".format(unicode(dest_usage_key.course_key)), ) ) display_name = display_name or _("Empty") # Prevent name being None course = store.get_course(dest_usage_key.course_key) course.tabs.append( StaticTab( name=display_name, url_slug=dest_usage_key.name, ) ) store.update_item(course, user.id) return created_block
def _save_xblock(user, xblock, data=None, children_strings=None, metadata=None, nullout=None, grader_type=None, publish=None): """ Saves xblock w/ its fields. Has special processing for grader_type, publish, and nullout and Nones in metadata. nullout means to truly set the field to None whereas nones in metadata mean to unset them (so they revert to default). """ store = modulestore() # Perform all xblock changes within a (single-versioned) transaction with store.bulk_operations(xblock.location.course_key): # Don't allow updating an xblock and discarding changes in a single operation (unsupported by UI). if publish == "discard_changes": store.revert_to_published(xblock.location, user.id) # Returning the same sort of result that we do for other save operations. In the future, # we may want to return the full XBlockInfo. return JsonResponse({'id': unicode(xblock.location)}) old_metadata = own_metadata(xblock) old_content = xblock.get_explicitly_set_fields_by_scope(Scope.content) if data: # TODO Allow any scope.content fields not just "data" (exactly like the get below this) xblock.data = data else: data = old_content['data'] if 'data' in old_content else None if children_strings is not None: children = [] for child_string in children_strings: children.append(usage_key_with_run(child_string)) # if new children have been added, remove them from their old parents new_children = set(children) - set(xblock.children) for new_child in new_children: old_parent_location = store.get_parent_location(new_child) if old_parent_location: old_parent = store.get_item(old_parent_location) old_parent.children.remove(new_child) old_parent = _update_with_callback(old_parent, user) else: # the Studio UI currently doesn't present orphaned children, so assume this is an error return JsonResponse( { "error": "Invalid data, possibly caused by concurrent authors." }, 400) # make sure there are no old children that became orphans # In a single-author (no-conflict) scenario, all children in the persisted list on the server should be # present in the updated list. If there are any children that have been dropped as part of this update, # then that would be an error. # # We can be even more restrictive in a multi-author (conflict), by returning an error whenever # len(old_children) > 0. However, that conflict can still be "merged" if the dropped child had been # re-parented. Hence, the check for the parent in the any statement below. # # Note that this multi-author conflict error should not occur in modulestores (such as Split) that support # atomic write transactions. In Split, if there was another author who moved one of the "old_children" # into another parent, then that child would have been deleted from this parent on the server. However, # this is error could occur in modulestores (such as Draft) that do not support atomic write-transactions old_children = set(xblock.children) - set(children) if any( store.get_parent_location(old_child) == xblock.location for old_child in old_children): # since children are moved as part of a single transaction, orphans should not be created return JsonResponse( { "error": "Invalid data, possibly caused by concurrent authors." }, 400) # set the children on the xblock xblock.children = children # also commit any metadata which might have been passed along if nullout is not None or metadata is not None: # the postback is not the complete metadata, as there's system metadata which is # not presented to the end-user for editing. So let's use the original (existing_item) and # 'apply' the submitted metadata, so we don't end up deleting system metadata. if nullout is not None: for metadata_key in nullout: setattr(xblock, metadata_key, None) # update existing metadata with submitted metadata (which can be partial) # IMPORTANT NOTE: if the client passed 'null' (None) for a piece of metadata that means 'remove it'. If # the intent is to make it None, use the nullout field if metadata is not None: for metadata_key, value in metadata.items(): field = xblock.fields[metadata_key] if value is None: field.delete_from(xblock) else: try: value = field.from_json(value) except ValueError: return JsonResponse({"error": "Invalid data"}, 400) field.write_to(xblock, value) # update the xblock and call any xblock callbacks xblock = _update_with_callback(xblock, user, old_metadata, old_content) # for static tabs, their containing course also records their display name if xblock.location.category == 'static_tab': course = store.get_course(xblock.location.course_key) # find the course's reference to this tab and update the name. static_tab = CourseTabList.get_tab_by_slug(course.tabs, xblock.location.name) # only update if changed if static_tab and static_tab['name'] != xblock.display_name: static_tab['name'] = xblock.display_name store.update_item(course, user.id) result = { 'id': unicode(xblock.location), 'data': data, 'metadata': own_metadata(xblock) } if grader_type is not None: result.update( CourseGradingModel.update_section_grader_type( xblock, grader_type, user)) # If publish is set to 'republish' and this item is not in direct only categories and has previously been published, # then this item should be republished. This is used by staff locking to ensure that changing the draft # value of the staff lock will also update the published version, but only at the unit level. if publish == 'republish' and xblock.category not in DIRECT_ONLY_CATEGORIES: if modulestore().has_published_version(xblock): publish = 'make_public' # Make public after updating the xblock, in case the caller asked for both an update and a publish. # Used by Bok Choy tests and by republishing of staff locks. if publish == 'make_public': modulestore().publish(xblock.location, user.id) # Note that children aren't being returned until we have a use case. return JsonResponse(result, encoder=EdxJSONEncoder)
def _save_item(user, usage_key, data=None, children=None, metadata=None, nullout=None, grader_type=None, publish=None): """ Saves xblock w/ its fields. Has special processing for grader_type, publish, and nullout and Nones in metadata. nullout means to truly set the field to None whereas nones in metadata mean to unset them (so they revert to default). """ store = modulestore() try: existing_item = store.get_item(usage_key) except ItemNotFoundError: if usage_key.category in CREATE_IF_NOT_FOUND: # New module at this location, for pages that are not pre-created. # Used for course info handouts. existing_item = store.create_and_save_xmodule(usage_key, user.id) else: raise except InvalidLocationError: log.error("Can't find item by location.") return JsonResponse({"error": "Can't find item by location: " + unicode(usage_key)}, 404) old_metadata = own_metadata(existing_item) old_content = existing_item.get_explicitly_set_fields_by_scope(Scope.content) if publish: if publish == 'make_private': try: store.unpublish(existing_item.location, user.id), except ItemNotFoundError: pass elif publish == 'create_draft': try: # This recursively clones the item subtree and marks the copies as draft store.convert_to_draft(existing_item.location, user.id) except DuplicateItemError: pass if data: # TODO Allow any scope.content fields not just "data" (exactly like the get below this) existing_item.data = data else: data = old_content['data'] if 'data' in old_content else None if children is not None: children_usage_keys = [ UsageKey.from_string(child) for child in children ] existing_item.children = children_usage_keys # also commit any metadata which might have been passed along if nullout is not None or metadata is not None: # the postback is not the complete metadata, as there's system metadata which is # not presented to the end-user for editing. So let's use the original (existing_item) and # 'apply' the submitted metadata, so we don't end up deleting system metadata. if nullout is not None: for metadata_key in nullout: setattr(existing_item, metadata_key, None) # update existing metadata with submitted metadata (which can be partial) # IMPORTANT NOTE: if the client passed 'null' (None) for a piece of metadata that means 'remove it'. If # the intent is to make it None, use the nullout field if metadata is not None: for metadata_key, value in metadata.items(): field = existing_item.fields[metadata_key] if value is None: field.delete_from(existing_item) else: try: value = field.from_json(value) except ValueError: return JsonResponse({"error": "Invalid data"}, 400) field.write_to(existing_item, value) if callable(getattr(existing_item, "editor_saved", None)): existing_item.editor_saved(user, old_metadata, old_content) # commit to datastore store.update_item(existing_item, user.id) # for static tabs, their containing course also records their display name if usage_key.category == 'static_tab': course = store.get_course(usage_key.course_key) # find the course's reference to this tab and update the name. static_tab = CourseTabList.get_tab_by_slug(course.tabs, usage_key.name) # only update if changed if static_tab and static_tab['name'] != existing_item.display_name: static_tab['name'] = existing_item.display_name store.update_item(course, user.id) result = { 'id': unicode(usage_key), 'data': data, 'metadata': own_metadata(existing_item) } if grader_type is not None: result.update(CourseGradingModel.update_section_grader_type(existing_item, grader_type, user)) # Make public after updating the xblock, in case the caller asked # for both an update and a publish. if publish and publish == 'make_public': modulestore().publish(existing_item.location, user.id) # Note that children aren't being returned until we have a use case. return JsonResponse(result)
def _save_item(request, usage_loc, item_location, data=None, children=None, metadata=None, nullout=None, grader_type=None, publish=None): """ Saves xblock w/ its fields. Has special processing for grader_type, publish, and nullout and Nones in metadata. nullout means to truly set the field to None whereas nones in metadata mean to unset them (so they revert to default). The item_location is still the old-style location whereas usage_loc is a BlockUsageLocator """ store = get_modulestore(item_location) try: existing_item = store.get_item(item_location) except ItemNotFoundError: if item_location.category in CREATE_IF_NOT_FOUND: # New module at this location, for pages that are not pre-created. # Used for course info handouts. store.create_and_save_xmodule(item_location) existing_item = store.get_item(item_location) else: raise except InvalidLocationError: log.error("Can't find item by location.") return JsonResponse({"error": "Can't find item by location: " + str(item_location)}, 404) if publish: if publish == 'make_private': _xmodule_recurse(existing_item, lambda i: modulestore().unpublish(i.location)) elif publish == 'create_draft': # This clones the existing item location to a draft location (the draft is # implicit, because modulestore is a Draft modulestore) modulestore().convert_to_draft(item_location) if data: store.update_item(item_location, data) else: data = existing_item.get_explicitly_set_fields_by_scope(Scope.content) if children is not None: children_ids = [ loc_mapper().translate_locator_to_location(BlockUsageLocator(child_locator)).url() for child_locator in children ] store.update_children(item_location, children_ids) # cdodge: also commit any metadata which might have been passed along if nullout is not None or metadata is not None: # the postback is not the complete metadata, as there's system metadata which is # not presented to the end-user for editing. So let's use the original (existing_item) and # 'apply' the submitted metadata, so we don't end up deleting system metadata. if nullout is not None: for metadata_key in nullout: setattr(existing_item, metadata_key, None) # update existing metadata with submitted metadata (which can be partial) # IMPORTANT NOTE: if the client passed 'null' (None) for a piece of metadata that means 'remove it'. If # the intent is to make it None, use the nullout field if metadata is not None: for metadata_key, value in metadata.items(): if metadata_key == "locator_term": temp_key = "direct_term" json_array = json.loads(value) for x in json_array: old_loc = str(loc_mapper().translate_locator_to_location(x["direct_element_id"])) i = old_loc.rfind("/") short_name = old_loc[i+1:] x["direct_element_id"] = short_name for every_edge in x["disjunctions"]: for every_cond in every_edge["conjunctions"]: old_loc = str(loc_mapper().translate_locator_to_location(every_cond["source_element_id"])) i = old_loc.rfind("/") short_name = old_loc[i+1:] every_cond["source_element_id"] = short_name temp_value = json.dumps(json_array) else: temp_key = metadata_key temp_value = value field = existing_item.fields[temp_key] if temp_value is None: field.delete_from(existing_item) else: try: temp_value = field.from_json(temp_value) except ValueError: return JsonResponse({"error": "Invalid data"}, 400) field.write_to(existing_item, temp_value) # Save the data that we've just changed to the underlying # MongoKeyValueStore before we update the mongo datastore. existing_item.save() # commit to datastore store.update_metadata(item_location, own_metadata(existing_item)) if existing_item.category == 'video': manage_video_subtitles_save(existing_item, existing_item) result = { 'id': unicode(usage_loc), 'data': data, 'metadata': own_metadata(existing_item) } if grader_type is not None: result.update(CourseGradingModel.update_section_grader_type(existing_item, grader_type)) # Make public after updating the xblock, in case the caller asked # for both an update and a publish. if publish and publish == 'make_public': _xmodule_recurse( existing_item, lambda i: modulestore().publish(i.location, request.user.id) ) # Note that children aren't being returned until we have a use case. return JsonResponse(result)
def _save_item(usage_loc, item_location, data=None, children=None, metadata=None, nullout=None, grader_type=None ): """ Saves xblock w/ its fields. Has special processing for grader_type and nullout and Nones in metadata. nullout means to truly set the field to None whereas nones in metadata mean to unset them (so they revert to default). The item_location is still the old-style location whereas usage_loc is a BlockUsageLocator """ store = get_modulestore(item_location) try: existing_item = store.get_item(item_location) except ItemNotFoundError: if item_location.category in CREATE_IF_NOT_FOUND: # New module at this location, for pages that are not pre-created. # Used for course info handouts. store.create_and_save_xmodule(item_location) existing_item = store.get_item(item_location) else: raise except InvalidLocationError: log.error("Can't find item by location.") return JsonResponse({"error": "Can't find item by location: " + str(item_location)}, 404) if data: store.update_item(item_location, data) else: data = existing_item.get_explicitly_set_fields_by_scope(Scope.content) if children is not None: children_ids = [ loc_mapper().translate_locator_to_location(BlockUsageLocator(child_locator)).url() for child_locator in children ] store.update_children(item_location, children_ids) # cdodge: also commit any metadata which might have been passed along if nullout is not None or metadata is not None: # the postback is not the complete metadata, as there's system metadata which is # not presented to the end-user for editing. So let's use the original (existing_item) and # 'apply' the submitted metadata, so we don't end up deleting system metadata. if nullout is not None: for metadata_key in nullout: setattr(existing_item, metadata_key, None) # update existing metadata with submitted metadata (which can be partial) # IMPORTANT NOTE: if the client passed 'null' (None) for a piece of metadata that means 'remove it'. If # the intent is to make it None, use the nullout field if metadata is not None: for metadata_key, value in metadata.items(): field = existing_item.fields[metadata_key] if value is None: field.delete_from(existing_item) else: try: value = field.from_json(value) except ValueError: return JsonResponse({"error": "Invalid data"}, 400) field.write_to(existing_item, value) # Save the data that we've just changed to the underlying # MongoKeyValueStore before we update the mongo datastore. existing_item.save() # commit to datastore store.update_metadata(item_location, own_metadata(existing_item)) if existing_item.category == 'video': manage_video_subtitles_save(existing_item, existing_item) result = { 'id': unicode(usage_loc), 'data': data, 'metadata': own_metadata(existing_item) } if grader_type is not None: result.update(CourseGradingModel.update_section_grader_type(existing_item, grader_type)) # Note that children aren't being returned until we have a use case. return JsonResponse(result)
def _create_item(request): """View for create items.""" usage_key = usage_key_with_run(request.json['parent_locator']) if not has_studio_write_access(request.user, usage_key.course_key): raise PermissionDenied() category = request.json['category'] display_name = request.json.get('display_name') if isinstance(usage_key, LibraryUsageLocator): # Only these categories are supported at this time. if category not in ['html', 'problem', 'video']: return HttpResponseBadRequest( "Category '%s' not supported for Libraries" % category, content_type='text/plain' ) store = modulestore() with store.bulk_operations(usage_key.course_key): parent = store.get_item(usage_key) dest_usage_key = usage_key.replace(category=category, name=uuid4().hex) # get the metadata, display_name, and definition from the request metadata = {} data = None template_id = request.json.get('boilerplate') if template_id: clz = parent.runtime.load_block_type(category) if clz is not None: template = clz.get_template(template_id) if template is not None: metadata = template.get('metadata', {}) data = template.get('data') if display_name is not None: metadata['display_name'] = display_name # Entrance Exams: Chapter module positioning child_position = None if settings.FEATURES.get('ENTRANCE_EXAMS', False): is_entrance_exam = request.json.get('is_entrance_exam', False) if category == 'chapter' and is_entrance_exam: metadata['is_entrance_exam'] = is_entrance_exam metadata['in_entrance_exam'] = True # Inherited metadata, all children will have it child_position = 0 # TODO need to fix components that are sending definition_data as strings, instead of as dicts # For now, migrate them into dicts here. if isinstance(data, basestring): data = {'data': data} created_block = store.create_child( request.user.id, usage_key, dest_usage_key.block_type, block_id=dest_usage_key.block_id, definition_data=data, metadata=metadata, runtime=parent.runtime, position=child_position ) # Entrance Exams: Grader assignment if settings.FEATURES.get('ENTRANCE_EXAMS', False): course = store.get_course(usage_key.course_key) if hasattr(course, 'entrance_exam_enabled') and course.entrance_exam_enabled: if category == 'sequential' and request.json.get('parent_locator') == course.entrance_exam_id: grader = { "type": "Entrance Exam", "min_count": 0, "drop_count": 0, "short_label": "Entrance", "weight": 0 } grading_model = CourseGradingModel.update_grader_from_json( course.id, grader, request.user ) CourseGradingModel.update_section_grader_type( created_block, grading_model['type'], request.user ) # VS[compat] cdodge: This is a hack because static_tabs also have references from the course module, so # if we add one then we need to also add it to the policy information (i.e. metadata) # we should remove this once we can break this reference from the course to static tabs if category == 'static_tab': display_name = display_name or _("Empty") # Prevent name being None course = store.get_course(dest_usage_key.course_key) course.tabs.append( StaticTab( name=display_name, url_slug=dest_usage_key.name, ) ) store.update_item(course, request.user.id) return JsonResponse( {"locator": unicode(created_block.location), "courseKey": unicode(created_block.location.course_key)} )
def block_clone_handler(request, course_key_string): """ Permite clonar o conteúdo de uma dada semana do experimento. Além de clonar, nesta função faz a definição do experimento. o que insere entradas nas tabelas ExperimentDefinition, StrategyRandomization e OpcoesExperiment. :param request: http request com parent_location (location do curso) e mais source_locator (location da section) :param course_key_string: useless :return: json com OK """ letras = ['A', 'B', 'C', 'D'] if request.method in ('PUT', 'POST'): # Get location Course com base no valor passado pelo Json locatorCursokey = UsageKey.from_string(request.json['parent_locator']) locatorSectionKey = UsageKey.from_string(request.json['source_locator']) if not has_course_access(request.user, locatorSectionKey): raise PermissionDenied() # Verifica se já tem um experimento de acordo com o SourceLocation expSection = None try: opcExp = OpcoesExperiment.objects.get(sectionExp='%s' % request.json['source_locator']) temExp = True expSection = opcExp.experimento # pega o experimento para cadastrar a nova versao except: temExp = False # Pesquisa no banco de dados o Curso do qual duplicará os módulos course = modulestore().get_item(locatorCursokey, depth=3) sections = course.get_children() quantidade = 0 for section in sections: if locatorSectionKey == section.location: NewLocatorItemSection = create_item(locatorCursokey, 'chapter', section.display_name_with_default, request) # tem que Mudar para HomeWork, ou qualquer tipo que eu definir SectionLocation = NewLocatorItemSection descript = get_modulestore(NewLocatorItemSection).get_item(NewLocatorItemSection) storeSection = get_modulestore(NewLocatorItemSection) try: existing_item = storeSection.get_item(SectionLocation) field = existing_item.fields['start'] if section.start is not None: print "NÃO É NULO " field.write_to(existing_item, section.start) storeSection.update_item(existing_item, request.user.id) except: print "Start Date and end Date" subsections = section.get_children() quantidade = 0 if temExp: opcExp3 = OpcoesExperiment() opcExp3.experimento = expSection opcExp3.sectionExp = "%s" % NewLocatorItemSection opcExp3.sectionExp_url = '%s' % getURLSection(locatorCursokey, NewLocatorItemSection) lenOpcs = len(OpcoesExperiment.objects.filter(experimento=expSection)) opcExp3.version = letras[lenOpcs] opcExp3.save() st = opcExp3.experimento.strategy st.percents +=';0.0' st.save() else: st = StrategyRandomization() st.strategyType = 'UniformChoice' st.percents = '0.0;0.0' st.save() # Experiment defintion exp = ExperimentDefinition() exp.course = request.json['parent_locator'] exp.userTeacher = request.user now = datetime.datetime.now() exp.descricao='MyExperiment %s ' % now exp.strategy = st exp.save() # Define a primeira versão do experimento opcExp = OpcoesExperiment() opcExp.experimento = exp opcExp.sectionExp = "%s" % locatorSectionKey opcExp.sectionExp_url = "%s" % section.url_name opcExp.version = 'A' opcExp.save() # Define a segunda versão do experimento opcExp2 = OpcoesExperiment() opcExp2.experimento = exp opcExp2.sectionExp = "%s" % NewLocatorItemSection opcExp2.sectionExp_url = '%s' % getURLSection(locatorCursokey, NewLocatorItemSection) opcExp2.version = 'B' opcExp2.save() for subsection in subsections: print print print "Clonando SubSeção: ", subsection.location NewLocatorItemSubsection = create_item(NewLocatorItemSection, 'sequential', subsection.display_name_with_default, request) # Agora iremos testar os Units units_Subsection = subsection.get_children() print "Information about the subsection: " print "subsection.format: ", subsection.format print "subsection.start: ", subsection.start print "Subsection locator: ", NewLocatorItemSubsection # tem que Mudar para HomeWork, ou qualquer tipo que eu definir # subLocation = loc_mapper().translate_locator_to_location(NewLocatorItemSubsection) # print "vert Location: ", subLocation # old_location = course_location.replace(category='course_info', name=block) # # descript = get_modulestore(NewLocatorItemSubsection).get_item(NewLocatorItemSubsection) print "Descript: ", descript CourseGradingModel.update_section_grader_type(descript, subsection.format, request.user) # Start Value storeSection = get_modulestore(NewLocatorItemSubsection) try: existing_item = storeSection.get_item(NewLocatorItemSubsection) field = existing_item.fields['start'] if subsection.start is not None: print "NÃO É NULO " field.write_to(existing_item, subsection.start) storeSection.update_item(existing_item, request.user.id) except: print "Deu erro" # Print all Units for unit in units_Subsection: originalState = compute_publish_state(unit) destinationUnit = duplicate_item(NewLocatorItemSubsection, unit.location, unit.display_name_with_default, request.user) # Nesta parte faz-se a leitura se e privado ou publico, se publico, seta a variavel como publico try: print "Vou fazer o translation -- destinationUnit ", destinationUnit # unitLocation = loc_mapper().translate_locator_to_location(destinationUnit) unitLocation = destinationUnit print "unity location: ", unitLocation # Start Value storeUnit = get_modulestore(unitLocation) print "STORE UNIT" try: existing_itemUnit = storeUnit.get_item(unitLocation) except: print "Deu erro" print "Antes do public" if originalState == 'public': def _publish(block): # This is super gross, but prevents us from publishing something that # we shouldn't. Ideally, all modulestores would have a consistant # interface for publishing. However, as of now, only the DraftMongoModulestore # does, so we have to check for the attribute explicitly. store = get_modulestore(block.location) print "Peguei o Store" if hasattr(store, 'publish'): store.publish(block.location, request.user.id) _xmodule_recurse( existing_itemUnit, _publish ) except: print "Erro ao setar publico" dataR = {'ok': quantidade } # return JsonResponse(dataR)
def _save_xblock(user, xblock, data=None, children=None, metadata=None, nullout=None, grader_type=None, publish=None): """ Saves xblock w/ its fields. Has special processing for grader_type, publish, and nullout and Nones in metadata. nullout means to truly set the field to None whereas nones in metadata mean to unset them (so they revert to default). """ store = modulestore() # Don't allow updating an xblock and discarding changes in a single operation (unsupported by UI). if publish == "discard_changes": store.revert_to_published(xblock.location, user.id) # Returning the same sort of result that we do for other save operations. In the future, # we may want to return the full XBlockInfo. return JsonResponse({'id': unicode(xblock.location)}) old_metadata = own_metadata(xblock) old_content = xblock.get_explicitly_set_fields_by_scope(Scope.content) if data: # TODO Allow any scope.content fields not just "data" (exactly like the get below this) xblock.data = data else: data = old_content['data'] if 'data' in old_content else None if children is not None: children_usage_keys = [] for child in children: child_usage_key = usage_key_with_run(child) children_usage_keys.append(child_usage_key) xblock.children = children_usage_keys # also commit any metadata which might have been passed along if nullout is not None or metadata is not None: # the postback is not the complete metadata, as there's system metadata which is # not presented to the end-user for editing. So let's use the original (existing_item) and # 'apply' the submitted metadata, so we don't end up deleting system metadata. if nullout is not None: for metadata_key in nullout: setattr(xblock, metadata_key, None) # update existing metadata with submitted metadata (which can be partial) # IMPORTANT NOTE: if the client passed 'null' (None) for a piece of metadata that means 'remove it'. If # the intent is to make it None, use the nullout field if metadata is not None: for metadata_key, value in metadata.items(): field = xblock.fields[metadata_key] if value is None: field.delete_from(xblock) else: try: value = field.from_json(value) except ValueError: return JsonResponse({"error": "Invalid data"}, 400) field.write_to(xblock, value) if callable(getattr(xblock, "editor_saved", None)): xblock.editor_saved(user, old_metadata, old_content) # commit to datastore store.update_item(xblock, user.id) # for static tabs, their containing course also records their display name if xblock.location.category == 'static_tab': course = store.get_course(xblock.location.course_key) # find the course's reference to this tab and update the name. static_tab = CourseTabList.get_tab_by_slug(course.tabs, xblock.location.name) # only update if changed if static_tab and static_tab['name'] != xblock.display_name: static_tab['name'] = xblock.display_name store.update_item(course, user.id) result = { 'id': unicode(xblock.location), 'data': data, 'metadata': own_metadata(xblock) } if grader_type is not None: result.update( CourseGradingModel.update_section_grader_type( xblock, grader_type, user)) # If publish is set to 'republish' and this item is not in direct only categories and has previously been published, # then this item should be republished. This is used by staff locking to ensure that changing the draft # value of the staff lock will also update the published version, but only at the unit level. if publish == 'republish' and xblock.category not in DIRECT_ONLY_CATEGORIES: if modulestore().has_published_version(xblock): publish = 'make_public' # Make public after updating the xblock, in case the caller asked for both an update and a publish. # Used by Bok Choy tests and by republishing of staff locks. if publish == 'make_public': modulestore().publish(xblock.location, user.id) # Note that children aren't being returned until we have a use case. return JsonResponse(result, encoder=EdxJSONEncoder)
def _save_item(request, usage_loc, item_location, data=None, children=None, metadata=None, nullout=None, grader_type=None, publish=None): """ Saves xblock w/ its fields. Has special processing for grader_type, publish, and nullout and Nones in metadata. nullout means to truly set the field to None whereas nones in metadata mean to unset them (so they revert to default). The item_location is still the old-style location whereas usage_loc is a BlockUsageLocator """ store = get_modulestore(item_location) try: existing_item = store.get_item(item_location) except ItemNotFoundError: if item_location.category in CREATE_IF_NOT_FOUND: # New module at this location, for pages that are not pre-created. # Used for course info handouts. store.create_and_save_xmodule(item_location) existing_item = store.get_item(item_location) else: raise except InvalidLocationError: log.error("Can't find item by location.") return JsonResponse( {"error": "Can't find item by location: " + str(item_location)}, 404) old_metadata = own_metadata(existing_item) if publish: if publish == 'make_private': _xmodule_recurse(existing_item, lambda i: modulestore().unpublish(i.location)) elif publish == 'create_draft': # This recursively clones the existing item location to a draft location (the draft is # implicit, because modulestore is a Draft modulestore) _xmodule_recurse( existing_item, lambda i: modulestore().convert_to_draft(i.location)) if data: # TODO Allow any scope.content fields not just "data" (exactly like the get below this) existing_item.data = data else: data = existing_item.get_explicitly_set_fields_by_scope(Scope.content) if children is not None: children_ids = [ loc_mapper().translate_locator_to_location( BlockUsageLocator(child_locator)).url() for child_locator in children ] existing_item.children = children_ids # also commit any metadata which might have been passed along if nullout is not None or metadata is not None: # the postback is not the complete metadata, as there's system metadata which is # not presented to the end-user for editing. So let's use the original (existing_item) and # 'apply' the submitted metadata, so we don't end up deleting system metadata. if nullout is not None: for metadata_key in nullout: setattr(existing_item, metadata_key, None) # update existing metadata with submitted metadata (which can be partial) # IMPORTANT NOTE: if the client passed 'null' (None) for a piece of metadata that means 'remove it'. If # the intent is to make it None, use the nullout field if metadata is not None: for metadata_key, value in metadata.items(): field = existing_item.fields[metadata_key] if value is None: field.delete_from(existing_item) else: try: value = field.from_json(value) except ValueError: return JsonResponse({"error": "Invalid data"}, 400) field.write_to(existing_item, value) if existing_item.category == 'video': manage_video_subtitles_save(existing_item, request.user, old_metadata, generate_translation=True) # commit to datastore store.update_item(existing_item, request.user.id) result = { 'id': unicode(usage_loc), 'data': data, 'metadata': own_metadata(existing_item) } if grader_type is not None: result.update( CourseGradingModel.update_section_grader_type( existing_item, grader_type, request.user)) # Make public after updating the xblock, in case the caller asked # for both an update and a publish. if publish and publish == 'make_public': def _publish(block): # This is super gross, but prevents us from publishing something that # we shouldn't. Ideally, all modulestores would have a consistant # interface for publishing. However, as of now, only the DraftMongoModulestore # does, so we have to check for the attribute explicitly. store = get_modulestore(block.location) if hasattr(store, 'publish'): store.publish(block.location, request.user.id) _xmodule_recurse(existing_item, _publish) # Note that children aren't being returned until we have a use case. return JsonResponse(result)
def create_xblock(parent_locator, user, category, display_name, boilerplate=None, is_entrance_exam=False): """ Performs the actual grunt work of creating items/xblocks -- knows nothing about requests, views, etc. """ store = modulestore() usage_key = usage_key_with_run(parent_locator) with store.bulk_operations(usage_key.course_key): parent = store.get_item(usage_key) dest_usage_key = usage_key.replace(category=category, name=uuid4().hex) # get the metadata, display_name, and definition from the caller metadata = {} data = None template_id = boilerplate if template_id: clz = parent.runtime.load_block_type(category) if clz is not None: template = clz.get_template(template_id) if template is not None: metadata = template.get('metadata', {}) data = template.get('data') if display_name is not None: metadata['display_name'] = display_name # We should use the 'fields' kwarg for newer module settings/values (vs. metadata or data) fields = {} # Entrance Exams: Chapter module positioning child_position = None if settings.FEATURES.get('ENTRANCE_EXAMS', False): if category == 'chapter' and is_entrance_exam: fields['is_entrance_exam'] = is_entrance_exam fields['in_entrance_exam'] = True # Inherited metadata, all children will have it child_position = 0 # TODO need to fix components that are sending definition_data as strings, instead of as dicts # For now, migrate them into dicts here. if isinstance(data, basestring): data = {'data': data} created_block = store.create_child( user.id, usage_key, dest_usage_key.block_type, block_id=dest_usage_key.block_id, fields=fields, definition_data=data, metadata=metadata, runtime=parent.runtime, position=child_position, ) # Entrance Exams: Grader assignment if settings.FEATURES.get('ENTRANCE_EXAMS', False): course_key = usage_key.course_key course = store.get_course(course_key) if hasattr(course, 'entrance_exam_enabled') and course.entrance_exam_enabled: if category == 'sequential' and parent_locator == course.entrance_exam_id: # Clean up any pre-existing entrance exam graders remove_entrance_exam_graders(course_key, user) grader = { "type": GRADER_TYPES['ENTRANCE_EXAM'], "min_count": 0, "drop_count": 0, "short_label": "Entrance", "weight": 0 } grading_model = CourseGradingModel.update_grader_from_json( course.id, grader, user ) CourseGradingModel.update_section_grader_type( created_block, grading_model['type'], user ) # VS[compat] cdodge: This is a hack because static_tabs also have references from the course module, so # if we add one then we need to also add it to the policy information (i.e. metadata) # we should remove this once we can break this reference from the course to static tabs if category == 'static_tab': dog_stats_api.increment( DEPRECATION_VSCOMPAT_EVENT, tags=( "location:create_xblock_static_tab", u"course:{}".format(unicode(dest_usage_key.course_key)), ) ) display_name = display_name or _("Empty") # Prevent name being None course = store.get_course(dest_usage_key.course_key) course.tabs.append( StaticTab( name=display_name, url_slug=dest_usage_key.name, ) ) store.update_item(course, user.id) return created_block
def _save_xblock(user, xblock, data=None, children=None, metadata=None, nullout=None, grader_type=None, publish=None): """ Saves xblock w/ its fields. Has special processing for grader_type, publish, and nullout and Nones in metadata. nullout means to truly set the field to None whereas nones in metadata mean to unset them (so they revert to default). """ store = modulestore() # Don't allow updating an xblock and discarding changes in a single operation (unsupported by UI). if publish == "discard_changes": store.revert_to_published(xblock.location, user.id) # Returning the same sort of result that we do for other save operations. In the future, # we may want to return the full XBlockInfo. return JsonResponse({'id': unicode(xblock.location)}) old_metadata = own_metadata(xblock) old_content = xblock.get_explicitly_set_fields_by_scope(Scope.content) if data: # TODO Allow any scope.content fields not just "data" (exactly like the get below this) xblock.data = data else: data = old_content['data'] if 'data' in old_content else None if children is not None: children_usage_keys = [] for child in children: child_usage_key = usage_key_with_run(child) children_usage_keys.append(child_usage_key) xblock.children = children_usage_keys # also commit any metadata which might have been passed along if nullout is not None or metadata is not None: # the postback is not the complete metadata, as there's system metadata which is # not presented to the end-user for editing. So let's use the original (existing_item) and # 'apply' the submitted metadata, so we don't end up deleting system metadata. if nullout is not None: for metadata_key in nullout: setattr(xblock, metadata_key, None) # update existing metadata with submitted metadata (which can be partial) # IMPORTANT NOTE: if the client passed 'null' (None) for a piece of metadata that means 'remove it'. If # the intent is to make it None, use the nullout field if metadata is not None: for metadata_key, value in metadata.items(): field = xblock.fields[metadata_key] if value is None: field.delete_from(xblock) else: try: value = field.from_json(value) except ValueError: return JsonResponse({"error": "Invalid data"}, 400) field.write_to(xblock, value) if callable(getattr(xblock, "editor_saved", None)): xblock.editor_saved(user, old_metadata, old_content) # commit to datastore store.update_item(xblock, user.id) # for static tabs, their containing course also records their display name if xblock.location.category == 'static_tab': course = store.get_course(xblock.location.course_key) # find the course's reference to this tab and update the name. static_tab = CourseTabList.get_tab_by_slug(course.tabs, xblock.location.name) # only update if changed if static_tab and static_tab['name'] != xblock.display_name: static_tab['name'] = xblock.display_name store.update_item(course, user.id) result = { 'id': unicode(xblock.location), 'data': data, 'metadata': own_metadata(xblock) } if grader_type is not None: result.update(CourseGradingModel.update_section_grader_type(xblock, grader_type, user)) # If publish is set to 'republish' and this item is not in direct only categories and has previously been published, # then this item should be republished. This is used by staff locking to ensure that changing the draft # value of the staff lock will also update the published version, but only at the unit level. if publish == 'republish' and xblock.category not in DIRECT_ONLY_CATEGORIES: if modulestore().has_published_version(xblock): publish = 'make_public' # Make public after updating the xblock, in case the caller asked for both an update and a publish. # Used by Bok Choy tests and by republishing of staff locks. if publish == 'make_public': modulestore().publish(xblock.location, user.id) # Note that children aren't being returned until we have a use case. return JsonResponse(result, encoder=EdxJSONEncoder)
def _save_item(user, usage_key, data=None, children=None, metadata=None, nullout=None, grader_type=None, publish=None): """ Saves xblock w/ its fields. Has special processing for grader_type, publish, and nullout and Nones in metadata. nullout means to truly set the field to None whereas nones in metadata mean to unset them (so they revert to default). """ store = modulestore() try: existing_item = store.get_item(usage_key) except ItemNotFoundError: if usage_key.category in CREATE_IF_NOT_FOUND: # New module at this location, for pages that are not pre-created. # Used for course info handouts. existing_item = store.create_item(user.id, usage_key.course_key, usage_key.block_type, usage_key.block_id) else: raise except InvalidLocationError: log.error("Can't find item by location.") return JsonResponse( {"error": "Can't find item by location: " + unicode(usage_key)}, 404) old_metadata = own_metadata(existing_item) old_content = existing_item.get_explicitly_set_fields_by_scope( Scope.content) if publish: if publish == 'make_private': try: store.unpublish(existing_item.location, user.id), except ItemNotFoundError: pass elif publish == 'create_draft': try: store.convert_to_draft(existing_item.location, user.id) except DuplicateItemError: pass if data: # TODO Allow any scope.content fields not just "data" (exactly like the get below this) existing_item.data = data else: data = old_content['data'] if 'data' in old_content else None if children is not None: children_usage_keys = [] for child in children: child_usage_key = UsageKey.from_string(child) child_usage_key = child_usage_key.replace(course_key=modulestore( ).fill_in_run(child_usage_key.course_key)) children_usage_keys.append(child_usage_key) existing_item.children = children_usage_keys # also commit any metadata which might have been passed along if nullout is not None or metadata is not None: # the postback is not the complete metadata, as there's system metadata which is # not presented to the end-user for editing. So let's use the original (existing_item) and # 'apply' the submitted metadata, so we don't end up deleting system metadata. if nullout is not None: for metadata_key in nullout: setattr(existing_item, metadata_key, None) # update existing metadata with submitted metadata (which can be partial) # IMPORTANT NOTE: if the client passed 'null' (None) for a piece of metadata that means 'remove it'. If # the intent is to make it None, use the nullout field if metadata is not None: for metadata_key, value in metadata.items(): field = existing_item.fields[metadata_key] if value is None: field.delete_from(existing_item) else: try: value = field.from_json(value) except ValueError: return JsonResponse({"error": "Invalid data"}, 400) field.write_to(existing_item, value) if callable(getattr(existing_item, "editor_saved", None)): existing_item.editor_saved(user, old_metadata, old_content) # commit to datastore store.update_item(existing_item, user.id) # for static tabs, their containing course also records their display name if usage_key.category == 'static_tab': course = store.get_course(usage_key.course_key) # find the course's reference to this tab and update the name. static_tab = CourseTabList.get_tab_by_slug(course.tabs, usage_key.name) # only update if changed if static_tab and static_tab['name'] != existing_item.display_name: static_tab['name'] = existing_item.display_name store.update_item(course, user.id) result = { 'id': unicode(usage_key), 'data': data, 'metadata': own_metadata(existing_item) } if grader_type is not None: result.update( CourseGradingModel.update_section_grader_type( existing_item, grader_type, user)) # Make public after updating the xblock, in case the caller asked # for both an update and a publish. if publish and publish == 'make_public': modulestore().publish(existing_item.location, user.id) # Note that children aren't being returned until we have a use case. return JsonResponse(result)