def static_tab(request, course_id, tab_slug): """ Display the courses tab with the given name. Assumes the course_id is in a valid format. """ try: course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) except InvalidKeyError: raise Http404 course = get_course_with_access(request.user, 'load', course_key) tab = CourseTabList.get_tab_by_slug(course.tabs, tab_slug) if tab is None: raise Http404 contents = get_static_tab_contents( request, course, tab ) if contents is None: raise Http404 return render_to_response('courseware/static_tab.html', { 'course': course, 'tab': tab, 'tab_contents': contents, })
def update_item(self, xblock, user_id=None, allow_not_found=False, force=False): """ Update the persisted version of xblock to reflect its current values. xblock: which xblock to persist user_id: who made the change (ignored for now by this modulestore) allow_not_found: whether to create a new object if one didn't already exist or give an error force: force is meaningless for this modulestore """ try: definition_data = self._convert_reference_fields_to_strings(xblock, xblock.get_explicitly_set_fields_by_scope()) payload = { 'definition.data': definition_data, 'metadata': self._convert_reference_fields_to_strings(xblock, own_metadata(xblock)), } if xblock.has_children: children = self._convert_reference_fields_to_strings(xblock, {'children': xblock.children}) payload.update({'definition.children': children['children']}) self._update_single_item(xblock.scope_ids.usage_id, payload) # for static tabs, their containing course also records their display name if xblock.scope_ids.block_type == 'static_tab': course = self._get_course_for_item(xblock.scope_ids.usage_id) # find the course's reference to this tab and update the name. static_tab = CourseTabList.get_tab_by_slug(course.tabs, xblock.scope_ids.usage_id.name) # only update if changed if static_tab and static_tab['name'] != xblock.display_name: static_tab['name'] = xblock.display_name self.update_item(course, user_id) # recompute (and update) the metadata inheritance tree which is cached self.refresh_cached_metadata_inheritance_tree(xblock.scope_ids.usage_id.course_key, xblock.runtime) # fire signal that we've written to DB except ItemNotFoundError: if not allow_not_found: raise
def static_tab(request, course_id, tab_slug): """ Display the courses tab with the given name. Assumes the course_id is in a valid format. """ course = get_course_with_access(request.user, course_id, 'load') tab = CourseTabList.get_tab_by_slug(course.tabs, tab_slug) if tab is None: raise Http404 contents = get_static_tab_contents( request, course, tab ) if contents is None: raise Http404 return render_to_response('courseware/static_tab.html', { 'course': course, 'tab': tab, 'tab_contents': contents, })
def static_tab(request, course_id, tab_slug): """ Display the courses tab with the given name. Assumes the course_id is in a valid format. """ course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = get_course_with_access(request.user, 'load', course_key) tab = CourseTabList.get_tab_by_slug(course.tabs, tab_slug) if tab is None: raise Http404 contents = get_static_tab_contents( request, course, tab ) if contents is None: raise Http404 return render_to_response('courseware/static_tab.html', { 'course': course, 'tab': tab, 'tab_contents': contents, })
def test_get_static_tab_contents(self): course = get_course_by_id(self.toy_course_key) request = get_request_for_user(UserFactory.create()) tab = CourseTabList.get_tab_by_slug(course.tabs, 'resources') # Test render works okay tab_content = get_static_tab_contents(request, course, tab) self.assertIn(self.toy_course_key.to_deprecated_string(), tab_content) self.assertIn('static_tab', tab_content) # Test when render raises an exception with patch('courseware.views.get_module') as mock_module_render: mock_module_render.return_value = MagicMock(render=Mock( side_effect=Exception('Render failed!'))) static_tab = get_static_tab_contents(request, course, tab) self.assertIn("this module is temporarily unavailable", static_tab)
def test_get_static_tab_contents(self): course = get_course_by_id('edX/toy/2012_Fall') request = get_request_for_user(UserFactory.create()) tab = CourseTabList.get_tab_by_slug(course, 'resources') # Test render works okay tab_content = get_static_tab_contents(request, course, tab) self.assertIn('edX/toy/2012_Fall', tab_content) self.assertIn('static_tab', tab_content) # Test when render raises an exception with patch('courseware.views.get_module') as mock_module_render: mock_module_render.return_value = MagicMock( render=Mock(side_effect=Exception('Render failed!')) ) static_tab = get_static_tab_contents(request, course, tab) self.assertIn("this module is temporarily unavailable", static_tab)
def static_tab(request, course_id, tab_slug): """ Display the courses tab with the given name. Assumes the course_id is in a valid format. """ course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = get_course_with_access(request.user, "load", course_key) tab = CourseTabList.get_tab_by_slug(course.tabs, tab_slug) if tab is None: raise Http404 contents = get_static_tab_contents(request, course, tab) if contents is None: raise Http404 return render_to_response("courseware/static_tab.html", {"course": course, "tab": tab, "tab_contents": contents})
def update_item(self, xblock, user=None, allow_not_found=False): """ Update the persisted version of xblock to reflect its current values. location: Something that can be passed to Location data: A nested dictionary of problem data """ try: definition_data = xblock.get_explicitly_set_fields_by_scope() payload = { 'definition.data': definition_data, 'metadata': own_metadata(xblock), } if xblock.has_children: # convert all to urls xblock.children = [ child.url() if isinstance(child, Location) else child for child in xblock.children ] payload.update({'definition.children': xblock.children}) self._update_single_item(xblock.location, payload) # for static tabs, their containing course also records their display name if xblock.category == 'static_tab': course = self._get_course_for_item(xblock.location) # 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 self.update_item(course, user) # recompute (and update) the metadata inheritance tree which is cached # was conditional on children or metadata having changed before dhm made one update to rule them all self.refresh_cached_metadata_inheritance_tree(xblock.location) # fire signal that we've written to DB self.fire_updated_modulestore_signal( get_course_id_no_run(xblock.location), xblock.location) except ItemNotFoundError: if not allow_not_found: raise
def update_item(self, xblock, user=None, allow_not_found=False): """ Update the persisted version of xblock to reflect its current values. location: Something that can be passed to Location data: A nested dictionary of problem data """ try: definition_data = xblock.get_explicitly_set_fields_by_scope() payload = { 'definition.data': definition_data, 'metadata': own_metadata(xblock), } if xblock.has_children: # convert all to urls xblock.children = [child.url() if isinstance(child, Location) else child for child in xblock.children] payload.update({'definition.children': xblock.children}) self._update_single_item(xblock.location, payload) # for static tabs, their containing course also records their display name if xblock.category == 'static_tab': course = self._get_course_for_item(xblock.location) # find the course's reference to this tab and update the name. static_tab = CourseTabList.get_tab_by_slug(course, xblock.location.name) # only update if changed if static_tab and static_tab['name'] != xblock.display_name: static_tab['name'] = xblock.display_name self.update_item(course, user) # recompute (and update) the metadata inheritance tree which is cached # was conditional on children or metadata having changed before dhm made one update to rule them all self.refresh_cached_metadata_inheritance_tree(xblock.location) # fire signal that we've written to DB self.fire_updated_modulestore_signal(get_course_id_no_run(xblock.location), xblock.location) except ItemNotFoundError: if not allow_not_found: raise
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)
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_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_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 _load_extra_content(self, system, course_descriptor, category, content_path, course_dir): """ Import fields data content from files """ for filepath in glob.glob(content_path / '*'): if not os.path.isfile(filepath): continue if filepath.endswith('~'): # skip *~ files continue with open(filepath) as f: try: if filepath.find('.json') != -1: # json file with json data content slug, loc, data_content = self._import_field_content( course_descriptor, category, filepath) if data_content is None: continue else: try: # get and update data field in xblock runtime module = system.load_item(loc) for key, value in data_content.items(): setattr(module, key, value) module.save() except ItemNotFoundError: module = None data_content['location'] = loc data_content['category'] = category else: slug = os.path.splitext(os.path.basename(filepath))[0] loc = course_descriptor.scope_ids.usage_id.replace( category=category, name=slug) # html file with html data content html = f.read() try: module = system.load_item(loc) module.data = html module.save() except ItemNotFoundError: module = None data_content = { 'data': html, 'location': loc, 'category': category } if module is None: module = system.construct_xblock( category, # We're loading a descriptor, so student_id is meaningless # We also don't have separate notions of definition and usage ids yet, # so we use the location for both ScopeIds(None, category, loc, loc), DictFieldData(data_content), ) # VS[compat]: # Hack because we need to pull in the 'display_name' for static tabs (because we need to edit them) # lint-amnesty, pylint: disable=line-too-long # from the course policy if category == "static_tab": tab = CourseTabList.get_tab_by_slug( tab_list=course_descriptor.tabs, url_slug=slug) if tab: module.display_name = tab.name module.course_staff_only = tab.course_staff_only module.data_dir = course_dir module.save() self.modules[course_descriptor.id][ module.scope_ids.usage_id] = module except Exception as exc: # pylint: disable=broad-except logging.exception( "Failed to load %s. Skipping... \ Exception: %s", filepath, str(exc)) system.error_tracker("ERROR: " + str(exc))
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: 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)
def _load_extra_content(self, system, course_descriptor, category, content_path, course_dir): """ Import fields data content from files """ for filepath in glob.glob(content_path / "*"): if not os.path.isfile(filepath): continue if filepath.endswith("~"): # skip *~ files continue with open(filepath) as f: try: if filepath.find(".json") != -1: # json file with json data content slug, loc, data_content = self._import_field_content(course_descriptor, category, filepath) if data_content is None: continue else: try: # get and update data field in xblock runtime module = system.load_item(loc) for key, value in data_content.iteritems(): setattr(module, key, value) module.save() except ItemNotFoundError: module = None data_content["location"] = loc data_content["category"] = category else: slug = os.path.splitext(os.path.basename(filepath))[0] loc = course_descriptor.scope_ids.usage_id.replace(category=category, name=slug) # html file with html data content html = f.read().decode("utf-8") try: module = system.load_item(loc) module.data = html module.save() except ItemNotFoundError: module = None data_content = {"data": html, "location": loc, "category": category} if module is None: module = system.construct_xblock( category, # We're loading a descriptor, so student_id is meaningless # We also don't have separate notions of definition and usage ids yet, # so we use the location for both ScopeIds(None, category, loc, loc), DictFieldData(data_content), ) # VS[compat]: # Hack because we need to pull in the 'display_name' for static tabs (because we need to edit them) # from the course policy if category == "static_tab": tab = CourseTabList.get_tab_by_slug(tab_list=course_descriptor.tabs, url_slug=slug) if tab: module.display_name = tab.name module.data_dir = course_dir module.save() self.modules[course_descriptor.id][module.scope_ids.usage_id] = module except Exception as exc: # pylint: disable=broad-except logging.exception( "Failed to load %s. Skipping... \ Exception: %s", filepath, unicode(exc), ) system.error_tracker("ERROR: " + unicode(exc))
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)