Repository: oppia/oppia

Issue:
[Contributor Dashboard] Mark all translation suggestions corresponding to a given exploration as rejected whenever an exploration is deleted or removed from the story.

[Contributor Dashboard] Mark all translation suggestions corresponding to a given exploration as rejected whenever an exploration is deleted or removed from the story.



Discussion:

Resolving patch:
diff --git a/core/domain/story_services.py b/core/domain/story_services.py
--- a/core/domain/story_services.py
+++ b/core/domain/story_services.py
@@ -456,6 +456,8 @@ def update_story(
     _save_story(committer_id, new_story, commit_message, change_list)
     create_story_summary(new_story.id)
     opportunity_services.update_exploration_opportunities(old_story, new_story)
+    suggestion_services.auto_reject_translation_suggestions_for_exp_ids(
+        exp_ids_removed_from_story)
 
     exploration_context_models_to_be_deleted = (
         exp_models.ExplorationContextModel.get_multi(
diff --git a/core/domain/suggestion_services.py b/core/domain/suggestion_services.py
--- a/core/domain/suggestion_services.py
+++ b/core/domain/suggestion_services.py
@@ -161,8 +161,9 @@ def get_translation_suggestions_with_exp_ids(exp_ids):
 
     Returns:
         list(Suggestion). A list of translation suggestions that
-        correspond to the given exploration ids, up to a maximum of
-        feconf.DEFAULT_QUERY_LIMIT suggestions.
+        correspond to the given exploration ids. Note: it is not
+        guaranteed that the suggestions returned are ordered by the
+        exploration ids in exp_ids.
     """
     if len(exp_ids) == 0:
         return []
diff --git a/core/storage/suggestion/gae_models.py b/core/storage/suggestion/gae_models.py
--- a/core/storage/suggestion/gae_models.py
+++ b/core/storage/suggestion/gae_models.py
@@ -123,7 +123,7 @@
 class GeneralSuggestionModel(base_models.BaseModel):
     """Model to store suggestions made by Oppia users.
 
-    The ID of the suggestions are created is the same as the ID of the thread
+    The ID of the suggestions created is the same as the ID of the thread
     linked to the suggestion.
     """
 
@@ -251,16 +251,23 @@ def get_translation_suggestions_with_exp_ids(cls, exp_ids):
 
         Returns:
             list(SuggestionModel). A list of translation suggestions that
-            correspond to the given exploration ids, up to a maximum of
-            feconf.DEFAULT_QUERY_LIMIT suggestions.
+            correspond to the given exploration ids. Note: it is not
+            guaranteed that the suggestions returned are ordered by the
+            exploration ids in exp_ids.
         """
-        return (
+        query = (
             cls.get_all()
+            .order(cls.key)
             .filter(cls.suggestion_type == SUGGESTION_TYPE_TRANSLATE_CONTENT)
             .filter(cls.target_id.IN(exp_ids))
-            .fetch(feconf.DEFAULT_QUERY_LIMIT)
         )
-
+        suggestion_models = []
+        cursor, more = (None, True)
+        while more:
+            results, cursor, more = query.fetch_page(
+                feconf.DEFAULT_QUERY_LIMIT, start_cursor=cursor)
+            suggestion_models.extend(results)
+        return suggestion_models
 
     @classmethod
     def get_all_stale_suggestions(cls):


Test patch:
diff --git a/core/domain/suggestion_services_test.py b/core/domain/suggestion_services_test.py
--- a/core/domain/suggestion_services_test.py
+++ b/core/domain/suggestion_services_test.py
@@ -1249,7 +1249,44 @@ def test_delete_topic_rejects_translation_suggestion(self):
 
         topic_services.delete_topic(self.author_id, self.TOPIC_ID)
 
-        # Suggestion should be rejected after corresponding topic is deleted.
+        # Suggestion should be rejected after the topic is deleted.
+        suggestions = suggestion_services.query_suggestions(
+            [('author_id', self.author_id), ('target_id', self.EXP_ID)])
+        self.assertEqual(len(suggestions), 1)
+        self.assertEqual(
+            suggestions[0].status, suggestion_models.STATUS_REJECTED)
+
+    def test_delete_story_rejects_translation_suggestion(self):
+        self.create_translation_suggestion_associated_with_exp(
+            self.EXP_ID, self.author_id)
+        self.assert_created_suggestion_is_valid(self.EXP_ID, self.author_id)
+
+        story_services.delete_story(self.author_id, self.STORY_ID)
+
+        # Suggestion should be rejected after the story is deleted.
+        suggestions = suggestion_services.query_suggestions(
+            [('author_id', self.author_id), ('target_id', self.EXP_ID)])
+        self.assertEqual(len(suggestions), 1)
+        self.assertEqual(
+            suggestions[0].status, suggestion_models.STATUS_REJECTED)
+
+    def test_remove_exp_from_story_rejects_translation_suggestion(self):
+        self.create_translation_suggestion_associated_with_exp(
+            self.EXP_ID, self.author_id)
+        self.assert_created_suggestion_is_valid(self.EXP_ID, self.author_id)
+
+        # Removes the exploration from the story.
+        story_services.update_story(
+            self.owner_id, self.STORY_ID, [story_domain.StoryChange({
+                'cmd': 'update_story_node_property',
+                'property_name': 'exploration_id',
+                'node_id': 'node_1',
+                'old_value': self.EXP_ID,
+                'new_value': None
+            })], 'Removed exploration.')
+
+        # Suggestion should be rejected after exploration is removed from the
+        # story.
         suggestions = suggestion_services.query_suggestions(
             [('author_id', self.author_id), ('target_id', self.EXP_ID)])
         self.assertEqual(len(suggestions), 1)
diff --git a/core/storage/suggestion/gae_models_test.py b/core/storage/suggestion/gae_models_test.py
--- a/core/storage/suggestion/gae_models_test.py
+++ b/core/storage/suggestion/gae_models_test.py
@@ -21,6 +21,7 @@
 
 from core.platform import models
 from core.tests import test_utils
+import feconf
 
 (base_models, suggestion_models, user_models) = models.Registry.import_models(
     [models.NAMES.base_model, models.NAMES.suggestion, models.NAMES.user])
@@ -343,6 +344,35 @@ def test_get_translation_suggestions_with_exp_ids_with_invalid_exp(
             .get_translation_suggestions_with_exp_ids(
                 ['invalid_exp'])), 0)
 
+    def test_get_translation_suggestions_with_exp_ids_past_default_query_limit(
+            self):
+        suggestion_models.GeneralSuggestionModel.create(
+            suggestion_models.SUGGESTION_TYPE_TRANSLATE_CONTENT,
+            suggestion_models.TARGET_TYPE_EXPLORATION,
+            'exp4', self.target_version_at_submission,
+            suggestion_models.STATUS_IN_REVIEW, 'author_3',
+            'reviewer_2', self.change_cmd, self.score_category,
+            'exploration.exp1.thread_9')
+        suggestion_models.GeneralSuggestionModel.create(
+            suggestion_models.SUGGESTION_TYPE_TRANSLATE_CONTENT,
+            suggestion_models.TARGET_TYPE_EXPLORATION,
+            'exp5', self.target_version_at_submission,
+            suggestion_models.STATUS_IN_REVIEW, 'author_3',
+            'reviewer_2', self.change_cmd, self.score_category,
+            'exploration.exp1.thread_10')
+
+        with self.swap(feconf, 'DEFAULT_QUERY_LIMIT', 1):
+            suggestion_model_results = (
+                suggestion_models
+                .GeneralSuggestionModel
+                .get_translation_suggestions_with_exp_ids(
+                    ['exp4', 'exp5'])
+            )
+
+        # Assert that there are two translation suggestions with the given
+        # exploration ids found. There should be two fetch_page calls.
+        self.assertEqual(len(suggestion_model_results), 2)
+
     def test_get_all_stale_suggestions(self):
         with self.swap(
             suggestion_models, 'THRESHOLD_TIME_BEFORE_ACCEPT_IN_MSECS', 0):