Repository: oppia/oppia

Issue:
Add language filtering to /getreviewableopportunitieshandler
<!--
  - Thanks for taking the time to propose a new feature for the Oppia project.
  - Before filing a new issue, please do a quick search to check that it hasn't
  - already been filed on the [issue tracker](https://github.com/oppia/oppia/issues)._
  -->
**Is your feature request related to a problem? Please describe.**
`/getreviewableopportunitieshandler` needs to support a `language` param that will filter opportunities by language. This is needed to filter the languages in https://github.com/oppia/oppia/pull/16157.
**Describe the solution you'd like**
Add `language` param to `/getreviewableopportunitieshandler`.
**Describe alternatives you've considered**
N/A
**Additional context**
This is needed to complete the https://github.com/oppia/oppia/pull/16157 PR, which in turns solves the https://github.com/oppia/oppia/issues/15493 issue.
Add language filtering to /getreviewableopportunitieshandler
<!--
  - Thanks for taking the time to propose a new feature for the Oppia project.
  - Before filing a new issue, please do a quick search to check that it hasn't
  - already been filed on the [issue tracker](https://github.com/oppia/oppia/issues)._
  -->
**Is your feature request related to a problem? Please describe.**
`/getreviewableopportunitieshandler` needs to support a `language` param that will filter opportunities by language. This is needed to filter the languages in https://github.com/oppia/oppia/pull/16157.
**Describe the solution you'd like**
Add `language` param to `/getreviewableopportunitieshandler`.
**Describe alternatives you've considered**
N/A
**Additional context**
This is needed to complete the https://github.com/oppia/oppia/pull/16157 PR, which in turns solves the https://github.com/oppia/oppia/issues/15493 issue.


Discussion:

Resolving patch:
diff --git a/core/controllers/contributor_dashboard.py b/core/controllers/contributor_dashboard.py
--- a/core/controllers/contributor_dashboard.py
+++ b/core/controllers/contributor_dashboard.py
@@ -283,6 +283,7 @@ class ReviewableOpportunitiesHandlerNormalizedRequestDict(TypedDict):
     """
 
     topic_name: Optional[str]
+    language_code: str
 
 
 class ReviewableOpportunitiesHandler(
@@ -301,6 +302,12 @@ class ReviewableOpportunitiesHandler(
                     'type': 'basestring'
                 },
                 'default_value': None
+            },
+            'language_code': {
+                'schema': {
+                    'type': 'basestring'
+                },
+                'default_value': None
             }
         }
     }
@@ -310,12 +317,13 @@ def get(self) -> None:
         """Handles GET requests."""
         assert self.normalized_request is not None
         topic_name = self.normalized_request.get('topic_name')
+        language = self.normalized_request.get('language_code')
         opportunity_dicts: List[
             opportunity_domain.PartialExplorationOpportunitySummaryDict
         ] = []
         if self.user_id:
             for opp in self._get_reviewable_exploration_opportunity_summaries(
-                self.user_id, topic_name
+                self.user_id, topic_name, language
             ):
                 if opp is not None:
                     opportunity_dicts.append(opp.to_dict())
@@ -325,7 +333,7 @@ def get(self) -> None:
         self.render_json(self.values)
 
     def _get_reviewable_exploration_opportunity_summaries(
-        self, user_id: str, topic_name: Optional[str]
+        self, user_id: str, topic_name: Optional[str], language: Optional[str]
     ) -> List[Optional[opportunity_domain.ExplorationOpportunitySummary]]:
         """Returns exploration opportunity summaries that have translation
         suggestions that are reviewable by the supplied user. The result is
@@ -337,6 +345,9 @@ def _get_reviewable_exploration_opportunity_summaries(
             topic_name: str or None. A topic name for which to filter the
                 exploration opportunity summaries. If 'All' is supplied, all
                 available exploration opportunity summaries will be returned.
+            language: str. ISO 639-1 language code for which to filter the
+                exploration opportunity summaries. If it is None, all
+                available exploration opportunity summaries will be returned.
 
         Returns:
             list(ExplorationOpportunitySummary). A list of the matching
@@ -379,7 +390,7 @@ def _get_reviewable_exploration_opportunity_summaries(
         in_review_suggestions, _ = (
             suggestion_services
             .get_reviewable_translation_suggestions_by_offset(
-                user_id, topic_exp_ids, None, 0))
+                user_id, topic_exp_ids, None, 0, language))
         # Filter out suggestions that should not be shown to the user.
         # This is defined as a set as we only care about the unique IDs.
         in_review_suggestion_target_ids = {
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
@@ -982,7 +982,8 @@ def get_reviewable_translation_suggestions_by_offset(
     user_id: str,
     opportunity_summary_exp_ids: Optional[List[str]],
     limit: Optional[int],
-    offset: int
+    offset: int,
+    language: Optional[str] = None
 ) -> Tuple[List[suggestion_registry.SuggestionTranslateContent], int]:
     """Returns a list of translation suggestions matching the
      passed opportunity IDs which the user can review.
@@ -1000,6 +1001,8 @@ def get_reviewable_translation_suggestions_by_offset(
             all available results are returned.
         offset: int. The number of results to skip from the beginning of all
             results matching the query.
+        language: str. ISO 639-1 language code for which to filter. If it is
+            None, all available languages will be returned.
 
     Returns:
         Tuple of (results, next_offset). Where:
@@ -1012,6 +1015,11 @@ def get_reviewable_translation_suggestions_by_offset(
         user_id)
     language_codes = (
         contribution_rights.can_review_translation_for_language_codes)
+
+    # No language means all languages.
+    if language is not None:
+        language_codes = [language] if language in language_codes else []
+
     # The user cannot review any translations, so return early.
     if len(language_codes) == 0:
         return [], offset


Test patch:
diff --git a/core/controllers/contributor_dashboard_test.py b/core/controllers/contributor_dashboard_test.py
--- a/core/controllers/contributor_dashboard_test.py
+++ b/core/controllers/contributor_dashboard_test.py
@@ -81,6 +81,8 @@ def setUp(self) -> None:
         self.set_curriculum_admins([self.CURRICULUM_ADMIN_USERNAME])
         user_services.allow_user_to_review_translation_in_language(
             self.admin_id, 'hi')
+        user_services.allow_user_to_review_translation_in_language(
+            self.admin_id, 'es')
 
         explorations = [self.save_new_valid_exploration(
             '%s' % i,
@@ -446,6 +448,65 @@ def test_get_reviewable_translation_opportunities_returns_in_review_suggestions(
         self.assertEqual(
             response['opportunities'], [self.expected_opportunity_dict_1])
 
+    def test_get_reviewable_translation_opportunities_filtering_language( # pylint: disable=line-too-long
+        self
+    ) -> None:
+        # Create a translation suggestion in Hindi.
+        change_dict = {
+            'cmd': 'add_translation',
+            'content_id': 'content',
+            'language_code': 'hi',
+            'content_html': '',
+            'state_name': 'Introduction',
+            'translation_html': '<p>Translation for content.</p>'
+        }
+        suggestion_services.create_suggestion(
+            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
+            feconf.ENTITY_TYPE_EXPLORATION,
+            '0', 1, self.owner_id, change_dict, 'description')
+
+        # Create a translation suggestion in Spanish.
+        change_dict = {
+            'cmd': 'add_translation',
+            'content_id': 'content',
+            'language_code': 'es',
+            'content_html': '',
+            'state_name': 'Introduction',
+            'translation_html': '<p>Translation for content 2.</p>'
+        }
+        suggestion_services.create_suggestion(
+            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
+            feconf.ENTITY_TYPE_EXPLORATION,
+            '1', 1, self.owner_id, change_dict, 'description 2')
+        self.login(self.CURRICULUM_ADMIN_EMAIL)
+
+        response = self.get_json(
+            '%s' % feconf.REVIEWABLE_OPPORTUNITIES_URL,
+            params={'language_code': 'es'})
+        # Should only return opportunities in Spanish.
+        self.assertEqual(
+            response['opportunities'], [self.expected_opportunity_dict_2])
+
+        response = self.get_json(
+            '%s' % feconf.REVIEWABLE_OPPORTUNITIES_URL,
+            params={'language_code': 'hi'})
+        # Should only return opportunities in Hindi.
+        self.assertEqual(
+            response['opportunities'], [self.expected_opportunity_dict_1])
+
+        response = self.get_json(
+            '%s' % feconf.REVIEWABLE_OPPORTUNITIES_URL,
+            params={'language_code': 'pt'})
+        # Should be empty.
+        self.assertEqual(
+            response['opportunities'], [])
+
+        response = self.get_json(
+            '%s' % feconf.REVIEWABLE_OPPORTUNITIES_URL)
+        # Should return all opportunities.
+        self.assertEqual(
+            response['opportunities'], [self.expected_opportunity_dict_1, self.expected_opportunity_dict_2])
+
     def test_raises_error_if_story_contain_none_exploration_id(self) -> None:
         # Create a new exploration and linked story.
         continue_state_name = 'continue state'
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
@@ -1752,6 +1752,54 @@ def test_get_reviewable_translation_suggestions_with_no_reviewable_languages( #
         self.assertEqual(len(suggestions), 0)
         self.assertEqual(offset, 0)
 
+    def test_get_reviewable_translation_suggestions_with_language_filter(
+        self
+    ) -> None:
+        # Add a few translation suggestions in different languages.
+        self._create_translation_suggestion_with_language_code('hi')
+        self._create_translation_suggestion_with_language_code('hi')
+        self._create_translation_suggestion_with_language_code('pt')
+        self._create_translation_suggestion_with_language_code('bn')
+        self._create_translation_suggestion_with_language_code('bn')
+        # Provide the user permission to review suggestions in particular
+        # languages.
+        user_services.allow_user_to_review_translation_in_language(
+            self.reviewer_id_1, 'hi')
+        user_services.allow_user_to_review_translation_in_language(
+            self.reviewer_id_1, 'pt')
+
+        # Get reviewable translation suggestions in Hindi.
+        language_to_filter = 'hi'
+        suggestions, _ = (
+            suggestion_services.
+            get_reviewable_translation_suggestions_by_offset(
+                self.reviewer_id_1, self.opportunity_summary_ids,
+                constants.OPPORTUNITIES_PAGE_SIZE, 0, language_to_filter))
+
+        # Expect that the results correspond to translation suggestions that the
+        # user has rights to review.
+        self.assertEqual(len(suggestions), 2)
+        self.assertEqual(suggestions[0].change.language_code, 'hi')
+        self.assertEqual(suggestions[1].change.language_code, 'hi')
+
+        # Get reviewable translation suggestions in Spanish (there are none).
+        language_to_filter = 'es'
+        suggestions, _ = (
+            suggestion_services.
+            get_reviewable_translation_suggestions_by_offset(
+                self.reviewer_id_1, self.opportunity_summary_ids,
+                constants.OPPORTUNITIES_PAGE_SIZE, 0, language_to_filter))
+
+        # Expect that the results correspond to translation suggestions that the
+        # user has rights to review.
+        self.assertEqual(len(suggestions), 0)
+        actual_language_code_list = [
+            suggestion.change.language_code
+            for suggestion in suggestions
+        ]
+        expected_language_code_list: List[str] = []
+        self.assertEqual(actual_language_code_list, expected_language_code_list)
+
     def test_get_reviewable_question_suggestions(self) -> None:
         # Add a few translation suggestions in different languages.
         self._create_translation_suggestion_with_language_code('hi')