예제 #1
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_domain.ACTIVITY_STATUS_PRIVATE:
        collection.validate(strict=True)
    else:
        collection.validate(strict=False)

    # Validate that all explorations referenced by the collection exist.
    exp_ids = collection.exploration_ids
    exp_summaries = (
        exp_fetchers.get_exploration_summaries_matching_ids(exp_ids))
    exp_summaries_dict = {
        exp_id: exp_summaries[ind]
        for (ind, exp_id) in enumerate(exp_ids)
    }
    for collection_node in collection.nodes:
        if not exp_summaries_dict[collection_node.exploration_id]:
            raise utils.ValidationError(
                'Expected collection to only reference valid explorations, '
                'but found an exploration with ID: %s (was it deleted?)' %
                collection_node.exploration_id)

    # Ensure no explorations are being added that are 'below' the public status
    # of this collection. If the collection is private, it can have both
    # private and public explorations. If it's public, it can only have public
    # explorations.
    # TODO(bhenning): Ensure the latter is enforced above when trying to
    # publish a collection.
    if rights_manager.is_collection_public(collection.id):
        validate_exps_in_collection_are_public(collection)

    # Collection model cannot be none as we are passing the collection as a
    # parameter and also this function is called by update_collection which only
    # works if the collection is put into the datastore.
    collection_model = collection_models.CollectionModel.get(collection.id,
                                                             strict=False)
    if collection.version > collection_model.version:
        raise Exception(
            'Unexpected error: trying to update version %s of collection '
            'from version %s. Please reload the page and try again.' %
            (collection_model.version, collection.version))
    elif collection.version < collection_model.version:
        raise Exception(
            'Trying to update version %s of collection from version %s, '
            'which is too old. Please reload the page and try again.' %
            (collection_model.version, collection.version))

    collection_model.category = collection.category
    collection_model.title = collection.title
    collection_model.objective = collection.objective
    collection_model.language_code = collection.language_code
    collection_model.tags = collection.tags
    collection_model.schema_version = collection.schema_version
    collection_model.collection_contents = {
        'nodes':
        [collection_node.to_dict() for collection_node in collection.nodes]
    }
    collection_model.node_count = len(collection_model.nodes)
    collection_model.commit(committer_id, commit_message, change_list)
    caching_services.delete_multi(caching_services.CACHE_NAMESPACE_COLLECTION,
                                  None, [collection.id])
    index_collections_given_ids([collection.id])

    collection.version += 1
예제 #2
0
    def get(self):
        """Handles GET requests."""

        def _round_average_ratings(rating):
            """Returns the rounded average rating to display on the creator
            dashboard.

            Args:
                rating: float. The rating of the lesson.

            Returns:
                float. The rounded average value of rating.
            """
            return round(rating, feconf.AVERAGE_RATINGS_DASHBOARD_PRECISION)

        # We need to do the filtering because some activities that were
        # originally subscribed to may have been deleted since.
        subscribed_exploration_summaries = [
            summary for summary in
            exp_fetchers.get_exploration_summaries_matching_ids(
                subscription_services.get_exploration_ids_subscribed_to(
                    self.user_id))
            if summary is not None]
        subscribed_collection_summaries = [
            summary for summary in
            collection_services.get_collection_summaries_matching_ids(
                subscription_services.get_collection_ids_subscribed_to(
                    self.user_id))
            if summary is not None]

        exploration_ids_subscribed_to = [
            summary.id for summary in subscribed_exploration_summaries]

        exp_summary_dicts = summary_services.get_displayable_exp_summary_dicts(
            subscribed_exploration_summaries)
        collection_summary_dicts = []

        feedback_thread_analytics = (
            feedback_services.get_thread_analytics_multi(
                exploration_ids_subscribed_to))

        # TODO(bhenning): Update this to use unresolved answers from
        # stats_services once the training interface is enabled and it's cheaper
        # to retrieve top answers from stats_services.
        for ind, exploration in enumerate(exp_summary_dicts):
            exploration.update(feedback_thread_analytics[ind].to_dict())

        exp_summary_dicts = sorted(
            exp_summary_dicts,
            key=lambda x: (x['num_open_threads'], x['last_updated_msec']),
            reverse=True)

        if constants.ENABLE_NEW_STRUCTURE_PLAYERS:
            topic_summaries = topic_services.get_all_topic_summaries()
            topic_summary_dicts = [
                summary.to_dict() for summary in topic_summaries]

        if role_services.ACTION_CREATE_COLLECTION in self.user.actions:
            for collection_summary in subscribed_collection_summaries:
                # TODO(sll): Reuse _get_displayable_collection_summary_dicts()
                # in summary_services, instead of replicating it like this.
                collection_summary_dicts.append({
                    'id': collection_summary.id,
                    'title': collection_summary.title,
                    'category': collection_summary.category,
                    'objective': collection_summary.objective,
                    'language_code': collection_summary.language_code,
                    'last_updated': utils.get_time_in_millisecs(
                        collection_summary.collection_model_last_updated),
                    'created_on': utils.get_time_in_millisecs(
                        collection_summary.collection_model_created_on),
                    'status': collection_summary.status,
                    'node_count': collection_summary.node_count,
                    'community_owned': collection_summary.community_owned,
                    'thumbnail_icon_url': (
                        utils.get_thumbnail_icon_url_for_category(
                            collection_summary.category)),
                    'thumbnail_bg_color': utils.get_hex_color_for_category(
                        collection_summary.category),
                })

        dashboard_stats = (
            user_jobs_continuous.UserStatsAggregator.get_dashboard_stats(
                self.user_id))
        dashboard_stats.update({
            'total_open_feedback': feedback_services.get_total_open_threads(
                feedback_thread_analytics)
        })
        if dashboard_stats and dashboard_stats.get('average_ratings'):
            dashboard_stats['average_ratings'] = (
                _round_average_ratings(dashboard_stats['average_ratings']))

        last_week_stats = (
            user_services.get_last_week_dashboard_stats(self.user_id))

        if last_week_stats and len(last_week_stats.keys()) != 1:
            logging.error(
                '\'last_week_stats\' should contain only one key-value pair'
                ' denoting last week dashboard stats of the user keyed by a'
                ' datetime string.')
            last_week_stats = None

        if last_week_stats:
            # 'last_week_stats' is a dict with only one key-value pair denoting
            # last week dashboard stats of the user keyed by a datetime string.
            datetime_of_stats = last_week_stats.keys()[0]
            last_week_stats_average_ratings = (
                last_week_stats.values()[0].get('average_ratings'))
            if last_week_stats_average_ratings:
                last_week_stats[datetime_of_stats]['average_ratings'] = (
                    _round_average_ratings(last_week_stats_average_ratings))

        subscriber_ids = subscription_services.get_all_subscribers_of_creator(
            self.user_id)
        subscribers_settings = user_services.get_users_settings(subscriber_ids)
        subscribers_list = []
        for index, subscriber_settings in enumerate(subscribers_settings):
            subscriber_summary = {
                'subscriber_picture_data_url': (
                    subscriber_settings.profile_picture_data_url),
                'subscriber_username': subscriber_settings.username,
                'subscriber_impact': (
                    user_services.get_user_impact_score(subscriber_ids[index]))
            }

            subscribers_list.append(subscriber_summary)

        user_settings = user_services.get_user_settings(
            self.user_id, strict=False)
        creator_dashboard_display_pref = (
            user_settings.creator_dashboard_display_pref)

        suggestions_created_by_user = suggestion_services.query_suggestions(
            [('author_id', self.user_id),
             (
                 'suggestion_type',
                 suggestion_models.SUGGESTION_TYPE_EDIT_STATE_CONTENT)])
        suggestions_which_can_be_reviewed = (
            suggestion_services
            .get_all_suggestions_that_can_be_reviewed_by_user(self.user_id))

        for s in suggestions_created_by_user:
            s.populate_old_value_of_change()

        for s in suggestions_which_can_be_reviewed:
            s.populate_old_value_of_change()

        suggestion_dicts_created_by_user = (
            [s.to_dict() for s in suggestions_created_by_user])
        suggestion_dicts_which_can_be_reviewed = (
            [s.to_dict() for s in suggestions_which_can_be_reviewed])

        ids_of_suggestions_created_by_user = (
            [s['suggestion_id'] for s in suggestion_dicts_created_by_user])
        ids_of_suggestions_which_can_be_reviewed = (
            [s['suggestion_id']
             for s in suggestion_dicts_which_can_be_reviewed])

        threads_linked_to_suggestions_by_user = (
            [t.to_dict() for t in feedback_services.get_multiple_threads(
                ids_of_suggestions_created_by_user)])
        threads_linked_to_suggestions_which_can_be_reviewed = (
            [t.to_dict() for t in feedback_services.get_multiple_threads(
                ids_of_suggestions_which_can_be_reviewed)])

        self.values.update({
            'explorations_list': exp_summary_dicts,
            'collections_list': collection_summary_dicts,
            'dashboard_stats': dashboard_stats,
            'last_week_stats': last_week_stats,
            'subscribers_list': subscribers_list,
            'display_preference': creator_dashboard_display_pref,
            'threads_for_created_suggestions_list': (
                threads_linked_to_suggestions_by_user),
            'threads_for_suggestions_to_review_list': (
                threads_linked_to_suggestions_which_can_be_reviewed),
            'created_suggestions_list': suggestion_dicts_created_by_user,
            'suggestions_to_review_list': suggestion_dicts_which_can_be_reviewed
        })
        if constants.ENABLE_NEW_STRUCTURE_PLAYERS:
            self.values.update({
                'topic_summary_dicts': topic_summary_dicts
            })
        self.render_json(self.values)
예제 #3
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.

    Returns:
        list(dict). A list of groups for the library index page. Each group is
        represented by a dict with the following keys and values:
            - activity_summary_dicts: list(dict). A list of dicts representing
                activity summaries.
            - categories: list(str). The list of group categories.
            - header_i18n_id: str. The i18n id for the header of the category.
            - has_full_results_page: bool. Whether the group header links to
                a "full results" page. This is always True for the
                "exploration category" groups.
            - full_results_url: str. The URL to the corresponding "full results"
                page.
    """
    # Collect all collection ids so that the summary details can be retrieved
    # with a single get_multi() call.
    all_collection_ids = []
    header_id_to_collection_ids = {}
    for group in _LIBRARY_INDEX_GROUPS:
        collection_ids = search_services.search_collections(
            '', group['search_categories'], language_codes, 8)[0]
        header_id_to_collection_ids[group['header_i18n_id']] = collection_ids
        all_collection_ids += collection_ids

    collection_summaries = [
        summary for summary in collection_services.
        get_collection_summaries_matching_ids(all_collection_ids)
        if summary is not None
    ]
    collection_summary_dicts = {
        summary_dict['id']: summary_dict
        for summary_dict in _get_displayable_collection_summary_dicts(
            collection_summaries)
    }

    # Collect all exp ids so that the summary details can be retrieved with a
    # single get_multi() call.
    all_exp_ids = []
    header_to_exp_ids = {}
    for group in _LIBRARY_INDEX_GROUPS:
        exp_ids = search_services.search_explorations(
            '', group['search_categories'], language_codes, 8)[0]
        header_to_exp_ids[group['header_i18n_id']] = exp_ids
        all_exp_ids += exp_ids

    exp_summaries = [
        summary for summary in
        exp_fetchers.get_exploration_summaries_matching_ids(all_exp_ids)
        if summary is not None
    ]

    exp_summary_dicts = {
        summary_dict['id']: summary_dict
        for summary_dict in get_displayable_exp_summary_dicts(exp_summaries)
    }

    results = []
    for group in _LIBRARY_INDEX_GROUPS:
        summary_dicts = []
        collection_ids_to_display = (
            header_id_to_collection_ids[group['header_i18n_id']])
        summary_dicts = [
            collection_summary_dicts[collection_id]
            for collection_id in collection_ids_to_display
            if collection_id in collection_summary_dicts
        ]

        exp_ids_to_display = header_to_exp_ids[group['header_i18n_id']]
        summary_dicts += [
            exp_summary_dicts[exp_id] for exp_id in exp_ids_to_display
            if exp_id in exp_summary_dicts
        ]

        if not summary_dicts:
            continue

        results.append({
            'header_i18n_id': group['header_i18n_id'],
            'categories': group['search_categories'],
            'activity_summary_dicts': summary_dicts,
            'has_full_results_page': True,
            'full_results_url': None,
        })

    return results
예제 #4
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_fetchers.get_exploration_summaries_matching_ids(exp_ids))
    exp_summaries_dict = {
        exp_id: exp_summaries[ind]
        for (ind, exp_id) in enumerate(exp_ids)
    }
    for node in story.story_contents.nodes:
        if (node.exploration_id
                is not None) and (not exp_summaries_dict[node.exploration_id]):
            raise utils.ValidationError(
                'Expected story to only reference valid explorations, '
                'but found an exploration with ID: %s (was it deleted?)' %
                node.exploration_id)

    # Story model cannot be None as story is passed as parameter here and that
    # is only possible if a story model with that story id exists. Also this is
    # a private function and so it cannot be called independently with any
    # story object.
    story_model = story_models.StoryModel.get(story.id)
    if story.version > story_model.version:
        raise Exception(
            'Unexpected error: trying to update version %s of story '
            'from version %s. Please reload the page and try again.' %
            (story_model.version, story.version))
    elif story.version < story_model.version:
        raise Exception(
            'Trying to update version %s of story from version %s, '
            'which is too old. Please reload the page and try again.' %
            (story_model.version, story.version))

    topic = topic_fetchers.get_topic_by_id(story.corresponding_topic_id,
                                           strict=False)
    if topic is None:
        raise utils.ValidationError(
            'Expected story to only belong to a valid topic, but found an '
            'topic with ID: %s' % story.corresponding_topic_id)

    canonical_story_ids = topic.get_canonical_story_ids()
    additional_story_ids = topic.get_additional_story_ids()
    if story.id not in canonical_story_ids + additional_story_ids:
        raise Exception(
            'Expected story to belong to the topic %s, but it is '
            'neither a part of the canonical stories or the additional stories '
            'of the topic.' % story.corresponding_topic_id)

    story_model.description = story.description
    story_model.title = story.title
    story_model.notes = story.notes
    story_model.language_code = story.language_code
    story_model.story_contents_schema_version = (
        story.story_contents_schema_version)
    story_model.story_contents = story.story_contents.to_dict()
    story_model.corresponding_topic_id = story.corresponding_topic_id
    story_model.version = story.version
    change_dicts = [change.to_dict() for change in change_list]
    story_model.commit(committer_id, commit_message, change_dicts)
    memcache_services.delete(story_fetchers.get_story_memcache_key(story.id))
    story.version += 1