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)
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)
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)
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)
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})
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)
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)
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
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)
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)
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)
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)
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)
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]