def get_displayable_exp_summary_dicts_matching_ids(exploration_ids, user=None): """Gets a summary of explorations in human readable form from exploration ids. Given a list of exploration ids, optionally filters the list for explorations that are currently non-private and not deleted, and returns a list of dicts of the corresponding exploration summaries. This function can also filter based on a user ID who has edit access to the corresponding exploration, where the editor ID is for private explorations. Please use this function when needing summary information to display on exploration summary tiles in the frontend. Args: exploration_ids: list(str). List of exploration ids. user: UserActionsInfo or None. Object having user_id, role and actions for given user. Return: list(dict). A list of exploration summary dicts in human readable form. Example: [ { 'category': u'A category', 'community_owned': False, 'id': 'eid2', 'language_code': 'en', 'num_views': 0, 'objective': u'An objective', 'status': 'public', 'tags': [], 'thumbnail_bg_color': '#a33f40', 'thumbnail_icon_url': self.get_static_asset_url( '/images/subjects/Lightbulb.svg'), 'title': u'Exploration 2 Albert title', }, ] """ exploration_summaries = ( exp_services.get_exploration_summaries_matching_ids(exploration_ids)) exploration_rights_objects = ( rights_manager.get_multiple_exploration_rights_by_ids(exploration_ids)) filtered_exploration_summaries = [] for (exploration_summary, exploration_rights) in ( zip(exploration_summaries, exploration_rights_objects)): if exploration_summary is None: continue if exploration_rights is None: continue if exploration_summary.status == ( rights_manager.ACTIVITY_STATUS_PRIVATE): if user is None: continue if not rights_manager.check_can_edit_activity( user, exploration_rights): continue filtered_exploration_summaries.append(exploration_summary) return get_displayable_exp_summary_dicts(filtered_exploration_summaries)
def get(self): """Handles GET requests.""" try: exp_ids = json.loads(self.request.get('stringified_exp_ids')) except Exception: raise self.PageNotFoundException if (not isinstance(exp_ids, list) or not all([ isinstance(exp_id, basestring) for exp_id in exp_ids])): raise self.PageNotFoundException exp_summaries = exp_services.get_exploration_summaries_matching_ids( exp_ids) self.values.update({ 'summaries': [(None if exp_summary is None else { 'id': exp_summary.id, 'title': exp_summary.title, 'category': exp_summary.category, 'objective': exp_summary.objective, 'language_code': exp_summary.language_code, 'last_updated': utils.get_time_in_millisecs( exp_summary.exploration_model_last_updated), 'status': exp_summary.status, 'community_owned': exp_summary.community_owned, 'thumbnail_image_url': exp_summary.thumbnail_image_url, }) for exp_summary in exp_summaries] }) self.render_json(self.values)
def get_displayable_exp_summary_dicts_matching_ids(exploration_ids, editor_user_id=None): """Given a list of exploration ids, optionally filters the list for explorations that are currently non-private and not deleted, and returns a list of dicts of the corresponding exploration summaries. This function can also filter based on a user ID who has edit access to the corresponding exploration, where the editor ID is for private explorations. Please use this function when needing summary information to display on exploration summary tiles in the frontend. """ exploration_summaries = ( exp_services.get_exploration_summaries_matching_ids(exploration_ids)) filtered_exploration_summaries = [] for exploration_summary in exploration_summaries: if exploration_summary is None: continue if exploration_summary.status == ( rights_manager.ACTIVITY_STATUS_PRIVATE): if editor_user_id is None: continue if not rights_manager.Actor(editor_user_id).can_edit( feconf.ACTIVITY_TYPE_EXPLORATION, exploration_summary.id): continue filtered_exploration_summaries.append(exploration_summary) return get_displayable_exp_summary_dicts(filtered_exploration_summaries)
def require_activities_to_be_public(activity_references): """Raises an exception if any activity reference in the list does not exist, or is not public. """ exploration_ids, collection_ids = activity_services.split_by_type( activity_references) activity_summaries_by_type = [{ 'type': feconf.ACTIVITY_TYPE_EXPLORATION, 'ids': exploration_ids, 'summaries': exp_services.get_exploration_summaries_matching_ids(exploration_ids), }, { 'type': feconf.ACTIVITY_TYPE_COLLECTION, 'ids': collection_ids, 'summaries': collection_services.get_collection_summaries_matching_ids( collection_ids), }] for activities_info in activity_summaries_by_type: for index, summary in enumerate(activities_info['summaries']): if summary is None: raise Exception( 'Cannot feature non-existent %s with id %s' % (activities_info['type'], activities_info['ids'][index])) if summary.status == rights_manager.ACTIVITY_STATUS_PRIVATE: raise Exception( 'Cannot feature private %s with id %s' % (activities_info['type'], activities_info['ids'][index]))
def require_activities_to_be_public(activity_references): """Raises an exception if any activity reference in the list does not exist, or is not public. """ exploration_ids, collection_ids = activity_services.split_by_type( activity_references) activity_summaries_by_type = [{ 'type': feconf.ACTIVITY_TYPE_EXPLORATION, 'ids': exploration_ids, 'summaries': exp_services.get_exploration_summaries_matching_ids( exploration_ids), }, { 'type': feconf.ACTIVITY_TYPE_COLLECTION, 'ids': collection_ids, 'summaries': collection_services.get_collection_summaries_matching_ids( collection_ids), }] for activities_info in activity_summaries_by_type: for index, summary in enumerate(activities_info['summaries']): if summary is None: raise Exception( 'Cannot feature non-existent %s with id %s' % (activities_info['type'], activities_info['ids'][index])) if summary.status == rights_manager.ACTIVITY_STATUS_PRIVATE: raise Exception( 'Cannot feature private %s with id %s' % (activities_info['type'], activities_info['ids'][index]))
def get(self): """Handles GET requests.""" if self.user_id is None: raise self.PageNotFoundException subscribed_summaries = ( exp_services.get_exploration_summaries_matching_ids( subscription_services.get_exploration_ids_subscribed_to( self.user_id))) def _get_intro_card_color(category): return ( feconf.CATEGORIES_TO_COLORS[category] if category in feconf.CATEGORIES_TO_COLORS else feconf.DEFAULT_COLOR) explorations_list = [] for exp_summary in subscribed_summaries: if exp_summary is None: continue feedback_thread_analytics = feedback_services.get_thread_analytics( exp_summary.id) explorations_list.append({ 'id': exp_summary.id, 'title': exp_summary.title, 'category': exp_summary.category, 'objective': exp_summary.objective, 'language_code': exp_summary.language_code, 'last_updated': utils.get_time_in_millisecs( exp_summary.exploration_model_last_updated), 'created_on': utils.get_time_in_millisecs( exp_summary.exploration_model_created_on), 'status': exp_summary.status, 'community_owned': exp_summary.community_owned, 'is_editable': True, 'thumbnail_icon_url': ( utils.get_thumbnail_icon_url_for_category( exp_summary.category)), 'thumbnail_bg_color': utils.get_hex_color_for_category( exp_summary.category), 'ratings': exp_summary.ratings, 'num_open_threads': ( feedback_thread_analytics.num_open_threads), 'num_total_threads': ( feedback_thread_analytics.num_total_threads), }) explorations_list = sorted( explorations_list, key=lambda x: (x['num_open_threads'], x['last_updated']), reverse=True) self.values.update({ 'explorations_list': explorations_list, }) self.render_json(self.values)
def get_displayable_exp_summary_dicts_matching_ids( exploration_ids, editor_user_id=None): """Gets a summary of explorations in human readable form from exploration ids. Given a list of exploration ids, optionally filters the list for explorations that are currently non-private and not deleted, and returns a list of dicts of the corresponding exploration summaries. This function can also filter based on a user ID who has edit access to the corresponding exploration, where the editor ID is for private explorations. Please use this function when needing summary information to display on exploration summary tiles in the frontend. Args: exploration_ids: list(str). List of exploration ids. editor_user_id: str or None. If provided, the returned value is filtered based on a user ID who has edit access to the corresponding explorations. Otherwise, the returned list is not filtered. Return: list(dict). A list of exploration summary dicts in human readable form. Example: [ { 'category': u'A category', 'community_owned': False, 'id': 'eid2', 'language_code': 'en', 'num_views': 0, 'objective': u'An objective', 'status': 'public', 'tags': [], 'thumbnail_bg_color': '#a33f40', 'thumbnail_icon_url': self.get_static_asset_url( '/images/subjects/Lightbulb.svg'), 'title': u'Exploration 2 Albert title', }, ] """ exploration_summaries = ( exp_services.get_exploration_summaries_matching_ids(exploration_ids)) filtered_exploration_summaries = [] for exploration_summary in exploration_summaries: if exploration_summary is None: continue if exploration_summary.status == ( rights_manager.ACTIVITY_STATUS_PRIVATE): if editor_user_id is None: continue if not rights_manager.Actor(editor_user_id).can_edit( feconf.ACTIVITY_TYPE_EXPLORATION, exploration_summary.id): continue filtered_exploration_summaries.append(exploration_summary) return get_displayable_exp_summary_dicts(filtered_exploration_summaries)
def get_displayable_exp_summary_dicts_matching_ids(exploration_ids): """Given a list of exploration ids, filters the list for explorations that are currently non-private and not deleted, and returns a list of dicts of the corresponding exploration summaries. """ displayable_exp_summaries = [] exploration_summaries = ( exp_services.get_exploration_summaries_matching_ids(exploration_ids)) view_counts = (stats_jobs_continuous.StatisticsAggregator.get_views_multi( exploration_ids)) for ind, exploration_summary in enumerate(exploration_summaries): if exploration_summary and exploration_summary.status != ( rights_manager.ACTIVITY_STATUS_PRIVATE): displayable_exp_summaries.append({ 'id': exploration_summary.id, 'title': exploration_summary.title, 'category': exploration_summary.category, 'objective': exploration_summary.objective, 'language_code': exploration_summary.language_code, 'last_updated_msec': utils.get_time_in_millisecs( exploration_summary.exploration_model_last_updated), 'status': exploration_summary.status, 'ratings': exploration_summary.ratings, 'community_owned': exploration_summary.community_owned, 'human_readable_contributors_summary': get_human_readable_contributors_summary( exploration_summary.contributors_summary), 'contributor_names': user_services.get_human_readable_user_ids( exploration_summary.contributor_ids), 'tags': exploration_summary.tags, 'thumbnail_icon_url': utils.get_thumbnail_icon_url_for_category( exploration_summary.category), 'thumbnail_bg_color': utils.get_hex_color_for_category(exploration_summary.category), 'num_views': view_counts[ind], }) return displayable_exp_summaries
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. """ language_codes_suffix = '' if language_codes: language_codes_suffix = ' language_code=("%s")' % ( '" OR "'.join(language_codes)) def _generate_query(categories): # This assumes that 'categories' is non-empty. return 'category=("%s")%s' % ('" OR "'.join(categories), language_codes_suffix) # 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 = exp_services.search_explorations( _generate_query(group['search_categories']), 8)[0] header_to_exp_ids[group['header']] = exp_ids all_exp_ids += exp_ids all_summaries = [ summary for summary in exp_services.get_exploration_summaries_matching_ids(all_exp_ids) if summary is not None ] all_summary_dicts = { summary_dict['id']: summary_dict for summary_dict in _get_displayable_exp_summary_dicts(all_summaries) } results = [] for group in _LIBRARY_INDEX_GROUPS: exp_ids_to_display = header_to_exp_ids[group['header']] summary_dicts = [ all_summary_dicts[exp_id] for exp_id in exp_ids_to_display if exp_id in all_summary_dicts ] if not summary_dicts: continue results.append({ 'header': group['header'], 'categories': group['search_categories'], 'activity_summary_dicts': summary_dicts, }) return results
def get_exploration_metadata_dicts(exploration_ids, user): """Given a list of exploration ids, optionally filters the list for explorations that are currently non-private and not deleted, and returns a list of dicts of the corresponding exploration summaries for collection node search. Args: exploration_ids: list(str). A list of exploration ids for which exploration metadata dicts are to be returned. user: UserActionsInfo. Object having user_id, role and actions for given user. Returns: list(dict). A list of metadata dicts corresponding to the given exploration ids. Each dict has three keys: 'id': the exploration id; 'title': the exploration title; 'objective': the exploration objective. """ exploration_summaries = ( exp_services.get_exploration_summaries_matching_ids(exploration_ids)) exploration_rights_objects = ( rights_manager.get_multiple_exploration_rights_by_ids(exploration_ids)) filtered_exploration_summaries = [] for (exploration_summary, exploration_rights) in (zip(exploration_summaries, exploration_rights_objects)): if exploration_summary is None: continue if exploration_rights is None: continue if exploration_summary.status == ( rights_manager.ACTIVITY_STATUS_PRIVATE): if user.user_id is None: continue if not rights_manager.check_can_edit_activity( user, exploration_rights): continue filtered_exploration_summaries.append(exploration_summary) return [ summary.to_metadata_dict() for summary in filtered_exploration_summaries ]
def get_exploration_metadata_dicts(exploration_ids, editor_user_id=None): """Given a list of exploration ids, optionally filters the list for explorations that are currently non-private and not deleted, and returns a list of dicts of the corresponding exploration summaries for collection node search. Args: exploration_ids: list(str). A list of exploration ids for which exploration metadata dicts are to be returned. editor_user_id: str or None. The id of the user performing the query. If not None, private explorations that are editable by this user are also returned. Returns: list(dict). A list of metadata dicts corresponding to the given exploration ids. Each dict has three keys: 'id': the exploration id; 'title': the exploration title; 'objective': the exploration objective. """ exploration_summaries = ( exp_services.get_exploration_summaries_matching_ids(exploration_ids)) filtered_exploration_summaries = [] for exploration_summary in exploration_summaries: if exploration_summary is None: continue if exploration_summary.status == ( rights_manager.ACTIVITY_STATUS_PRIVATE): if editor_user_id is None: continue if not rights_manager.Actor(editor_user_id).can_edit( feconf.ACTIVITY_TYPE_EXPLORATION, exploration_summary.id): continue filtered_exploration_summaries.append(exploration_summary) return [ summary.to_metadata_dict() for summary in filtered_exploration_summaries]
def get_incomplete_exp_summaries(user_id): """Returns a list of summaries of the incomplete exploration ids and the number of explorations deleted from the list as they are no longer present. Args: user_id: str. The id of the learner. Returns: list(ExplorationSummary). List of ExplorationSummary domain objects of the incomplete explorations. int. The number of explorations deleted from the list as they are no longer present. """ incomplete_exploration_ids = get_all_incomplete_exp_ids(user_id) number_deleted = 0 for exploration_id in incomplete_exploration_ids: if not exp_services.does_exploration_exists(exploration_id): number_deleted = number_deleted + 1 remove_exp_from_incomplete_list(user_id, exploration_id) return exp_services.get_exploration_summaries_matching_ids( incomplete_exploration_ids), number_deleted
def get(self): """Handles GET requests.""" def _get_intro_card_color(category): """Returns the intro card color according to the category. Args: category: str. The category of the lesson. Returns: str. The intro card color according to the category. """ return (constants.CATEGORIES_TO_COLORS[category] if category in constants.CATEGORIES_TO_COLORS else constants.DEFAULT_COLOR) 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_services.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 last_week_stats.get('average_ratings'): last_week_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(self, collection_id): """Populates the data on the individual collection page.""" try: collection = collection_services.get_collection_by_id( collection_id) except Exception as e: raise self.PageNotFoundException(e) 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) } # TODO(bhenning): Users should not be recommended explorations they # have completed outside the context of a collection. next_exploration_ids = None completed_exploration_ids = None if self.user_id: completed_exploration_ids = ( collection_services.get_completed_exploration_ids( self.user_id, collection_id)) next_exploration_ids = collection.get_next_exploration_ids( completed_exploration_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_exploration_ids = [] collection_dict = collection.to_dict() collection_dict['next_exploration_ids'] = next_exploration_ids collection_dict['completed_exploration_ids'] = ( completed_exploration_ids) # 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']: summary = exp_summaries_dict.get(collection_node['exploration_id']) collection_node['exploration'] = { 'id': collection_node['exploration_id'], 'title': summary.title if summary else None, 'category': summary.category if summary else None, 'objective': summary.objective if summary else None, 'ratings': summary.ratings if summary else None, 'last_updated_msec': utils.get_time_in_millisecs( summary.exploration_model_last_updated) if summary else None, 'thumbnail_icon_url': utils.get_thumbnail_icon_url_for_category(summary.category), 'thumbnail_bg_color': utils.get_hex_color_for_category(summary.category), } self.values.update({ 'can_edit': (self.user_id and rights_manager.Actor(self.user_id).can_edit( rights_manager.ACTIVITY_TYPE_COLLECTION, collection_id)), 'collection': collection_dict, 'info_card_image_url': utils.get_info_card_url_for_category(collection.category), 'is_logged_in': bool(self.user_id), 'session_id': utils.generate_new_session_id(), }) self.render_json(self.values)
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_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 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 = story_models.StoryModel.get(story.id, strict=False) if story_model is None: story_model = story_models.StoryModel(id=story.id) else: 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)) story_model.description = story.description story_model.title = story.title story_model.notes = story.notes story_model.language_code = story.language_code story_model.schema_version = story.schema_version story_model.story_contents = story.story_contents.to_dict() 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(_get_story_memcache_key(story.id)) story.version += 1
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. Return: 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. """ language_codes_suffix = '' if language_codes: language_codes_suffix = ' language_code=("%s")' % ( '" OR "'.join(language_codes)) def _generate_query(categories): # This assumes that 'categories' is non-empty. return 'category=("%s")%s' % ('" OR "'.join(categories), language_codes_suffix) # 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 = collection_services.search_collections( _generate_query(group['search_categories']), 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 = exp_services.search_explorations( _generate_query(group['search_categories']), 8)[0] header_to_exp_ids[group['header_i18n_id']] = exp_ids all_exp_ids += exp_ids exp_summaries = [ summary for summary in exp_services.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 get(self): """Handles GET requests.""" if self.user_id is None: raise self.PageNotFoundException def _get_intro_card_color(category): return (feconf.CATEGORIES_TO_COLORS[category] if category in feconf.CATEGORIES_TO_COLORS else feconf.DEFAULT_COLOR) def _round_average_ratings(rating): return round(rating, feconf.AVERAGE_RATINGS_DASHBOARD_PRECISION) exploration_ids_subscribed_to = ( subscription_services.get_exploration_ids_subscribed_to( self.user_id)) subscribed_exploration_summaries = filter( None, (exp_services.get_exploration_summaries_matching_ids( exploration_ids_subscribed_to))) subscribed_collection_summaries = filter( None, (collection_services.get_collection_summaries_matching_ids( subscription_services.get_collection_ids_subscribed_to( self.user_id)))) exp_summary_list = summary_services.get_displayable_exp_summary_dicts( subscribed_exploration_summaries) collection_summary_list = [] feedback_thread_analytics = ( feedback_services.get_thread_analytics_multi( exploration_ids_subscribed_to)) unresolved_answers_dict = ( stats_services.get_exps_unresolved_answers_for_default_rule( exploration_ids_subscribed_to)) total_unresolved_answers = 0 for ind, exploration in enumerate(exp_summary_list): exploration.update(feedback_thread_analytics[ind].to_dict()) exploration.update({ 'num_unresolved_answers': (unresolved_answers_dict[exploration['id']]['count'] if exploration['id'] in unresolved_answers_dict else 0), 'top_unresolved_answers': (unresolved_answers_dict[ exploration['id']]['unresolved_answers'] [:feconf.TOP_UNRESOLVED_ANSWERS_COUNT_DASHBOARD]) }) total_unresolved_answers += exploration['num_unresolved_answers'] exp_summary_list = sorted( exp_summary_list, key=lambda x: (x['num_open_threads'], x['last_updated_msec']), reverse=True) if (self.username in config_domain.WHITELISTED_COLLECTION_EDITOR_USERNAMES.value): 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_list.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), 'total_unresolved_answers': total_unresolved_answers }) 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 last_week_stats.get('average_ratings'): last_week_stats['average_ratings'] = (_round_average_ratings( last_week_stats['average_ratings'])) self.values.update({ 'explorations_list': exp_summary_list, 'collections_list': collection_summary_list, 'dashboard_stats': dashboard_stats, 'last_week_stats': last_week_stats }) self.render_json(self.values)
def get(self): """Handles GET requests.""" if self.user_id is None: raise self.PageNotFoundException def _get_intro_card_color(category): return (feconf.CATEGORIES_TO_COLORS[category] if category in feconf.CATEGORIES_TO_COLORS else feconf.DEFAULT_COLOR) subscribed_exploration_summaries = ( exp_services.get_exploration_summaries_matching_ids( subscription_services.get_exploration_ids_subscribed_to( self.user_id))) subscribed_collection_summaries = ( collection_services.get_collection_summaries_matching_ids( subscription_services.get_collection_ids_subscribed_to( self.user_id))) explorations_list = [] collections_list = [] for exp_summary in subscribed_exploration_summaries: if exp_summary is None: continue feedback_thread_analytics = feedback_services.get_thread_analytics( exp_summary.id) # TODO(sll): Reuse _get_displayable_exp_summary_dicts() in # summary_services, instead of replicating it like this. explorations_list.append({ 'id': exp_summary.id, 'title': exp_summary.title, 'category': exp_summary.category, 'objective': exp_summary.objective, 'language_code': exp_summary.language_code, 'last_updated': utils.get_time_in_millisecs( exp_summary.exploration_model_last_updated), 'created_on': utils.get_time_in_millisecs( exp_summary.exploration_model_created_on), 'status': exp_summary.status, 'community_owned': exp_summary.community_owned, 'thumbnail_icon_url': (utils.get_thumbnail_icon_url_for_category( exp_summary.category)), 'thumbnail_bg_color': utils.get_hex_color_for_category(exp_summary.category), 'ratings': exp_summary.ratings, 'num_open_threads': (feedback_thread_analytics.num_open_threads), 'num_total_threads': (feedback_thread_analytics.num_total_threads), }) explorations_list = sorted(explorations_list, key=lambda x: (x['num_open_threads'], x['last_updated']), reverse=True) if (self.username in config_domain.WHITELISTED_COLLECTION_EDITOR_USERNAMES.value): for collection_summary in subscribed_collection_summaries: if collection_summary is None: continue # TODO(sll): Reuse _get_displayable_collection_summary_dicts() # in summary_services, instead of replicating it like this. collections_list.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, '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), }) self.values.update({ 'explorations_list': explorations_list, 'collections_list': collections_list, }) self.render_json(self.values)
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(self): """Handles GET requests.""" def _get_intro_card_color(category): return ( constants.CATEGORIES_TO_COLORS[category] if category in constants.CATEGORIES_TO_COLORS else constants.DEFAULT_COLOR) def _round_average_ratings(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_services.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 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 last_week_stats.get('average_ratings'): last_week_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) 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, }) self.render_json(self.values)