Repository: oppia/oppia

Issue:
Review Tests - Redirect learners to concept cards and automatically start a review test afterwards.
Part of Milestone 2.2 in @sophiewu6 's GSoC project


Discussion:

Resolving patch:
diff --git a/core/controllers/acl_decorators.py b/core/controllers/acl_decorators.py
--- a/core/controllers/acl_decorators.py
+++ b/core/controllers/acl_decorators.py
@@ -23,6 +23,7 @@
 from core.domain import question_services
 from core.domain import rights_manager
 from core.domain import role_services
+from core.domain import skill_domain
 from core.domain import skill_services
 from core.domain import story_services
 from core.domain import subtopic_page_services
@@ -108,26 +109,27 @@ def test_can_play(self, exploration_id, **kwargs):
     return test_can_play
 
 
-def can_view_skill(handler):
-    """Decorator to check whether user can play a given skill.
+def can_view_skills(handler):
+    """Decorator to check whether user can view multiple given skills.
 
     Args:
         handler: function. The function to be decorated.
 
     Returns:
         function. The newly decorated function that can also
-            check if the user can play a given skill.
+            check if the user can view multiple given skills.
     """
 
-    def test_can_play(self, skill_id, **kwargs):
-        """Checks if the user can play the skill.
+    def test_can_view(self, comma_separated_skill_ids, **kwargs):
+        """Checks if the user can view the skills.
 
         Args:
-            skill_id: str. The skill id.
+            comma_separated_skill_ids: str. The skill ids
+                separated by commas.
             **kwargs: *. Keyword arguments.
 
         Returns:
-            bool. Whether the user can play the given skill.
+            bool. Whether the user can view the given skills.
 
         Raises:
             PageNotFoundException: The page is not found.
@@ -135,15 +137,23 @@ def test_can_play(self, skill_id, **kwargs):
         # This is a temporary check, since a decorator is required for every
         # method. Once skill publishing is done, whether given skill is
         # published should be checked here.
-        skill = skill_services.get_skill_by_id(skill_id, strict=False)
+        skill_ids = comma_separated_skill_ids.split(',')
 
-        if skill is not None:
-            return handler(self, skill_id, **kwargs)
-        else:
-            raise self.PageNotFoundException
-    test_can_play.__wrapped__ = True
+        try:
+            for skill_id in skill_ids:
+                skill_domain.Skill.require_valid_skill_id(skill_id)
+        except Exception:
+            raise self.InvalidInputException
 
-    return test_can_play
+        try:
+            skill_services.get_multi_skills(skill_ids)
+        except Exception as e:
+            raise self.PageNotFoundException(e)
+
+        return handler(self, comma_separated_skill_ids, **kwargs)
+    test_can_view.__wrapped__ = True
+
+    return test_can_view
 
 
 def can_play_collection(handler):
diff --git a/core/controllers/concept_card_viewer.py b/core/controllers/concept_card_viewer.py
--- a/core/controllers/concept_card_viewer.py
+++ b/core/controllers/concept_card_viewer.py
@@ -26,18 +26,22 @@ class ConceptCardDataHandler(base.BaseHandler):
 
     GET_HANDLER_ERROR_RETURN_TYPE = feconf.HANDLER_TYPE_JSON
 
-    @acl_decorators.can_view_skill
-    def get(self, skill_id):
+    @acl_decorators.can_view_skills
+    def get(self, comma_separated_skill_ids):
         """Handles GET requests."""
 
         if not constants.ENABLE_NEW_STRUCTURE_PLAYERS:
             raise self.PageNotFoundException
 
-        skill = skill_services.get_skill_by_id(skill_id, strict=False)
+        skill_ids = comma_separated_skill_ids.split(',')
+        skills = skill_services.get_multi_skills(skill_ids)
+
+        concept_card_dicts = []
+        for skill in skills:
+            concept_card_dicts.append(skill.skill_contents.to_dict())
 
-        skill_dict = skill.to_dict()
         self.values.update({
-            'concept_card_dict': skill_dict['skill_contents']
+            'concept_card_dicts': concept_card_dicts
         })
 
         self.render_json(self.values)
diff --git a/main.py b/main.py
--- a/main.py
+++ b/main.py
@@ -604,7 +604,7 @@ def ui_access_wrapper(self, *args, **kwargs):
         topic_editor.TopicPublishSendMailHandler),
 
     get_redirect_route(
-        r'%s/<skill_id>' % feconf.CONCEPT_CARD_DATA_URL_PREFIX,
+        r'%s/<comma_separated_skill_ids>' % feconf.CONCEPT_CARD_DATA_URL_PREFIX,
         concept_card_viewer.ConceptCardDataHandler),
     get_redirect_route(
         r'%s/<question_id>/<skill_id>' % feconf.QUESTION_SKILL_LINK_URL_PREFIX,


Test patch:
diff --git a/core/controllers/concept_card_viewer_test.py b/core/controllers/concept_card_viewer_test.py
--- a/core/controllers/concept_card_viewer_test.py
+++ b/core/controllers/concept_card_viewer_test.py
@@ -45,19 +45,38 @@ def setUp(self):
             state_domain.WrittenTranslations.from_dict({
                 'translations_mapping': {'1': {}, '2': {}, '3': {}}
             }))
+        self.skill_contents_1 = skill_domain.SkillContents(
+            state_domain.SubtitledHtml(
+                '1', '<p>Skill Explanation 1</p>'), [
+                    state_domain.SubtitledHtml('2', '<p>Example 3</p>'),
+                    state_domain.SubtitledHtml('3', '<p>Example 4</p>')],
+            state_domain.RecordedVoiceovers.from_dict(
+                {'voiceovers_mapping': {'1': {}, '2': {}, '3': {}}}),
+            state_domain.WrittenTranslations.from_dict({
+                'translations_mapping': {'1': {}, '2': {}, '3': {}}
+            }))
         self.admin = user_services.UserActionsInfo(self.admin_id)
         self.skill_id = skill_services.get_new_skill_id()
         self.save_new_skill(
             self.skill_id, self.admin_id, 'Description',
             skill_contents=self.skill_contents)
+        self.skill_id_1 = skill_services.get_new_skill_id()
+        self.save_new_skill(
+            self.skill_id_1, self.admin_id, 'Description',
+            skill_contents=self.skill_contents_1)
+        self.skill_id_2 = skill_services.get_new_skill_id()
 
-    def test_get_concept_card(self):
+    def test_get_concept_cards(self):
         with self.swap(constants, 'ENABLE_NEW_STRUCTURE_PLAYERS', True):
             json_response = self.get_json(
-                '%s/%s' % (feconf.CONCEPT_CARD_DATA_URL_PREFIX, self.skill_id))
+                '%s/%s,%s' % (
+                    feconf.CONCEPT_CARD_DATA_URL_PREFIX,
+                    self.skill_id, self.skill_id_1)
+                )
+            self.assertEqual(2, len(json_response['concept_card_dicts']))
             self.assertEqual(
                 '<p>Skill Explanation</p>',
-                json_response['concept_card_dict']['explanation']['html'])
+                json_response['concept_card_dicts'][0]['explanation']['html'])
             self.assertEqual(
                 [{
                     'content_id': '2',
@@ -66,10 +85,30 @@ def test_get_concept_card(self):
                     'content_id': '3',
                     'html': '<p>Example 2</p>'
                 }],
-                json_response['concept_card_dict']['worked_examples'])
+                json_response['concept_card_dicts'][0]['worked_examples'])
 
-    def test_get_concept_card_fails_when_new_structures_not_enabled(self):
+            self.assertEqual(
+                '<p>Skill Explanation 1</p>',
+                json_response['concept_card_dicts'][1]['explanation']['html'])
+            self.assertEqual(
+                [{
+                    'content_id': '2',
+                    'html': '<p>Example 3</p>'
+                }, {
+                    'content_id': '3',
+                    'html': '<p>Example 4</p>'
+                }],
+                json_response['concept_card_dicts'][1]['worked_examples'])
+
+    def test_get_concept_cards_fails_when_new_structures_not_enabled(self):
         with self.swap(constants, 'ENABLE_NEW_STRUCTURE_PLAYERS', False):
             self.get_json(
                 '%s/%s' % (feconf.CONCEPT_CARD_DATA_URL_PREFIX, self.skill_id),
                 expected_status_int=404)
+
+    def test_get_concept_cards_fails_when_skill_doesnt_exist(self):
+        with self.swap(constants, 'ENABLE_NEW_STRUCTURE_PLAYERS', True):
+            self.get_json(
+                '%s/%s' % (
+                    feconf.CONCEPT_CARD_DATA_URL_PREFIX, self.skill_id_2),
+                expected_status_int=404)