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
def update_block_location_translator(self, location, usage_id, old_course_id=None, autogenerated_usage_id=False): """ Update all existing maps from location's block to the new usage_id. Used for changing the usage_id, thus the usage_id is required. Returns the usage_id. (which is primarily useful in the case of autogenerated_usage_id) :param location: a fully specified Location :param usage_id: the desired new block_id. :param old_course_id: the old-style org/course or org/course/run string (optional) :param autogenerated_usage_id: a flag used mostly for internal calls to indicate that this usage_id was autogenerated and thus can be overridden if it's not unique. If you set this flag, the stored usage_id may not be the one you submitted. """ location_id = self._interpret_location_course_id( old_course_id, location) maps = self.location_map.find(location_id) encoded_location_name = self._encode_for_mongo(location.name) for map_entry in maps: # handle noop of renaming to same name if (encoded_location_name in map_entry['block_map'] and map_entry['block_map'][encoded_location_name].get( location.category) == usage_id): continue alt_usage_id = self._verify_uniqueness(usage_id, map_entry['block_map']) if alt_usage_id != usage_id: if autogenerated_usage_id: # revise already set ones and add to remaining ones usage_id = self.update_block_location_translator( location, alt_usage_id, old_course_id, True) return usage_id else: raise DuplicateItemError(usage_id, self, 'location_map') if location.category in map_entry['block_map'].setdefault( encoded_location_name, {}): map_entry['block_map'][encoded_location_name][ location.category] = usage_id self.location_map.update( {'_id': map_entry['_id']}, {'$set': { 'block_map': map_entry['block_map'] }}) return usage_id
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` """ item = None try: source_item = self.collection.find_one(location_to_query(source)) # allow for some programmatically generated substitutions in metadata, e.g. Discussion_id's should be auto-generated for key in source_item['metadata'].keys(): if source_item['metadata'][key] == '$$GUID$$': source_item['metadata'][key] = uuid4().hex source_item['_id'] = Location(location).dict() self.collection.insert( source_item, # Must include this to avoid the django debug toolbar (which defines the deprecated "safe=False") # from overriding our default value set in the init method. safe=self.collection.safe) item = self._load_items([source_item])[0] # 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 location.category == 'static_tab': course = self.get_course_for_item(item.location) existing_tabs = course.tabs or [] existing_tabs.append({ 'type': 'static_tab', 'name': item.display_name, 'url_slug': item.location.name }) course.tabs = existing_tabs self.update_metadata(course.location, course._model_data._kvs._metadata) except pymongo.errors.DuplicateKeyError: raise DuplicateItemError(location) # recompute (and update) the metadata inheritance tree which is cached self.refresh_cached_metadata_inheritance_tree(Location(location)) self.fire_updated_modulestore_signal( get_course_id_no_run(Location(location)), Location(location)) return item
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])
def add_block_location_translator(self, location, old_course_id=None, usage_id=None): """ Similar to translate_location which adds an entry if none is found, but this cannot create a new course mapping entry, only a block within such a mapping entry. If it finds no existing course maps, it raises ItemNotFoundError. In the case that there are more than one mapping record for the course identified by location, this method adds the mapping to all matching records! (translate_location only adds to one) It allows the caller to specify the new-style usage_id for the target rather than having the translate concoct its own. If the provided usage_id already exists in one of the found maps for the org/course, this function raises DuplicateItemError unless the old item id == the new one. If the caller does not provide a usage_id and there exists an entry in one of the course variants, it will use that entry. If more than one variant uses conflicting entries, it will raise DuplicateItemError. Returns the usage_id used in the mapping :param location: a fully specified Location :param old_course_id: the old-style org/course or org/course/run string (optional) :param usage_id: the desired new block_id. If left as None, this will generate one as per translate_location """ location_id = self._interpret_location_course_id( old_course_id, location) maps = self.location_map.find(location_id) if maps.count() == 0: raise ItemNotFoundError() # turn maps from cursor to list map_list = list(maps) encoded_location_name = self._encode_for_mongo(location.name) # check whether there's already a usage_id for this location (and it agrees w/ any passed in or found) for map_entry in map_list: if (encoded_location_name in map_entry['block_map'] and location.category in map_entry['block_map'][encoded_location_name]): if usage_id is None: usage_id = map_entry['block_map'][encoded_location_name][ location.category] elif usage_id != map_entry['block_map'][encoded_location_name][ location.category]: raise DuplicateItemError(usage_id, self, 'location_map') computed_usage_id = usage_id # update the maps (and generate a usage_id if it's not been set yet) for map_entry in map_list: if computed_usage_id is None: computed_usage_id = self._add_to_block_map( location, location_id, map_entry['block_map']) elif (encoded_location_name not in map_entry['block_map'] or location.category not in map_entry['block_map'][encoded_location_name]): alt_usage_id = self._verify_uniqueness(computed_usage_id, map_entry['block_map']) if alt_usage_id != computed_usage_id: if usage_id is not None: raise DuplicateItemError(usage_id, self, 'location_map') else: # revise already set ones and add to remaining ones computed_usage_id = self.update_block_location_translator( location, alt_usage_id, old_course_id, True) map_entry['block_map'].setdefault( encoded_location_name, {})[location.category] = computed_usage_id self.location_map.update( {'_id': map_entry['_id']}, {'$set': { 'block_map': map_entry['block_map'] }}) return computed_usage_id