Example #1
0
    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'])
Example #2
0
    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])
Example #3
0
 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)
Example #4
0
 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)))
Example #5
0
    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)
Example #6
0
    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)
Example #7
0
    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)
Example #8
0
    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)
Example #9
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)
        """
        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]
Example #10
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])