Repository: oppia/oppia

Issue:
[CD] Update skill opportunity progress percentage denominator to 10
This was a feature request by question coordinators to make progress easier to track, as currrently the target percentage per lesson is 20%. 

Changes should occur in core/templates/pages/contributor-dashboard-page/question-opportunities/question-opportunities.component.ts.


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
@@ -99,7 +99,7 @@ def get(self, opportunity_type):
                     search_cursor))
 
         elif opportunity_type == constants.OPPORTUNITY_TYPE_TRANSLATION:
-            topic_name = self.normalized_payload.get('topic_name')
+            topic_name = self.normalized_request.get('topic_name')
             if language_code is None:
                 raise self.InvalidInputException
             opportunities, next_cursor, more = (


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
@@ -73,7 +73,7 @@ def setUp(self):
             category='category%d' % i,
             end_state_name='End State',
             correctness_feedback_enabled=True
-        ) for i in range(2)]
+        ) for i in range(3)]
 
         for exp in explorations:
             self.publish_exploration(self.owner_id, exp.id)
@@ -85,6 +85,20 @@ def setUp(self):
         self.skill_id_1 = 'skill_id_1'
         self._publish_valid_topic(topic, [self.skill_id_0, self.skill_id_1])
 
+        self._create_story_for_translation_opportunity(
+            'story_id_0', self.topic_id, '0')
+        self._create_story_for_translation_opportunity(
+            'story_id_1', self.topic_id, '1')
+
+        self.topic_id_1 = '1'
+        topic = topic_domain.Topic.create_default_topic(
+            self.topic_id_1, 'topic1', 'url-fragment', 'description')
+        self.skill_id_2 = 'skill_id_2'
+        self._publish_valid_topic(topic, [self.skill_id_2])
+
+        self._create_story_for_translation_opportunity(
+            'story_id_2', self.topic_id_1, '2')
+
         # Add skill opportunity topic to a classroom.
         config_services.set_property(
             self.admin_id, 'classroom_pages_data', [{
@@ -107,54 +121,37 @@ def setUp(self):
             'question_count': 0,
             'topic_name': 'topic'
         }
+        self.expected_skill_opportunity_dict_2 = {
+            'id': self.skill_id_2,
+            'skill_description': 'skill_description',
+            'question_count': 0,
+            'topic_name': 'topic1'
+        }
 
-        stories = [story_domain.Story.create_default_story(
-            '%s' % i,
-            'title %d' % i,
-            'description %d' % i,
-            '0',
-            'title-%s' % chr(97 + i)
-        ) for i in range(2)]
-
-        for index, story in enumerate(stories):
-            story.language_code = 'en'
-            story_services.save_new_story(self.owner_id, story)
-            topic_services.add_canonical_story(
-                self.owner_id, topic.id, story.id)
-            topic_services.publish_story(
-                topic.id, story.id, self.admin_id)
-            story_services.update_story(
-                self.owner_id, story.id, [story_domain.StoryChange({
-                    'cmd': 'add_story_node',
-                    'node_id': 'node_1',
-                    'title': 'Node1',
-                }), story_domain.StoryChange({
-                    'cmd': 'update_story_node_property',
-                    'property_name': 'exploration_id',
-                    'node_id': 'node_1',
-                    'old_value': None,
-                    'new_value': explorations[index].id
-                })], 'Changes.')
-
-        # The content_count is 4 for the expected dicts below since a valid
-        # exploration with EndExploration is created above, so the content in
-        # the last state is also included in the count. This content includes:
-        # 2 content, 1 TextInput interaction customization argument
-        # (placeholder), and 1 outcome.
+        # The content_count is 2 for the expected dicts below since each
+        # corresponding exploration has one initial state and one end state.
         self.expected_opportunity_dict_1 = {
             'id': '0',
             'topic_name': 'topic',
-            'story_title': 'title 0',
+            'story_title': 'title story_id_0',
             'chapter_title': 'Node1',
             'content_count': 2,
             'translation_counts': {},
             'translation_in_review_counts': {}
         }
-
         self.expected_opportunity_dict_2 = {
             'id': '1',
             'topic_name': 'topic',
-            'story_title': 'title 1',
+            'story_title': 'title story_id_1',
+            'chapter_title': 'Node1',
+            'content_count': 2,
+            'translation_counts': {},
+            'translation_in_review_counts': {}
+        }
+        self.expected_opportunity_dict_3 = {
+            'id': '2',
+            'topic_name': 'topic1',
+            'story_title': 'title story_id_2',
             'chapter_title': 'Node1',
             'content_count': 2,
             'translation_counts': {},
@@ -215,10 +212,10 @@ def test_get_skill_opportunity_data_does_not_throw_for_deleted_topics(self):
         self.assertFalse(response['more'])
         self.assertIsInstance(response['next_cursor'], str)
 
-    def test_get_translation_opportunity_data(self):
+    def test_get_translation_opportunities_fetches_matching_opportunities(self):
         response = self.get_json(
             '%s/translation' % feconf.CONTRIBUTOR_OPPORTUNITIES_DATA_URL,
-            params={'language_code': 'hi'})
+            params={'language_code': 'hi', 'topic_name': 'topic'})
 
         self.assertEqual(
             response['opportunities'], [
@@ -232,11 +229,12 @@ def test_get_voiceover_opportunity_data(self):
             '%s/voiceover' % feconf.CONTRIBUTOR_OPPORTUNITIES_DATA_URL,
             params={'language_code': 'en'})
 
-        self.assertEqual(len(response['opportunities']), 2)
+        self.assertEqual(len(response['opportunities']), 3)
         self.assertEqual(
             response['opportunities'], [
                 self.expected_opportunity_dict_1,
-                self.expected_opportunity_dict_2])
+                self.expected_opportunity_dict_2,
+                self.expected_opportunity_dict_3])
         self.assertFalse(response['more'])
         self.assertIsInstance(response['next_cursor'], str)
 
@@ -261,6 +259,17 @@ def test_get_skill_opportunity_data_pagination(self):
             self.assertEqual(
                 next_response['opportunities'],
                 [self.expected_skill_opportunity_dict_1])
+            self.assertTrue(next_response['more'])
+            self.assertIsInstance(next_response['next_cursor'], str)
+
+            next_cursor = next_response['next_cursor']
+            next_response = self.get_json(
+                '%s/skill' % feconf.CONTRIBUTOR_OPPORTUNITIES_DATA_URL,
+                params={'cursor': next_cursor})
+
+            # Skill 2 is not part of a Classroom topic and so its corresponding
+            # opportunity is not returned.
+            self.assertEqual(len(next_response['opportunities']), 0)
             self.assertFalse(next_response['more'])
             self.assertIsInstance(next_response['next_cursor'], str)
 
@@ -270,16 +279,15 @@ def test_get_skill_opportunity_data_pagination_multiple_fetches(self):
             self.admin_id, 'classroom_pages_data')
 
         # Create a new topic.
-        topic_id = '1'
-        topic_name = 'topic1'
+        topic_id = '9'
+        topic_name = 'topic9'
         topic = topic_domain.Topic.create_default_topic(
-            topic_id, topic_name, 'url-fragment', 'description')
-        skill_id_2 = 'skill_id_2'
+            topic_id, topic_name, 'url-fragment-nine', 'description')
         skill_id_3 = 'skill_id_3'
         skill_id_4 = 'skill_id_4'
         skill_id_5 = 'skill_id_5'
         self._publish_valid_topic(
-            topic, [skill_id_2, skill_id_3, skill_id_4, skill_id_5])
+            topic, [skill_id_3, skill_id_4, skill_id_5])
 
         # Add new topic to a classroom.
         config_services.set_property(
@@ -292,23 +300,17 @@ def test_get_skill_opportunity_data_pagination_multiple_fetches(self):
             }])
 
         # Opportunities with IDs skill_id_0, skill_id_1, skill_id_2 will be
-        # fetched first. Since skill_id_0, skill_id_1 are not linked to a
-        # classroom, another fetch will be made to retrieve skill_id_3,
+        # fetched first. Since skill_id_0, skill_id_1, skill_id_2 are not linked
+        # to a classroom, another fetch will be made to retrieve skill_id_3,
         # skill_id_4, skill_id_5 to fulfill the page size.
         with self.swap(constants, 'OPPORTUNITIES_PAGE_SIZE', 3):
             response = self.get_json(
                 '%s/skill' % feconf.CONTRIBUTOR_OPPORTUNITIES_DATA_URL,
                 params={})
-            self.assertEqual(len(response['opportunities']), 4)
+            self.assertEqual(len(response['opportunities']), 3)
             self.assertEqual(
                 response['opportunities'],
                 [
-                    {
-                        'id': skill_id_2,
-                        'skill_description': 'skill_description',
-                        'question_count': 0,
-                        'topic_name': topic_name
-                    },
                     {
                         'id': skill_id_3,
                         'skill_description': 'skill_description',
@@ -335,7 +337,7 @@ def test_get_translation_opportunity_data_pagination(self):
         with self.swap(constants, 'OPPORTUNITIES_PAGE_SIZE', 1):
             response = self.get_json(
                 '%s/translation' % feconf.CONTRIBUTOR_OPPORTUNITIES_DATA_URL,
-                params={'language_code': 'hi'})
+                params={'language_code': 'hi', 'topic_name': 'topic'})
             self.assertEqual(len(response['opportunities']), 1)
             self.assertEqual(
                 response['opportunities'], [self.expected_opportunity_dict_1])
@@ -346,6 +348,7 @@ def test_get_translation_opportunity_data_pagination(self):
                 '%s/translation' % feconf.CONTRIBUTOR_OPPORTUNITIES_DATA_URL,
                 params={
                     'language_code': 'hi',
+                    'topic_name': 'topic',
                     'cursor': response['next_cursor']
                 }
             )
@@ -376,6 +379,18 @@ def test_get_voiceover_opportunity_data_pagination(self):
             self.assertEqual(
                 next_response['opportunities'],
                 [self.expected_opportunity_dict_2])
+            self.assertTrue(next_response['more'])
+            self.assertIsInstance(next_response['next_cursor'], str)
+
+            next_cursor = next_response['next_cursor']
+            next_response = self.get_json(
+                '%s/voiceover' % feconf.CONTRIBUTOR_OPPORTUNITIES_DATA_URL,
+                params={'language_code': 'en', 'cursor': next_cursor})
+
+            self.assertEqual(len(next_response['opportunities']), 1)
+            self.assertEqual(
+                next_response['opportunities'],
+                [self.expected_opportunity_dict_3])
             self.assertFalse(next_response['more'])
             self.assertIsInstance(next_response['next_cursor'], str)
 
@@ -405,6 +420,34 @@ def test_get_voiceover_opportunity_without_language_code(self):
                 '%s/voiceover' % feconf.CONTRIBUTOR_OPPORTUNITIES_DATA_URL,
                 expected_status_int=400)
 
+    def test_get_translation_opportunities_without_topic_name_returns_all_topics( # pylint: disable=line-too-long
+            self):
+        response = self.get_json(
+            '%s/translation' % feconf.CONTRIBUTOR_OPPORTUNITIES_DATA_URL,
+            params={'language_code': 'hi'})
+
+        self.assertEqual(
+            response['opportunities'], [
+                self.expected_opportunity_dict_1,
+                self.expected_opportunity_dict_2,
+                self.expected_opportunity_dict_3])
+        self.assertFalse(response['more'])
+        self.assertIsInstance(response['next_cursor'], str)
+
+    def test_get_translation_opportunities_with_empty_topic_name_returns_all_topics( # pylint: disable=line-too-long
+            self):
+        response = self.get_json(
+            '%s/translation' % feconf.CONTRIBUTOR_OPPORTUNITIES_DATA_URL,
+            params={'language_code': 'hi', 'topic_name': ''})
+
+        self.assertEqual(
+            response['opportunities'], [
+                self.expected_opportunity_dict_1,
+                self.expected_opportunity_dict_2,
+                self.expected_opportunity_dict_3])
+        self.assertFalse(response['more'])
+        self.assertIsInstance(response['next_cursor'], str)
+
     def test_get_opportunity_for_invalid_opportunity_type(self):
         with self.swap(constants, 'OPPORTUNITIES_PAGE_SIZE', 1):
             self.get_json(
@@ -450,6 +493,42 @@ def _publish_valid_topic(self, topic, uncategorized_skill_ids):
             topic_services.add_uncategorized_skill(
                 self.admin_id, topic.id, skill_id)
 
+    def _create_story_for_translation_opportunity(
+            self, story_id, topic_id, exploration_id):
+        """Creates a story and links it to the supplied topic and exploration.
+
+        Args:
+            story_id: str. The ID of new story.
+            topic_id: str. The ID of the topic for which to link the story.
+            exploration_id: str. The ID of the exploration that will be added
+                as a node to the story.
+        """
+        story = story_domain.Story.create_default_story(
+            story_id,
+            'title %s' % story_id,
+            'description',
+            topic_id,
+            'url-fragment')
+
+        story.language_code = 'en'
+        story_services.save_new_story(self.owner_id, story)
+        topic_services.add_canonical_story(
+            self.owner_id, topic_id, story.id)
+        topic_services.publish_story(
+            topic_id, story.id, self.admin_id)
+        story_services.update_story(
+            self.owner_id, story.id, [story_domain.StoryChange({
+                'cmd': 'add_story_node',
+                'node_id': 'node_1',
+                'title': 'Node1',
+            }), story_domain.StoryChange({
+                'cmd': 'update_story_node_property',
+                'property_name': 'exploration_id',
+                'node_id': 'node_1',
+                'old_value': None,
+                'new_value': exploration_id
+            })], 'Changes.')
+
 
 class TranslatableTextHandlerTest(test_utils.GenericTestBase):
     """Unit test for the ContributionOpportunitiesHandler."""
diff --git a/core/tests/protractor_desktop/contributorDashboard.js b/core/tests/protractor_desktop/contributorDashboard.js
--- a/core/tests/protractor_desktop/contributorDashboard.js
+++ b/core/tests/protractor_desktop/contributorDashboard.js
@@ -171,7 +171,7 @@ describe('Contributor dashboard page', function() {
     await contributorDashboardPage.waitForOpportunitiesToLoad();
     // After acceptance, progress percentage should be 1/50 = 2%.
     await contributorDashboardPage.expectOpportunityWithPropertiesToExist(
-      SKILL_DESCRIPTIONS[0], TOPIC_NAMES[0], null, '(2.00%)');
+      SKILL_DESCRIPTIONS[0], TOPIC_NAMES[0], null, '(10.00%)');
     await users.logout();
 
     // Validate the contribution status changed.
@@ -191,7 +191,7 @@ describe('Contributor dashboard page', function() {
     await contributorDashboardPage.navigateToSubmitQuestionTab();
     await contributorDashboardPage.waitForOpportunitiesToLoad();
     await contributorDashboardPage.expectOpportunityWithPropertiesToExist(
-      SKILL_DESCRIPTIONS[0], TOPIC_NAMES[0], null, '(2.00%)');
+      SKILL_DESCRIPTIONS[0], TOPIC_NAMES[0], null, '(10.00%)');
 
     // Submit suggestion as user0.
     await contributorDashboardPage.clickOpportunityActionButton(
@@ -234,7 +234,7 @@ describe('Contributor dashboard page', function() {
     await contributorDashboardPage.navigateToSubmitQuestionTab();
     await contributorDashboardPage.waitForOpportunitiesToLoad();
     await contributorDashboardPage.expectOpportunityWithPropertiesToExist(
-      SKILL_DESCRIPTIONS[0], TOPIC_NAMES[0], null, '(2.00%)');
+      SKILL_DESCRIPTIONS[0], TOPIC_NAMES[0], null, '(10.00%)');
     await users.logout();
 
     // Validate the contribution status changed.