def test_get_search_rank(self):
        self.save_new_valid_exploration(self.EXP_ID, self.owner_id)
        exp_summary = exp_fetchers.get_exploration_summary_by_id(self.EXP_ID)

        base_search_rank = 20

        self.assertEqual(
            search_services.get_search_rank_from_exp_summary(exp_summary),
            base_search_rank)

        rights_manager.publish_exploration(self.owner, self.EXP_ID)
        self.assertEqual(
            search_services.get_search_rank_from_exp_summary(exp_summary),
            base_search_rank)

        rating_services.assign_rating_to_exploration(
            self.owner_id, self.EXP_ID, 5)
        exp_summary = exp_fetchers.get_exploration_summary_by_id(self.EXP_ID)
        self.assertEqual(
            search_services.get_search_rank_from_exp_summary(exp_summary),
            base_search_rank + 10)

        rating_services.assign_rating_to_exploration(
            self.user_id_admin, self.EXP_ID, 2)
        exp_summary = exp_fetchers.get_exploration_summary_by_id(self.EXP_ID)
        self.assertEqual(
            search_services.get_search_rank_from_exp_summary(exp_summary),
            base_search_rank + 8)
    def test_search_ranks_cannot_be_negative(self):
        self.save_new_valid_exploration(self.EXP_ID, self.owner_id)
        exp_summary = exp_fetchers.get_exploration_summary_by_id(self.EXP_ID)

        base_search_rank = 20

        self.assertEqual(
            search_services.get_search_rank_from_exp_summary(exp_summary),
            base_search_rank)

        # A user can (down-)rate an exploration at most once.
        for i in range(50):
            rating_services.assign_rating_to_exploration(
                'user_id_1', self.EXP_ID, 1)
        exp_summary = exp_fetchers.get_exploration_summary_by_id(self.EXP_ID)
        self.assertEqual(
            search_services.get_search_rank_from_exp_summary(exp_summary),
            base_search_rank - 5)

        for i in range(50):
            rating_services.assign_rating_to_exploration(
                'user_id_%s' % i, self.EXP_ID, 1)

        # The rank will be at least 0.
        exp_summary = exp_fetchers.get_exploration_summary_by_id(self.EXP_ID)
        self.assertEqual(search_services.get_search_rank_from_exp_summary(
            exp_summary), 0)
Example #3
0
 def test_get_exploration_summary_by_id(self) -> None:
     fake_eid = 'fake_eid'
     fake_exp = exp_fetchers.get_exploration_summary_by_id(fake_eid,
                                                           strict=False)
     self.assertIsNone(fake_exp)
     exp_summary = exp_fetchers.get_exploration_summary_by_id(self.EXP_1_ID)
     self.assertIsNotNone(exp_summary)
     self.assertEqual(exp_summary.id, self.EXP_1_ID)
Example #4
0
    def test_contributors_with_only_reverts_not_included(self):
        # Let USER A make three commits.
        exploration = self.save_new_valid_exploration(
            self.EXP_ID, self.user_a_id, title='Exploration Title 1')

        exp_services.update_exploration(self.user_a_id, self.EXP_ID, [
            exp_domain.ExplorationChange({
                'cmd': 'edit_exploration_property',
                'property_name': 'title',
                'new_value': 'New Exploration Title'
            })
        ], 'Changed title.')
        exp_services.update_exploration(self.user_a_id, self.EXP_ID, [
            exp_domain.ExplorationChange({
                'cmd': 'edit_exploration_property',
                'property_name': 'objective',
                'new_value': 'New Objective'
            })
        ], 'Changed Objective.')

        # Let the second user revert version 3 to version 2.
        exp_services.revert_exploration(self.user_b_id, self.EXP_ID, 3, 2)

        output = self._run_one_off_job()
        self.assertEqual([['SUCCESS', 1]], output)

        exploration_summary = exp_fetchers.get_exploration_summary_by_id(
            exploration.id)
        self.assertEqual([self.user_a_id], exploration_summary.contributor_ids)
        self.assertEqual({self.user_a_id: 2},
                         exploration_summary.contributors_summary)
Example #5
0
def assign_rating_to_exploration(user_id, exploration_id, new_rating):
    """Records the rating awarded by the user to the exploration in both the
    user-specific data and exploration summary.

    This function validates the exploration id but not the user id.

    Args:
        user_id: str. The id of the user assigning the rating.
        exploration_id: str. The id of the exploration that is
            assigned a rating.
        new_rating: int. Value of assigned rating, should be between
            1 and 5 inclusive.
    """

    if not isinstance(new_rating, int):
        raise ValueError(
            'Expected the rating to be an integer, received %s' % new_rating)

    if new_rating not in ALLOWED_RATINGS:
        raise ValueError('Expected a rating 1-5, received %s.' % new_rating)

    try:
        exp_fetchers.get_exploration_by_id(exploration_id)
    except:
        raise Exception('Invalid exploration id %s' % exploration_id)

    def _update_user_rating():
        """Updates the user rating of the exploration. Returns the old rating
        before updation.
        """
        exp_user_data_model = user_models.ExplorationUserDataModel.get(
            user_id, exploration_id)
        if exp_user_data_model:
            old_rating = exp_user_data_model.rating
        else:
            old_rating = None
            exp_user_data_model = user_models.ExplorationUserDataModel.create(
                user_id, exploration_id)
        exp_user_data_model.rating = new_rating
        exp_user_data_model.rated_on = datetime.datetime.utcnow()
        exp_user_data_model.put()
        return old_rating
    old_rating = transaction_services.run_in_transaction(_update_user_rating)

    exploration_summary = exp_fetchers.get_exploration_summary_by_id(
        exploration_id)
    if not exploration_summary.ratings:
        exploration_summary.ratings = feconf.get_empty_ratings()
    exploration_summary.ratings[str(new_rating)] += 1
    if old_rating:
        exploration_summary.ratings[str(old_rating)] -= 1

    event_services.RateExplorationEventHandler.record(
        exploration_id, user_id, new_rating, old_rating)

    exploration_summary.scaled_average_rating = (
        exp_services.get_scaled_average_rating(
            exploration_summary.ratings))

    exp_services.save_exploration_summary(exploration_summary)
Example #6
0
    def test_reverts_not_counted(self):
        # Let USER A make 3 non-revert commits.
        exploration = self.save_new_valid_exploration(
            self.EXP_ID, self.user_a_id, title='Exploration Title')
        exp_services.update_exploration(self.user_a_id, self.EXP_ID, [
            exp_domain.ExplorationChange({
                'cmd': 'edit_exploration_property',
                'property_name': 'title',
                'new_value': 'New Exploration Title'
            })
        ], 'Changed title.')
        exp_services.update_exploration(self.user_a_id, self.EXP_ID, [
            exp_domain.ExplorationChange({
                'cmd': 'edit_exploration_property',
                'property_name': 'objective',
                'new_value': 'New Objective'
            })
        ], 'Changed Objective.')

        # Let USER A revert version 3 to version 2.
        exp_services.revert_exploration(self.user_a_id, self.EXP_ID, 3, 2)

        output = self._run_one_off_job()
        self.assertEqual([['SUCCESS', 1]], output)

        # Check that USER A's number of contributions is equal to 2.
        exploration_summary = exp_fetchers.get_exploration_summary_by_id(
            exploration.id)
        self.assertEqual([self.user_a_id], exploration_summary.contributor_ids)
        self.assertEqual({self.user_a_id: 2},
                         exploration_summary.contributors_summary)
    def test_rating_assignation(self):
        """Check ratings are correctly assigned to an exploration."""

        exp_services.save_new_exploration(
            self.EXP_ID,
            exp_domain.Exploration.create_default_exploration(self.EXP_ID))

        self.assertEqual(
            rating_services.get_overall_ratings_for_exploration(self.EXP_ID),
            {'1': 0, '2': 0, '3': 0, '4': 0, '5': 0})

        exp_summary = exp_fetchers.get_exploration_summary_by_id(self.EXP_ID)
        self.assertEqual(
            exp_summary.scaled_average_rating, 0)

        self.assertEqual(
            rating_services.get_user_specific_rating_for_exploration(
                self.USER_ID_1, self.EXP_ID), None)

        rating_services.assign_rating_to_exploration(
            self.USER_ID_1, self.EXP_ID, 2)
        rating_services.assign_rating_to_exploration(
            self.USER_ID_2, self.EXP_ID, 4)
        rating_services.assign_rating_to_exploration(
            self.USER_ID_1, self.EXP_ID, 3)

        exp_summary = exp_fetchers.get_exploration_summary_by_id(self.EXP_ID)
        self.assertAlmostEqual(
            exp_summary.scaled_average_rating, 1.5667471839848, places=4)

        self.assertEqual(
            rating_services.get_user_specific_rating_for_exploration(
                self.USER_ID_1, self.EXP_ID), 3)
        self.assertEqual(
            rating_services.get_user_specific_rating_for_exploration(
                self.USER_ID_2, self.EXP_ID), 4)
        self.assertEqual(
            rating_services.get_overall_ratings_for_exploration(self.EXP_ID),
            {'1': 0, '2': 0, '3': 1, '4': 1, '5': 0})

        rating_services.assign_rating_to_exploration(
            self.USER_ID_1, self.EXP_ID, 4)

        self.assertEqual(
            rating_services.get_overall_ratings_for_exploration(self.EXP_ID),
            {'1': 0, '2': 0, '3': 0, '4': 2, '5': 0})
Example #8
0
    def map(item):
        if item.deleted:
            return

        summary = exp_fetchers.get_exploration_summary_by_id(item.id)
        summary.contributors_summary = (
            exp_services.compute_exploration_contributors_summary(item.id))
        exp_services.save_exploration_summary(summary)
        yield ('SUCCESS', item.id)
Example #9
0
def handle_exploration_start(exp_id):
    """Handles a user's start of an exploration.

    Args:
        exp_id: str. The exploration which has been started.
    """
    exp_summary = exp_fetchers.get_exploration_summary_by_id(exp_id)
    if exp_summary:
        for user_id in exp_summary.owner_ids:
            _increment_total_plays_count_transactional(user_id)
Example #10
0
def get_overall_ratings_for_exploration(exploration_id):
    """Fetches all ratings for an exploration.

    Args:
        exploration_id: str. The id of the exploration.

    Returns:
        dict. A dict whose keys are '1', '2', '3', '4', '5' and whose
        values are nonnegative integers representing the frequency counts
        of each rating.
    """
    exp_summary = exp_fetchers.get_exploration_summary_by_id(exploration_id)
    return exp_summary.ratings
Example #11
0
def handle_exploration_rating(exp_id, rating, old_rating):
    """Handles a new rating for an exploration.

    Args:
        exp_id: str. The exploration which has been rated.
        rating: int. The new rating of the exploration.
        old_rating: int. The old rating of the exploration before
            refreshing.
    """
    exp_summary = exp_fetchers.get_exploration_summary_by_id(exp_id)
    if exp_summary:
        for user_id in exp_summary.owner_ids:
            _refresh_average_ratings_transactional(user_id, rating, old_rating)
Example #12
0
    def test_contributors_for_valid_nonrevert_contribution(self):
        # Let USER A make three commits.
        exploration = self.save_new_valid_exploration(self.EXP_ID,
                                                      self.user_a_id)
        collection = self.save_new_valid_collection(self.COL_ID,
                                                    self.user_a_id)

        exp_services.update_exploration(self.user_a_id, self.EXP_ID, [
            exp_domain.ExplorationChange({
                'cmd': 'edit_exploration_property',
                'property_name': 'title',
                'new_value': 'New Exploration Title'
            })
        ], 'Changed title.')
        exp_services.update_exploration(self.user_a_id, self.EXP_ID, [
            exp_domain.ExplorationChange({
                'cmd': 'edit_exploration_property',
                'property_name': 'objective',
                'new_value': 'New Objective'
            })
        ], 'Changed Objective.')
        collection_services.update_collection(
            self.user_a_id, self.COL_ID, [{
                'cmd': 'edit_collection_property',
                'property_name': 'title',
                'new_value': 'New Exploration Title'
            }], 'Changed title.')
        collection_services.update_collection(
            self.user_a_id, self.COL_ID, [{
                'cmd': 'edit_collection_property',
                'property_name': 'objective',
                'new_value': 'New Objective'
            }], 'Changed Objective.')

        output = self._run_one_off_job()
        self.assertEqual([['SUCCESS', 3]], output)

        exploration_summary = exp_fetchers.get_exploration_summary_by_id(
            exploration.id)
        self.assertEqual([self.user_a_id], exploration_summary.contributor_ids)
        self.assertEqual({self.user_a_id: 3},
                         exploration_summary.contributors_summary)

        collection_summary = collection_services.get_collection_summary_by_id(
            collection.id)
        self.assertEqual([self.user_a_id], collection_summary.contributor_ids)
        self.assertEqual({self.user_a_id: 3},
                         collection_summary.contributors_summary)
Example #13
0
    def map(model):
        if model.deleted:
            return

        if isinstance(model, collection_models.CollectionModel):
            summary = collection_services.get_collection_summary_by_id(model.id)
            summary.contributors_summary = (
                collection_services.compute_collection_contributors_summary(
                    model.id))
            summary.contributor_ids = list(summary.contributors_summary)
            collection_services.save_collection_summary(summary)
        else:
            summary = exp_fetchers.get_exploration_summary_by_id(model.id)
            summary.contributors_summary = (
                exp_services.compute_exploration_contributors_summary(model.id))
            summary.contributor_ids = list(summary.contributors_summary)
            exp_services.save_exploration_summary(summary)
        yield ('SUCCESS', model.id)
    def test_nonhuman_committers_not_counted(self):
        # Create a commit with the system user id.
        exploration = self.save_new_valid_exploration(
            self.EXP_ID, feconf.SYSTEM_COMMITTER_ID, title='Original Title')
        collection = self.save_new_valid_collection(self.COL_ID, self.user_a_id)

        # Create commits with all the system user ids.
        for system_id in constants.SYSTEM_USER_IDS:
            exp_services.update_exploration(
                system_id, self.EXP_ID, [exp_domain.ExplorationChange({
                    'cmd': 'edit_exploration_property',
                    'property_name': 'title',
                    'new_value': 'Title changed by %s' % system_id
                })], 'Changed title.')
            collection_services.update_collection(
                system_id, self.COL_ID, [{
                    'cmd': 'edit_collection_property',
                    'property_name': 'title',
                    'new_value': 'New Exploration Title'
                }], 'Changed title.')

        output = self._run_one_off_job()
        self.assertEqual([['SUCCESS', 3]], output)

        # Check that no system id was added to the exploration's
        # contributor's summary.
        exploration_summary = exp_fetchers.get_exploration_summary_by_id(
            exploration.id)
        collection_summary = collection_services.get_collection_summary_by_id(
            collection.id)
        for system_id in constants.SYSTEM_USER_IDS:
            self.assertNotIn(
                system_id,
                exploration_summary.contributors_summary)
            self.assertNotIn(
                system_id,
                exploration_summary.contributor_ids)
            self.assertNotIn(
                system_id,
                collection_summary.contributors_summary)
            self.assertNotIn(
                system_id,
                collection_summary.contributor_ids)
Example #15
0
    def map(item):
        """Implements the map function (generator).
        Computes exploration data for every contributor and owner of the
        exploration.

        Args:
            item: ExpSummaryModel. An instance of ExpSummaryModel.

        Yields:
            This function may yield as many times as appropriate 2-tuples in the
            format (str, dict), where
            - str. The unique ID of the user.
            - dict: A dict that includes entries for all the explorations that
                this user contributes to or owns. Each entry has the following
                keys:
                - 'exploration_impact_score': float. The impact score of all the
                    explorations contributed to by the user.
                - 'total_plays_for_owned_exp': int. Total plays of all
                    explorations owned by the user.
                - 'average_rating_for_owned_exp': float. Average of average
                    ratings of all explorations owned by the user.
                - 'num_ratings_for_owned_exp': int. Total number of ratings of
                    all explorations owned by the user.
        """
        if item.deleted:
            return

        exponent = 2.0 / 3

        # This is set to False only when the exploration impact score is not
        # valid to be calculated.
        calculate_exploration_impact_score = True

        # Get average rating and value per user.
        total_rating = 0
        for ratings_value in item.ratings:
            total_rating += item.ratings[ratings_value] * int(ratings_value)
        sum_of_ratings = sum(item.ratings.itervalues())

        average_rating = ((total_rating /
                           sum_of_ratings) if sum_of_ratings else None)

        if average_rating is not None:
            value_per_user = average_rating - 2
            if value_per_user <= 0:
                calculate_exploration_impact_score = False
        else:
            calculate_exploration_impact_score = False

        exploration_stats = stats_services.get_exploration_stats(
            item.id, item.version)
        # For each state, find the number of first entries to the state.
        # This is considered to be approximately equal to the number of
        # users who answered the state because very few users enter a state
        # and leave without answering anything at all.
        answer_count = exploration_stats.get_sum_of_first_hit_counts()
        num_starts = exploration_stats.num_starts

        # Turn answer count into reach.
        reach = answer_count**exponent

        exploration_summary = exp_fetchers.get_exploration_summary_by_id(
            item.id)
        contributors = exploration_summary.contributors_summary
        total_commits = sum(contributors.itervalues())
        if total_commits == 0:
            calculate_exploration_impact_score = False

        mapped_owner_ids = []
        for contrib_id in contributors:
            exploration_data = {}

            # Set the value of exploration impact score only if it needs to be
            # calculated.
            if calculate_exploration_impact_score:
                # Find fractional contribution for each contributor.
                contribution = (contributors[contrib_id] /
                                float(total_commits))

                # Find score for this specific exploration.
                exploration_data.update({
                    'exploration_impact_score':
                    (value_per_user * reach * contribution)
                })

            # If the user is an owner for the exploration, then update dict with
            # 'average ratings' and 'total plays' as well.
            if contrib_id in exploration_summary.owner_ids:
                mapped_owner_ids.append(contrib_id)
                # Get num starts (total plays) for the exploration.
                exploration_data.update({
                    'total_plays_for_owned_exp':
                    num_starts,
                })
                # Update data with average rating only if it is not None.
                if average_rating is not None:
                    exploration_data.update({
                        'average_rating_for_owned_exp':
                        average_rating,
                        'num_ratings_for_owned_exp':
                        sum_of_ratings
                    })
            yield (contrib_id, exploration_data)

        for owner_id in exploration_summary.owner_ids:
            if owner_id not in mapped_owner_ids:
                mapped_owner_ids.append(owner_id)
                # Get num starts (total plays) for the exploration.
                exploration_data = {
                    'total_plays_for_owned_exp': num_starts,
                }
                # Update data with average rating only if it is not None.
                if average_rating is not None:
                    exploration_data.update({
                        'average_rating_for_owned_exp':
                        average_rating,
                        'num_ratings_for_owned_exp':
                        sum_of_ratings
                    })
                yield (owner_id, exploration_data)
Example #16
0
    def _handle_incoming_event(cls, active_realtime_layer, event_type, *args):
        """Records incoming events in the given realtime layer.

        Args:
            active_realtime_layer: int. The currently active realtime datastore
                layer.
            event_type: str. The event triggered by a student. For example, when
                a student starts an exploration, an event of type `start` is
                triggered and the total play count is incremented. If he/she
                rates an exploration, an event of type `rate` is triggered and
                average rating of the realtime model is refreshed.
            *args: list(*).
                If event_type is 'start', then this is a 1-element list with
                following entry:
                - str. The ID of the exploration currently being played.
                If event_type is 'rate_exploration', then this is a 3-element
                list with following entries:
                - str. The ID of the exploration currently being played.
                - float. The rating given by user to the exploration.
                - float. The old rating of the exploration, before it is
                    refreshed.
        """
        exp_id = args[0]

        def _refresh_average_ratings(user_id, rating, old_rating):
            """Refreshes the average ratings in the given realtime layer.

            Args:
                user_id: str. The id of the user.
                rating: int. The new rating of the exploration.
                old_rating: int. The old rating of the exploration before
                    refreshing.
            """
            realtime_class = cls._get_realtime_datastore_class()
            realtime_model_id = realtime_class.get_realtime_id(
                active_realtime_layer, user_id)

            model = realtime_class.get(realtime_model_id, strict=False)
            if model is None:
                realtime_class(id=realtime_model_id,
                               average_ratings=rating,
                               num_ratings=1,
                               realtime_layer=active_realtime_layer).put()
            else:
                num_ratings = model.num_ratings
                average_ratings = model.average_ratings
                num_ratings += 1
                if average_ratings is not None:
                    sum_of_ratings = (average_ratings * (num_ratings - 1) +
                                      rating)
                    if old_rating is not None:
                        sum_of_ratings -= old_rating
                        num_ratings -= 1
                    model.average_ratings = sum_of_ratings / (num_ratings *
                                                              1.0)
                else:
                    model.average_ratings = rating
                model.num_ratings = num_ratings
                model.put()

        def _increment_total_plays_count(user_id):
            """Increments the total plays count of the exploration in the
            realtime layer.

            Args:
                user_id: str. The id of the user.
            """
            realtime_class = cls._get_realtime_datastore_class()
            realtime_model_id = realtime_class.get_realtime_id(
                active_realtime_layer, user_id)

            model = realtime_class.get(realtime_model_id, strict=False)
            if model is None:
                realtime_class(id=realtime_model_id,
                               total_plays=1,
                               realtime_layer=active_realtime_layer).put()
            else:
                model.total_plays += 1
                model.put()

        exp_summary = exp_fetchers.get_exploration_summary_by_id(exp_id)
        if exp_summary:
            for user_id in exp_summary.owner_ids:
                if event_type == feconf.EVENT_TYPE_START_EXPLORATION:
                    transaction_services.run_in_transaction(
                        _increment_total_plays_count, user_id)

                elif event_type == feconf.EVENT_TYPE_RATE_EXPLORATION:
                    rating = args[2]
                    old_rating = args[3]
                    transaction_services.run_in_transaction(
                        _refresh_average_ratings, user_id, rating, old_rating)
Example #17
0
def assign_rating_to_exploration(user_id: str, exploration_id: str,
                                 new_rating: int) -> None:
    """Records the rating awarded by the user to the exploration in both the
    user-specific data and exploration summary.

    This function validates the exploration id but not the user id.

    Args:
        user_id: str. The id of the user assigning the rating.
        exploration_id: str. The id of the exploration that is
            assigned a rating.
        new_rating: int. Value of assigned rating, should be between
            1 and 5 inclusive.

    Raises:
        ValueError. The assigned rating is not of type int.
        ValueError. The assigned rating is lower than 1 or higher than 5.
        ValueError. The exploration does not exist.
    """

    if not isinstance(new_rating, int):
        raise ValueError('Expected the rating to be an integer, received %s' %
                         new_rating)

    if new_rating not in ALLOWED_RATINGS:
        raise ValueError('Expected a rating 1-5, received %s.' % new_rating)

    exploration = exp_fetchers.get_exploration_by_id(  # type: ignore[no-untyped-call]
        exploration_id, strict=False)
    if exploration is None:
        raise ValueError('Invalid exploration id %s' % exploration_id)

    @transaction_services.run_in_transaction_wrapper
    def _update_user_rating_transactional() -> Optional[int]:
        """Updates the user rating of the exploration. Returns the old rating
        before updation.
        """
        exp_user_data_model = user_models.ExplorationUserDataModel.get(
            user_id, exploration_id)
        if exp_user_data_model:
            old_rating: Optional[int] = exp_user_data_model.rating
        else:
            old_rating = None
            exp_user_data_model = user_models.ExplorationUserDataModel.create(
                user_id, exploration_id)
        exp_user_data_model.rating = new_rating
        exp_user_data_model.rated_on = datetime.datetime.utcnow()
        exp_user_data_model.update_timestamps()
        exp_user_data_model.put()
        return old_rating

    old_rating = _update_user_rating_transactional()

    exploration_summary = exp_fetchers.get_exploration_summary_by_id(
        exploration_id)
    if not exploration_summary.ratings:
        exploration_summary.ratings = feconf.get_empty_ratings()
    exploration_summary.ratings[str(new_rating)] += 1
    if old_rating:
        exploration_summary.ratings[str(old_rating)] -= 1

    event_services.RateExplorationEventHandler.record(  # type: ignore[no-untyped-call]
        exploration_id, user_id, new_rating, old_rating)

    exploration_summary.scaled_average_rating = (
        exp_services.
        get_scaled_average_rating(  # type: ignore[no-untyped-call]
            exploration_summary.ratings))

    exp_services.save_exploration_summary(
        exploration_summary)  # type: ignore[no-untyped-call]