Example #1
0
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)
Example #2
0
    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)
Example #3
0
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)
Example #4
0
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]))
Example #5
0
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]))
Example #6
0
    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)
Example #7
0
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)
Example #8
0
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
Example #9
0
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
Example #10
0
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
    ]
Example #11
0
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]
Example #12
0
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
Example #13
0
    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)
Example #14
0
    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)
Example #15
0
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
Example #16
0
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
Example #17
0
    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)
Example #18
0
    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)
Example #19
0
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
Example #20
0
    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)