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_domain.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_fetchers.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 cannot be none as we are passing the collection as a # parameter and also this function is called by update_collection which only # works if the collection is put into the datastore. collection_model = collection_models.CollectionModel.get(collection.id, strict=False) 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] } collection_model.node_count = len(collection_model.nodes) collection_model.commit(committer_id, commit_message, change_list) caching_services.delete_multi(caching_services.CACHE_NAMESPACE_COLLECTION, None, [collection.id]) index_collections_given_ids([collection.id]) collection.version += 1
def get(self): """Handles GET requests.""" def _round_average_ratings(rating): """Returns the rounded average rating to display on the creator dashboard. Args: rating: float. The rating of the lesson. Returns: float. The rounded average value of rating. """ return round(rating, feconf.AVERAGE_RATINGS_DASHBOARD_PRECISION) # We need to do the filtering because some activities that were # originally subscribed to may have been deleted since. subscribed_exploration_summaries = [ summary for summary in exp_fetchers.get_exploration_summaries_matching_ids( subscription_services.get_exploration_ids_subscribed_to( self.user_id)) if summary is not None] subscribed_collection_summaries = [ summary for summary in collection_services.get_collection_summaries_matching_ids( subscription_services.get_collection_ids_subscribed_to( self.user_id)) if summary is not None] exploration_ids_subscribed_to = [ summary.id for summary in subscribed_exploration_summaries] exp_summary_dicts = summary_services.get_displayable_exp_summary_dicts( subscribed_exploration_summaries) collection_summary_dicts = [] feedback_thread_analytics = ( feedback_services.get_thread_analytics_multi( exploration_ids_subscribed_to)) # TODO(bhenning): Update this to use unresolved answers from # stats_services once the training interface is enabled and it's cheaper # to retrieve top answers from stats_services. for ind, exploration in enumerate(exp_summary_dicts): exploration.update(feedback_thread_analytics[ind].to_dict()) exp_summary_dicts = sorted( exp_summary_dicts, key=lambda x: (x['num_open_threads'], x['last_updated_msec']), reverse=True) if constants.ENABLE_NEW_STRUCTURE_PLAYERS: topic_summaries = topic_services.get_all_topic_summaries() topic_summary_dicts = [ summary.to_dict() for summary in topic_summaries] if role_services.ACTION_CREATE_COLLECTION in self.user.actions: for collection_summary in subscribed_collection_summaries: # TODO(sll): Reuse _get_displayable_collection_summary_dicts() # in summary_services, instead of replicating it like this. collection_summary_dicts.append({ 'id': collection_summary.id, 'title': collection_summary.title, 'category': collection_summary.category, 'objective': collection_summary.objective, 'language_code': collection_summary.language_code, 'last_updated': utils.get_time_in_millisecs( collection_summary.collection_model_last_updated), 'created_on': utils.get_time_in_millisecs( collection_summary.collection_model_created_on), 'status': collection_summary.status, 'node_count': collection_summary.node_count, 'community_owned': collection_summary.community_owned, 'thumbnail_icon_url': ( utils.get_thumbnail_icon_url_for_category( collection_summary.category)), 'thumbnail_bg_color': utils.get_hex_color_for_category( collection_summary.category), }) dashboard_stats = ( user_jobs_continuous.UserStatsAggregator.get_dashboard_stats( self.user_id)) dashboard_stats.update({ 'total_open_feedback': feedback_services.get_total_open_threads( feedback_thread_analytics) }) if dashboard_stats and dashboard_stats.get('average_ratings'): dashboard_stats['average_ratings'] = ( _round_average_ratings(dashboard_stats['average_ratings'])) last_week_stats = ( user_services.get_last_week_dashboard_stats(self.user_id)) if last_week_stats and len(last_week_stats.keys()) != 1: logging.error( '\'last_week_stats\' should contain only one key-value pair' ' denoting last week dashboard stats of the user keyed by a' ' datetime string.') last_week_stats = None if last_week_stats: # 'last_week_stats' is a dict with only one key-value pair denoting # last week dashboard stats of the user keyed by a datetime string. datetime_of_stats = last_week_stats.keys()[0] last_week_stats_average_ratings = ( last_week_stats.values()[0].get('average_ratings')) if last_week_stats_average_ratings: last_week_stats[datetime_of_stats]['average_ratings'] = ( _round_average_ratings(last_week_stats_average_ratings)) subscriber_ids = subscription_services.get_all_subscribers_of_creator( self.user_id) subscribers_settings = user_services.get_users_settings(subscriber_ids) subscribers_list = [] for index, subscriber_settings in enumerate(subscribers_settings): subscriber_summary = { 'subscriber_picture_data_url': ( subscriber_settings.profile_picture_data_url), 'subscriber_username': subscriber_settings.username, 'subscriber_impact': ( user_services.get_user_impact_score(subscriber_ids[index])) } subscribers_list.append(subscriber_summary) user_settings = user_services.get_user_settings( self.user_id, strict=False) creator_dashboard_display_pref = ( user_settings.creator_dashboard_display_pref) suggestions_created_by_user = suggestion_services.query_suggestions( [('author_id', self.user_id), ( 'suggestion_type', suggestion_models.SUGGESTION_TYPE_EDIT_STATE_CONTENT)]) suggestions_which_can_be_reviewed = ( suggestion_services .get_all_suggestions_that_can_be_reviewed_by_user(self.user_id)) for s in suggestions_created_by_user: s.populate_old_value_of_change() for s in suggestions_which_can_be_reviewed: s.populate_old_value_of_change() suggestion_dicts_created_by_user = ( [s.to_dict() for s in suggestions_created_by_user]) suggestion_dicts_which_can_be_reviewed = ( [s.to_dict() for s in suggestions_which_can_be_reviewed]) ids_of_suggestions_created_by_user = ( [s['suggestion_id'] for s in suggestion_dicts_created_by_user]) ids_of_suggestions_which_can_be_reviewed = ( [s['suggestion_id'] for s in suggestion_dicts_which_can_be_reviewed]) threads_linked_to_suggestions_by_user = ( [t.to_dict() for t in feedback_services.get_multiple_threads( ids_of_suggestions_created_by_user)]) threads_linked_to_suggestions_which_can_be_reviewed = ( [t.to_dict() for t in feedback_services.get_multiple_threads( ids_of_suggestions_which_can_be_reviewed)]) self.values.update({ 'explorations_list': exp_summary_dicts, 'collections_list': collection_summary_dicts, 'dashboard_stats': dashboard_stats, 'last_week_stats': last_week_stats, 'subscribers_list': subscribers_list, 'display_preference': creator_dashboard_display_pref, 'threads_for_created_suggestions_list': ( threads_linked_to_suggestions_by_user), 'threads_for_suggestions_to_review_list': ( threads_linked_to_suggestions_which_can_be_reviewed), 'created_suggestions_list': suggestion_dicts_created_by_user, 'suggestions_to_review_list': suggestion_dicts_which_can_be_reviewed }) if constants.ENABLE_NEW_STRUCTURE_PLAYERS: self.values.update({ 'topic_summary_dicts': topic_summary_dicts }) self.render_json(self.values)
def get_library_groups(language_codes): """Returns a list of groups for the library index page. Each group has a header and a list of dicts representing activity summaries. Args: language_codes: list(str). A list of language codes. Only explorations with these languages will be returned. Returns: list(dict). A list of groups for the library index page. Each group is represented by a dict with the following keys and values: - activity_summary_dicts: list(dict). A list of dicts representing activity summaries. - categories: list(str). The list of group categories. - header_i18n_id: str. The i18n id for the header of the category. - has_full_results_page: bool. Whether the group header links to a "full results" page. This is always True for the "exploration category" groups. - full_results_url: str. The URL to the corresponding "full results" page. """ # Collect all collection ids so that the summary details can be retrieved # with a single get_multi() call. all_collection_ids = [] header_id_to_collection_ids = {} for group in _LIBRARY_INDEX_GROUPS: collection_ids = search_services.search_collections( '', group['search_categories'], language_codes, 8)[0] header_id_to_collection_ids[group['header_i18n_id']] = collection_ids all_collection_ids += collection_ids collection_summaries = [ summary for summary in collection_services. get_collection_summaries_matching_ids(all_collection_ids) if summary is not None ] collection_summary_dicts = { summary_dict['id']: summary_dict for summary_dict in _get_displayable_collection_summary_dicts( collection_summaries) } # Collect all exp ids so that the summary details can be retrieved with a # single get_multi() call. all_exp_ids = [] header_to_exp_ids = {} for group in _LIBRARY_INDEX_GROUPS: exp_ids = search_services.search_explorations( '', group['search_categories'], language_codes, 8)[0] header_to_exp_ids[group['header_i18n_id']] = exp_ids all_exp_ids += exp_ids exp_summaries = [ summary for summary in exp_fetchers.get_exploration_summaries_matching_ids(all_exp_ids) if summary is not None ] exp_summary_dicts = { summary_dict['id']: summary_dict for summary_dict in get_displayable_exp_summary_dicts(exp_summaries) } results = [] for group in _LIBRARY_INDEX_GROUPS: summary_dicts = [] collection_ids_to_display = ( header_id_to_collection_ids[group['header_i18n_id']]) summary_dicts = [ collection_summary_dicts[collection_id] for collection_id in collection_ids_to_display if collection_id in collection_summary_dicts ] exp_ids_to_display = header_to_exp_ids[group['header_i18n_id']] summary_dicts += [ exp_summary_dicts[exp_id] for exp_id in exp_ids_to_display if exp_id in exp_summary_dicts ] if not summary_dicts: continue results.append({ 'header_i18n_id': group['header_i18n_id'], 'categories': group['search_categories'], 'activity_summary_dicts': summary_dicts, 'has_full_results_page': True, 'full_results_url': None, }) return results
def _save_story(committer_id, story, commit_message, change_list): """Validates a story and commits it to persistent storage. If successful, increments the version number of the incoming story domain object by 1. Args: committer_id: str. ID of the given committer. story: Story. The story domain object to be saved. commit_message: str. The commit message. change_list: list(StoryChange). List of changes applied to a story. Raises: ValidationError: An invalid exploration was referenced in the story. Exception: The story model and the incoming story domain object have different version numbers. """ if not change_list: raise Exception( 'Unexpected error: received an invalid change list when trying to ' 'save story %s: %s' % (story.id, change_list)) story.validate() # Validate that all explorations referenced by the story exist. exp_ids = [] for node in story.story_contents.nodes: if node.exploration_id is not None: exp_ids.append(node.exploration_id) exp_summaries = ( exp_fetchers.get_exploration_summaries_matching_ids(exp_ids)) exp_summaries_dict = { exp_id: exp_summaries[ind] for (ind, exp_id) in enumerate(exp_ids) } for node in story.story_contents.nodes: if (node.exploration_id is not None) and (not exp_summaries_dict[node.exploration_id]): raise utils.ValidationError( 'Expected story to only reference valid explorations, ' 'but found an exploration with ID: %s (was it deleted?)' % node.exploration_id) # Story model cannot be None as story is passed as parameter here and that # is only possible if a story model with that story id exists. Also this is # a private function and so it cannot be called independently with any # story object. story_model = story_models.StoryModel.get(story.id) if story.version > story_model.version: raise Exception( 'Unexpected error: trying to update version %s of story ' 'from version %s. Please reload the page and try again.' % (story_model.version, story.version)) elif story.version < story_model.version: raise Exception( 'Trying to update version %s of story from version %s, ' 'which is too old. Please reload the page and try again.' % (story_model.version, story.version)) topic = topic_fetchers.get_topic_by_id(story.corresponding_topic_id, strict=False) if topic is None: raise utils.ValidationError( 'Expected story to only belong to a valid topic, but found an ' 'topic with ID: %s' % story.corresponding_topic_id) canonical_story_ids = topic.get_canonical_story_ids() additional_story_ids = topic.get_additional_story_ids() if story.id not in canonical_story_ids + additional_story_ids: raise Exception( 'Expected story to belong to the topic %s, but it is ' 'neither a part of the canonical stories or the additional stories ' 'of the topic.' % story.corresponding_topic_id) story_model.description = story.description story_model.title = story.title story_model.notes = story.notes story_model.language_code = story.language_code story_model.story_contents_schema_version = ( story.story_contents_schema_version) story_model.story_contents = story.story_contents.to_dict() story_model.corresponding_topic_id = story.corresponding_topic_id story_model.version = story.version change_dicts = [change.to_dict() for change in change_list] story_model.commit(committer_id, commit_message, change_dicts) memcache_services.delete(story_fetchers.get_story_memcache_key(story.id)) story.version += 1