def update_collection(committer_id, collection_id, change_list, commit_message): """Updates a collection. Commits changes. Args: - committer_id: str. The id of the user who is performing the update action. - collection_id: str. The collection id. - change_list: list of dicts, each representing a CollectionChange object. These changes are applied in sequence to produce the resulting collection. - commit_message: str or None. A description of changes made to the collection. For published collections, this must be present; for unpublished collections, it may be equal to None. """ is_public = rights_manager.is_collection_public(collection_id) if is_public and not commit_message: raise ValueError( 'Collection is public so expected a commit message but ' 'received none.') collection = apply_change_list(collection_id, change_list) _save_collection(committer_id, collection, commit_message, change_list) update_collection_summary(collection.id, committer_id) if (not rights_manager.is_collection_private(collection.id) and committer_id != feconf.MIGRATION_BOT_USER_ID): user_services.update_first_contribution_msec_if_not_set( committer_id, utils.get_current_time_in_millisecs())
def get(self, collection_id): """Handles GET requests.""" collection = collection_services.get_collection_by_id( collection_id, strict=False) if (collection is None or not rights_manager.Actor(self.user_id).can_view( rights_manager.ACTIVITY_TYPE_COLLECTION, collection_id)): self.redirect('/') return can_edit = ( bool(self.user_id) and self.username not in config_domain.BANNED_USERNAMES.value and rights_manager.Actor(self.user_id).can_edit( rights_manager.ACTIVITY_TYPE_COLLECTION, collection_id)) self.values.update({ 'is_public': rights_manager.is_collection_public(collection_id), 'can_edit': can_edit, 'collection_id': collection.id, 'title': collection.title }) self.render_template('collection_editor/collection_editor.html')
def get(self, collection_id): """Handles GET requests.""" collection = collection_services.get_collection_by_id(collection_id, strict=False) if (collection is None or not rights_manager.Actor(self.user_id).can_view( rights_manager.ACTIVITY_TYPE_COLLECTION, collection_id)): self.redirect('/') return can_edit = ( bool(self.user_id) and self.username not in config_domain.BANNED_USERNAMES.value and rights_manager.Actor(self.user_id).can_edit( rights_manager.ACTIVITY_TYPE_COLLECTION, collection_id)) self.values.update({ 'is_public': rights_manager.is_collection_public(collection_id), 'can_edit': can_edit, 'collection_id': collection.id, 'title': collection.title }) self.render_template('collection_editor/collection_editor.html')
def update_collection( committer_id, collection_id, change_list, commit_message): """Update an collection. Commits changes. Args: - committer_id: str. The id of the user who is performing the update action. - collection_id: str. The collection id. - change_list: list of dicts, each representing a CollectionChange object. These changes are applied in sequence to produce the resulting collection. - commit_message: str or None. A description of changes made to the collection. For published collections, this must be present; for unpublished collections, it may be equal to None. """ is_public = rights_manager.is_collection_public(collection_id) if is_public and not commit_message: raise ValueError( 'Collection is public so expected a commit message but ' 'received none.') collection = apply_change_list(collection_id, change_list) _save_collection(committer_id, collection, commit_message, change_list) update_collection_summary(collection.id, committer_id) if not rights_manager.is_collection_private(collection.id): user_services.update_first_contribution_msec_if_not_set( committer_id, utils.get_current_time_in_millisecs())
def update_collection(committer_id, collection_id, change_list, commit_message): """Update an collection. Commits changes. Args: - committer_id: str. The id of the user who is performing the update action. - collection_id: str. The collection id. - change_list: list of dicts, each representing a CollectionChange object. These changes are applied in sequence to produce the resulting collection. - commit_message: str or None. A description of changes made to the collection. For published collections, this must be present; for unpublished collections, it may be equal to None. """ is_public = rights_manager.is_collection_public(collection_id) if is_public and not commit_message: raise ValueError( 'Collection is public so expected a commit message but ' 'received none.') collection = apply_change_list(collection_id, change_list) _save_collection(committer_id, collection, commit_message, change_list) update_collection_summary(collection.id)
def _save_collection(committer_id, collection, commit_message, change_list): """Validates a collection and commits it to persistent storage. If successful, increments the version number of the incoming collection domain object by 1. Args: committer_id: str. ID of the given committer. collection: Collection. The collection domain object to be saved. commit_message: str. The commit message. change_list: list(dict). List of changes applied to a collection. Each entry in change_list is a dict that represents a CollectionChange. Raises: ValidationError: An invalid exploration was referenced in the collection. Exception: The collection model and the incoming collection domain object have different version numbers. """ if not change_list: raise Exception( 'Unexpected error: received an invalid change list when trying to ' 'save collection %s: %s' % (collection.id, change_list)) collection_rights = rights_manager.get_collection_rights(collection.id) if collection_rights.status != rights_manager.ACTIVITY_STATUS_PRIVATE: collection.validate(strict=True) else: collection.validate(strict=False) # Validate that all explorations referenced by the collection exist. exp_ids = collection.exploration_ids exp_summaries = ( exp_services.get_exploration_summaries_matching_ids(exp_ids)) exp_summaries_dict = { exp_id: exp_summaries[ind] for (ind, exp_id) in enumerate(exp_ids) } for collection_node in collection.nodes: if not exp_summaries_dict[collection_node.exploration_id]: raise utils.ValidationError( 'Expected collection to only reference valid explorations, ' 'but found an exploration with ID: %s (was it deleted?)' % collection_node.exploration_id) # Ensure no explorations are being added that are 'below' the public status # of this collection. If the collection is private, it can have both # private and public explorations. If it's public, it can only have public # explorations. # TODO(bhenning): Ensure the latter is enforced above when trying to # publish a collection. if rights_manager.is_collection_public(collection.id): validate_exps_in_collection_are_public(collection) collection_model = collection_models.CollectionModel.get(collection.id, strict=False) if collection_model is None: collection_model = collection_models.CollectionModel(id=collection.id) else: if collection.version > collection_model.version: raise Exception( 'Unexpected error: trying to update version %s of collection ' 'from version %s. Please reload the page and try again.' % (collection_model.version, collection.version)) elif collection.version < collection_model.version: raise Exception( 'Trying to update version %s of collection from version %s, ' 'which is too old. Please reload the page and try again.' % (collection_model.version, collection.version)) collection_model.category = collection.category collection_model.title = collection.title collection_model.objective = collection.objective collection_model.language_code = collection.language_code collection_model.tags = collection.tags collection_model.schema_version = collection.schema_version collection_model.collection_contents = { 'nodes': [collection_node.to_dict() for collection_node in collection.nodes], 'skills': { skill_id: skill.to_dict() for skill_id, skill in collection.skills.iteritems() }, 'next_skill_index': collection.next_skill_index } collection_model.node_count = len(collection_model.nodes) collection_model.commit(committer_id, commit_message, change_list) memcache_services.delete(_get_collection_memcache_key(collection.id)) index_collections_given_ids([collection.id]) collection.version += 1
def get_learner_collection_dict_by_id(collection_id, user, strict=True, allow_invalid_explorations=False, version=None): """Gets a dictionary representation of a collection given by the provided collection ID. This dict includes user-specific playthrough information. Args: collection_id: str. The id of the collection. user: UserActionsInfo. Object having user_id, role and actions for given user. strict: bool. Whether to fail noisily if no collection with the given id exists in the datastore. allow_invalid_explorations: bool. Whether to also return explorations that are invalid, such as deleted/private explorations. version: str or None. The version number of the collection to be retrieved. If it is None, the latest version will be retrieved. Returns: dict. A dictionary that contains extra information along with the dict returned by collection_domain.Collection.to_dict() which includes useful data for the collection learner view. The information includes progress in the collection, information about explorations referenced within the collection, and a slightly nicer data structure for frontend work. Raises: ValidationError. If the collection retrieved using the given ID references non-existent explorations. """ collection = collection_services.get_collection_by_id(collection_id, strict=strict, version=version) exp_ids = collection.exploration_ids exp_summary_dicts = get_displayable_exp_summary_dicts_matching_ids( exp_ids, user=user) exp_summaries_dict_map = { exp_summary_dict['id']: exp_summary_dict for exp_summary_dict in exp_summary_dicts } # TODO(bhenning): Users should not be recommended explorations they have # completed outside the context of a collection (see #1461). next_exploration_id = None completed_exp_ids = None if user.user_id: completed_exp_ids = ( collection_services.get_valid_completed_exploration_ids( user.user_id, collection)) next_exploration_id = collection.get_next_exploration_id( completed_exp_ids) else: # If the user is not logged in or they have not completed any of # the explorations yet within the context of this collection, # recommend the initial exploration. next_exploration_id = collection.first_exploration_id completed_exp_ids = [] collection_dict = collection.to_dict() collection_dict['nodes'] = [node.to_dict() for node in collection.nodes] collection_dict['playthrough_dict'] = { 'next_exploration_id': next_exploration_id, 'completed_exploration_ids': completed_exp_ids } collection_dict['version'] = collection.version collection_is_public = rights_manager.is_collection_public(collection_id) # Insert an 'exploration' dict into each collection node, where the # dict includes meta information about the exploration (ID and title). for collection_node in collection_dict['nodes']: exploration_id = collection_node['exploration_id'] summary_dict = exp_summaries_dict_map.get(exploration_id) if not allow_invalid_explorations: if not summary_dict: raise utils.ValidationError( 'Expected collection to only reference valid ' 'explorations, but found an exploration with ID: %s (was ' 'the exploration deleted or is it a private exploration ' 'that you do not have edit access to?)' % exploration_id) if collection_is_public and rights_manager.is_exploration_private( exploration_id): raise utils.ValidationError( 'Cannot reference a private exploration within a public ' 'collection, exploration ID: %s' % exploration_id) if summary_dict: collection_node['exploration_summary'] = summary_dict else: collection_node['exploration_summary'] = None return collection_dict
def get_learner_collection_dict_by_id( collection_id, user_id, strict=True, allow_invalid_explorations=False, version=None): """Creates and returns a dictionary representation of a collection given by the provided collection ID. This dictionary contains extra information along with the dict returned by collection_domain.Collection.to_dict() which includes useful data for the collection learner view. The information includes progress in the collection, information about explorations referenced within the collection, and a slightly nicer data structure for frontend work. This raises a ValidationError if the collection retrieved using the given ID references non-existent explorations. which includes useful data for the collection learner view. """ collection = collection_services.get_collection_by_id( collection_id, strict=strict, version=version) exp_ids = collection.exploration_ids exp_summary_dicts = get_displayable_exp_summary_dicts_matching_ids( exp_ids, editor_user_id=user_id) exp_summaries_dict_map = { exp_summary_dict['id']: exp_summary_dict for exp_summary_dict in exp_summary_dicts } # TODO(bhenning): Users should not be recommended explorations they have # completed outside the context of a collection (see #1461). next_exploration_ids = None completed_exp_ids = None if user_id: completed_exp_ids = ( collection_services.get_valid_completed_exploration_ids( user_id, collection_id, collection)) next_exploration_ids = collection.get_next_exploration_ids( completed_exp_ids) else: # If the user is not logged in or they have not completed any of # the explorations yet within the context of this collection, # recommend the initial explorations. next_exploration_ids = collection.init_exploration_ids completed_exp_ids = [] collection_dict = collection.to_dict() collection_dict['skills'] = collection.skills collection_dict['playthrough_dict'] = { 'next_exploration_ids': next_exploration_ids, 'completed_exploration_ids': completed_exp_ids } collection_dict['version'] = collection.version collection_is_public = rights_manager.is_collection_public(collection_id) # Insert an 'exploration' dict into each collection node, where the # dict includes meta information about the exploration (ID and title). for collection_node in collection_dict['nodes']: exploration_id = collection_node['exploration_id'] summary_dict = exp_summaries_dict_map.get(exploration_id) if not allow_invalid_explorations: if not summary_dict: raise utils.ValidationError( 'Expected collection to only reference valid ' 'explorations, but found an exploration with ID: %s (was ' 'the exploration deleted or is it a private exploration ' 'that you do not have edit access to?)' % exploration_id) if collection_is_public and rights_manager.is_exploration_private( exploration_id): raise utils.ValidationError( 'Cannot reference a private exploration within a public ' 'collection, exploration ID: %s' % exploration_id) if summary_dict: collection_node['exploration_summary'] = summary_dict else: collection_node['exploration_summary'] = None return collection_dict
def get_learner_collection_dict_by_id(collection_id, user_id, strict=True, allow_invalid_explorations=False, version=None): """Creates and returns a dictionary representation of a collection given by the provided collection ID. This dictionary contains extra information along with the dict returned by collection_domain.Collection.to_dict() which includes useful data for the collection learner view. The information includes progress in the collection, information about explorations referenced within the collection, and a slightly nicer data structure for frontend work. This raises a ValidationError if the collection retrieved using the given ID references non-existent explorations. which includes useful data for the collection learner view. """ collection = collection_services.get_collection_by_id(collection_id, strict=strict, version=version) exp_ids = collection.exploration_ids exp_summary_dicts = get_displayable_exp_summary_dicts_matching_ids( exp_ids, editor_user_id=user_id) exp_summaries_dict_map = { exp_summary_dict['id']: exp_summary_dict for exp_summary_dict in exp_summary_dicts } # TODO(bhenning): Users should not be recommended explorations they have # completed outside the context of a collection (see #1461). next_exploration_ids = None completed_exp_ids = None if user_id: completed_exp_ids = ( collection_services.get_valid_completed_exploration_ids( user_id, collection_id, collection)) next_exploration_ids = collection.get_next_exploration_ids( completed_exp_ids) else: # If the user is not logged in or they have not completed any of # the explorations yet within the context of this collection, # recommend the initial explorations. next_exploration_ids = collection.init_exploration_ids completed_exp_ids = [] collection_dict = collection.to_dict() collection_dict['skills'] = collection.skills collection_dict['playthrough_dict'] = { 'next_exploration_ids': next_exploration_ids, 'completed_exploration_ids': completed_exp_ids } collection_dict['version'] = collection.version collection_is_public = rights_manager.is_collection_public(collection_id) # Insert an 'exploration' dict into each collection node, where the # dict includes meta information about the exploration (ID and title). for collection_node in collection_dict['nodes']: exploration_id = collection_node['exploration_id'] summary_dict = exp_summaries_dict_map.get(exploration_id) if not allow_invalid_explorations: if not summary_dict: raise utils.ValidationError( 'Expected collection to only reference valid ' 'explorations, but found an exploration with ID: %s (was ' 'the exploration deleted or is it a private exploration ' 'that you do not have edit access to?)' % exploration_id) if collection_is_public and rights_manager.is_exploration_private( exploration_id): raise utils.ValidationError( 'Cannot reference a private exploration within a public ' 'collection, exploration ID: %s' % exploration_id) if summary_dict: collection_node['exploration_summary'] = summary_dict else: collection_node['exploration_summary'] = None return collection_dict
def _save_collection(committer_id, collection, commit_message, change_list): """Validates an collection and commits it to persistent storage. If successful, increments the version number of the incoming collection domain object by 1. """ if not change_list: raise Exception( 'Unexpected error: received an invalid change list when trying to ' 'save collection %s: %s' % (collection.id, change_list)) collection_rights = rights_manager.get_collection_rights(collection.id) if collection_rights.status != rights_manager.ACTIVITY_STATUS_PRIVATE: collection.validate(strict=True) else: collection.validate(strict=False) # Validate that all explorations referenced by the collection exist. exp_ids = collection.exploration_ids exp_summaries = ( exp_services.get_exploration_summaries_matching_ids(exp_ids)) exp_summaries_dict = { exp_id: exp_summaries[ind] for (ind, exp_id) in enumerate(exp_ids) } for collection_node in collection.nodes: if not exp_summaries_dict[collection_node.exploration_id]: raise utils.ValidationError( 'Expected collection to only reference valid explorations, ' 'but found an exploration with ID: %s (was it deleted?)' % collection_node.exploration_id) # Ensure no explorations are being added that are 'below' the public status # of this collection. If the collection is private, it can have both # private and public explorations. If it's public, it can only have public # explorations. # TODO(bhenning): Ensure the latter is enforced above when trying to # publish a collection. if rights_manager.is_collection_public(collection.id): validate_exps_in_collection_are_public(collection) collection_model = collection_models.CollectionModel.get( collection.id, strict=False) if collection_model is None: collection_model = collection_models.CollectionModel(id=collection.id) else: if collection.version > collection_model.version: raise Exception( 'Unexpected error: trying to update version %s of collection ' 'from version %s. Please reload the page and try again.' % (collection_model.version, collection.version)) elif collection.version < collection_model.version: raise Exception( 'Trying to update version %s of collection from version %s, ' 'which is too old. Please reload the page and try again.' % (collection_model.version, collection.version)) collection_model.category = collection.category collection_model.title = collection.title collection_model.objective = collection.objective collection_model.language_code = collection.language_code collection_model.tags = collection.tags collection_model.schema_version = collection.schema_version collection_model.nodes = [ collection_node.to_dict() for collection_node in collection.nodes ] collection_model.node_count = len(collection_model.nodes) collection_model.commit(committer_id, commit_message, change_list) memcache_services.delete(_get_collection_memcache_key(collection.id)) index_collections_given_ids([collection.id]) collection.version += 1