def revert_to_published(self, location, user_id): """ Reverts an item to its last published version (recursively traversing all of its descendants). If no published version exists, a VersionConflictError is thrown. If a published version exists but there is no draft version of this item or any of its descendants, this method is a no-op. :raises InvalidVersionError: if no published version exists for the location specified """ if location.category in DIRECT_ONLY_CATEGORIES: return draft_course_key = location.course_key.for_branch( ModuleStoreEnum.BranchName.draft) with self.bulk_operations(draft_course_key): # get head version of Published branch published_course_structure = self._lookup_course( location.course_key.for_branch( ModuleStoreEnum.BranchName.published)).structure published_block = self._get_block_from_structure( published_course_structure, BlockKey.from_usage_key(location)) if published_block is None: raise InvalidVersionError(location) # create a new versioned draft structure draft_course_structure = self._lookup_course( draft_course_key).structure new_structure = self.version_structure(draft_course_key, draft_course_structure, user_id) # remove the block and its descendants from the new structure self._remove_subtree(BlockKey.from_usage_key(location), new_structure['blocks']) # copy over the block and its descendants from the published branch def copy_from_published(root_block_id): """ copies root_block_id and its descendants from published_course_structure to new_structure """ self._update_block_in_structure( new_structure, root_block_id, self._get_block_from_structure(published_course_structure, root_block_id)) block = self._get_block_from_structure(new_structure, root_block_id) for child_block_id in block.fields.get('children', []): copy_from_published(child_block_id) copy_from_published(BlockKey.from_usage_key(location)) # update course structure and index self.update_structure(draft_course_key, new_structure) index_entry = self._get_index_if_valid(draft_course_key) if index_entry is not None: self._update_head(draft_course_key, index_entry, ModuleStoreEnum.BranchName.draft, new_structure['_id'])
def _convert_to_draft(self, location, user_id, delete_published=False, ignore_if_draft=False): """ Internal method with additional internal parameters to convert a subtree to draft. Args: location: the location of the source (its revision must be MongoRevisionKey.published) user_id: the ID of the user doing the operation delete_published (Boolean): intended for use by unpublish ignore_if_draft(Boolean): for internal use only as part of depth first change Raises: InvalidVersionError: if the source can not be made into a draft ItemNotFoundError: if the source does not exist DuplicateItemError: if the source or any of its descendants already has a draft copy. Only useful for unpublish b/c we don't want unpublish to overwrite any existing drafts. """ # verify input conditions: can only convert to draft branch; so, verify that's the setting self._verify_branch_setting(ModuleStoreEnum.Branch.draft_preferred) _verify_revision_is_published(location) # ensure we are not creating a DRAFT of an item that is direct-only if location.category in DIRECT_ONLY_CATEGORIES: raise InvalidVersionError(location) def convert_item(item, to_be_deleted): """ Convert the subtree """ # collect the children's ids for future processing next_tier = [] for child in item.get('definition', {}).get('children', []): child_loc = Location.from_deprecated_string(child) next_tier.append(child_loc.to_deprecated_son()) # insert a new DRAFT version of the item item['_id']['revision'] = MongoRevisionKey.draft # ensure keys are in fixed and right order before inserting item['_id'] = self._id_dict_to_son(item['_id']) bulk_record = self._get_bulk_ops_record(location.course_key) bulk_record.dirty = True try: self.collection.insert(item) except pymongo.errors.DuplicateKeyError: # prevent re-creation of DRAFT versions, unless explicitly requested to ignore if not ignore_if_draft: raise DuplicateItemError(item['_id'], self, 'collection') # delete the old PUBLISHED version if requested if delete_published: item['_id']['revision'] = MongoRevisionKey.published to_be_deleted.append(item['_id']) return next_tier # convert the subtree using the original item as the root self._breadth_first(convert_item, [location])
def unpublish(self, location): """ Turn the published version into a draft, removing the published version """ if Location(location).category in DIRECT_ONLY_CATEGORIES: raise InvalidVersionError(location) super(DraftModuleStore, self).clone_item(location, as_draft(location)) super(DraftModuleStore, self).delete_item(location)
def clone_item(self, source, location): """ Clone a new item that is a copy of the item at the location `source` and writes it to `location` """ if Location(location).category in DIRECT_ONLY_CATEGORIES: raise InvalidVersionError(location) return wrap_draft(super(DraftModuleStore, self).clone_item(source, as_draft(location)))
def unpublish(self, location, user_id, **kwargs): """ Deletes the published version of the item. Returns the newly unpublished item. """ if location.block_type in DIRECT_ONLY_CATEGORIES: raise InvalidVersionError(location) with self.bulk_operations(location.course_key): self.delete_item(location, user_id, revision=ModuleStoreEnum.RevisionOption.published_only) return self.get_item(location.for_branch(ModuleStoreEnum.BranchName.draft), **kwargs)
def revert_to_published(self, location, user_id=None): """ Reverts an item to its last published version (recursively traversing all of its descendants). If no published version exists, an InvalidVersionError is thrown. If a published version exists but there is no draft version of this item or any of its descendants, this method is a no-op. It is also a no-op if the root item is in DIRECT_ONLY_CATEGORIES. :raises InvalidVersionError: if no published version exists for the location specified """ self._verify_branch_setting(ModuleStoreEnum.Branch.draft_preferred) _verify_revision_is_published(location) if location.block_type in DIRECT_ONLY_CATEGORIES: return if not self.has_item(location, revision=ModuleStoreEnum.RevisionOption.published_only): raise InvalidVersionError(location) def delete_draft_only(root_location): """ Helper function that calls delete on the specified location if a draft version of the item exists. If no draft exists, this function recursively calls itself on the children of the item. """ query = root_location.to_deprecated_son(prefix='_id.') del query['_id.revision'] versions_found = self.collection.find( query, {'_id': True, 'definition.children': True}, sort=[SORT_REVISION_FAVOR_DRAFT] ) versions_found = list(versions_found) # If 2 versions versions exist, we can assume one is a published version. Go ahead and do the delete # of the draft version. if len(versions_found) > 1: # Moving a child from published parent creates a draft of the parent and moved child. published_version = [ version for version in versions_found if version.get('_id').get('revision') != MongoRevisionKey.draft ] if len(published_version) > 0: # This change makes sure that parents are updated too i.e. an item will have only one parent. self.update_parent_if_moved(root_location, published_version[0], delete_draft_only, user_id) self._delete_subtree(root_location, [as_draft], draft_only=True) elif len(versions_found) == 1: # Since this method cannot be called on something in DIRECT_ONLY_CATEGORIES and we call # delete_subtree as soon as we find an item with a draft version, if there is only 1 version # it must be published (since adding a child to a published item creates a draft of the parent). item = versions_found[0] assert item.get('_id').get('revision') != MongoRevisionKey.draft for child in item.get('definition', {}).get('children', []): child_loc = BlockUsageLocator.from_string(child) delete_draft_only(child_loc) delete_draft_only(location)
def unpublish(self, location, user_id, **kwargs): """ Turn the published version into a draft, removing the published version. NOTE: unlike publish, this gives an error if called above the draftable level as it's intended to remove things from the published version """ # ensure we are not creating a DRAFT of an item that is direct-only if location.block_type in DIRECT_ONLY_CATEGORIES: raise InvalidVersionError(location) self._verify_branch_setting(ModuleStoreEnum.Branch.draft_preferred) self._convert_to_draft(location, user_id, delete_published=True) course_key = location.course_key self._flag_publish_event(course_key)
def create_xmodule(self, location, definition_data=None, metadata=None, system=None): """ Create the new xmodule but don't save it. Returns the new module with a draft locator :param location: a Location--must have a category :param definition_data: can be empty. The initial definition_data for the kvs :param metadata: can be empty, the initial metadata for the kvs :param system: if you already have an xmodule from the course, the xmodule.system value """ draft_loc = as_draft(location) if draft_loc.category in DIRECT_ONLY_CATEGORIES: raise InvalidVersionError(location) return super(DraftModuleStore, self).create_xmodule(draft_loc, definition_data, metadata, system)
def convert_to_draft(self, source_location): """ Create a copy of the source and mark its revision as draft. :param source: the location of the source (its revision must be None) """ original = self.collection.find_one(location_to_query(source_location)) draft_location = as_draft(source_location) if draft_location.category in DIRECT_ONLY_CATEGORIES: raise InvalidVersionError(source_location) original['_id'] = draft_location.dict() try: self.collection.insert(original) except pymongo.errors.DuplicateKeyError: raise DuplicateItemError(original['_id']) self.refresh_cached_metadata_inheritance_tree(draft_location) self.fire_updated_modulestore_signal( get_course_id_no_run(draft_location), draft_location) return self._load_items([original])[0]
def convert_to_draft(self, source_location): """ Create a copy of the source and mark its revision as draft. :param source: the location of the source (its revision must be None) """ if source_location.category in DIRECT_ONLY_CATEGORIES: raise InvalidVersionError(source_location) original = self.collection.find_one({'_id': source_location.to_deprecated_son()}) if not original: raise ItemNotFoundError(source_location) draft_location = as_draft(source_location) original['_id'] = draft_location.to_deprecated_son() try: self.collection.insert(original) except pymongo.errors.DuplicateKeyError: raise DuplicateItemError(original['_id']) self.refresh_cached_metadata_inheritance_tree(draft_location.course_key) return wrap_draft(self._load_items(source_location.course_key, [original])[0])