Repository: oppia/oppia

Issue:
Add functionality to get the set of interactions that are used in an exploration.
This is a feature request for the Admin page (located at /admin).

In the Misc tab, it would be nice to have a section where you can enter an exploration ID, click a button, and get the set of interaction IDs that that exploration contains. This is much easier than clicking through all the states in the exploration manually, and is useful for determining which interactions we will need to implement in Android.

Clicking the button should make a GET request to a backend handler that fetches the exploration, extracts its interaction IDs, and returns them to the frontend for display to the user. The returned IDs should be de-duplicated. Since this is an admin feature, the backend handler should be admin-accessible only.
Add functionality to get the set of interactions that are used in an exploration.
This is a feature request for the Admin page (located at /admin).

In the Misc tab, it would be nice to have a section where you can enter an exploration ID, click a button, and get the set of interaction IDs that that exploration contains. This is much easier than clicking through all the states in the exploration manually, and is useful for determining which interactions we will need to implement in Android.

Clicking the button should make a GET request to a backend handler that fetches the exploration, extracts its interaction IDs, and returns them to the frontend for display to the user. The returned IDs should be de-duplicated. Since this is an admin feature, the backend handler should be admin-accessible only.


Discussion:
@seanlip can you assign this to me
Done!
@seanlip sorry for the delay
I will start working on this now wasn't well before so I couldn't start working on this.
No worries. Thanks for the update and I hope you are feeling better!
@seanlip can you tell where to look for the get method or there isn't any method for this particular request ? i couldn't find any in admin.py
There's no method yet. The point of this issue is to implement it (full-stack).
@seanlip  I've completed the frontend part can you help me a bit for the backend i need to understand from where i can start i was able to a couple of things like there is annotation which will restrict the corresponding api to admin handler only but couldn't fully each and every thing
You may find the [codebase overview](https://github.com/oppia/oppia/wiki/Overview-of-the-Oppia-codebase) helpful, but otherwise my recommendation is to look at existing handlers and see how they work. The "annotations" you mention are called "decorators" -- look up that concept in Python, and search the codebase to see how a decorator is implemented. Otherwise, if you have more specific questions, please ask them, but make sure that your questions are specific.
Okay i will take a look thanks🙂
@seanlip so i was able to understand a bit about the architecture just wanted to confirm that i'm moving in the right direction 
So, first of all, i have to get the exploration object from exp_fetchers using get_exploration_by_id  funtionc and every exploration object has states in which there is an interaction object where i can get the id correct?
Yes.
Hey @aggarwalpulkit596 I wanted to check: how's this issue going?
@BenHenning backend code is complete just need to show on the frontend for that I've asked a bit of help from @DubeySandeep 
Thanks @aggarwalpulkit596. Do you have a PR with the backend code that you can send my way?

Also did you mean to unassign yourself from this issue?
@BenHenning https://github.com/oppia/oppia/pull/9111 here you go
Thanks! I'm guessing you meant to refer to #9139 instead of #9111, so I reviewed & commented on that.
Hi @oppia/core-maintainers, this issue is not assigned to any project. Can you please update the same? Thanks!
@seanlip can you assign this to me
Done!
@seanlip sorry for the delay
I will start working on this now wasn't well before so I couldn't start working on this.
No worries. Thanks for the update and I hope you are feeling better!
@seanlip can you tell where to look for the get method or there isn't any method for this particular request ? i couldn't find any in admin.py
There's no method yet. The point of this issue is to implement it (full-stack).
@seanlip  I've completed the frontend part can you help me a bit for the backend i need to understand from where i can start i was able to a couple of things like there is annotation which will restrict the corresponding api to admin handler only but couldn't fully each and every thing
You may find the [codebase overview](https://github.com/oppia/oppia/wiki/Overview-of-the-Oppia-codebase) helpful, but otherwise my recommendation is to look at existing handlers and see how they work. The "annotations" you mention are called "decorators" -- look up that concept in Python, and search the codebase to see how a decorator is implemented. Otherwise, if you have more specific questions, please ask them, but make sure that your questions are specific.
Okay i will take a look thanks🙂
@seanlip so i was able to understand a bit about the architecture just wanted to confirm that i'm moving in the right direction 
So, first of all, i have to get the exploration object from exp_fetchers using get_exploration_by_id  funtionc and every exploration object has states in which there is an interaction object where i can get the id correct?
Yes.
Hey @aggarwalpulkit596 I wanted to check: how's this issue going?
@BenHenning backend code is complete just need to show on the frontend for that I've asked a bit of help from @DubeySandeep 
Thanks @aggarwalpulkit596. Do you have a PR with the backend code that you can send my way?

Also did you mean to unassign yourself from this issue?
@BenHenning https://github.com/oppia/oppia/pull/9111 here you go
Thanks! I'm guessing you meant to refer to #9139 instead of #9111, so I reviewed & commented on that.
Hi @oppia/core-maintainers, this issue is not assigned to any project. Can you please update the same? Thanks!

Resolving patch:
diff --git a/core/controllers/admin.py b/core/controllers/admin.py
--- a/core/controllers/admin.py
+++ b/core/controllers/admin.py
@@ -2345,3 +2345,51 @@ def put(self) -> None:
                 language_coordinator, language_id)
 
         self.render_json({})
+
+
+class InteractionsByExplorationIdHandlerNormalizedRequestDict(TypedDict):
+    """Dict representation of InteractionsByExplorationIdHandler's
+    normalized_request dictionary.
+    """
+
+    exp_id: str
+
+
+class InteractionsByExplorationIdHandler(
+    base.BaseHandler[
+        InteractionsByExplorationIdHandlerNormalizedRequestDict, Dict[str, str]
+    ]
+):
+    """Handler for admin to retrive the list of interactions used in
+    an exploration.
+    """
+
+    GET_HANDLER_ERROR_RETURN_TYPE = feconf.HANDLER_TYPE_JSON
+    URL_PATH_ARGS_SCHEMAS: Dict[str, str] = {}
+    HANDLER_ARGS_SCHEMAS = {
+        'GET': {
+            'exp_id': {
+                'schema': {
+                    'type': 'basestring'
+                }
+            }
+        }
+    }
+
+    @acl_decorators.can_access_admin_page
+    def get(self) -> None:
+        assert self.normalized_request is not None
+        exploration_id = self.normalized_request['exp_id']
+
+        exploration = exp_fetchers.get_exploration_by_id(
+            exploration_id, strict=False)
+        if exploration is None:
+            raise self.InvalidInputException('Exploration does not exist.')
+
+        interaction_ids = [
+            {'id': state.interaction.id}
+            for state in exploration.states.values()
+            if state.interaction.id is not None
+        ]
+
+        self.render_json({'interactions': list(interaction_ids)})
diff --git a/main.py b/main.py
--- a/main.py
+++ b/main.py
@@ -981,6 +981,9 @@ def get_redirect_route(
         r'/verifyusermodelsdeletedhandler',
         admin.VerifyUserModelsDeletedHandler),
     get_redirect_route(r'/deleteuserhandler', admin.DeleteUserHandler),
+    get_redirect_route(
+        r'/interactions',
+        admin.InteractionsByExplorationIdHandler),
     get_redirect_route(r'/frontend_errors', FrontendErrorHandler),
 
     get_redirect_route(r'/session_begin', base.SessionBeginHandler),


Test patch:
diff --git a/core/controllers/admin_test.py b/core/controllers/admin_test.py
--- a/core/controllers/admin_test.py
+++ b/core/controllers/admin_test.py
@@ -3137,3 +3137,52 @@ def test_handler_raises_error_with_invalid_blog_post_title(self) -> None:
         )
         self.assertEqual(blog_post_count, 0)
         self.logout()
+
+
+class IntereactionByExplorationIdHandlerTests(test_utils.GenericTestBase):
+    """Tests for interaction by exploration handler."""
+
+    EXP_ID_1 = 'exp1'
+
+    def setUp(self) -> None:
+        """Complete the signup process for self.ADMIN_EMAIL."""
+        super().setUp()
+        self.signup(feconf.ADMIN_EMAIL_ADDRESS, 'testsuper')
+        self.signup(self.EDITOR_EMAIL, self.EDITOR_USERNAME)
+        self.editor_id = self.get_user_id_from_email(self.EDITOR_EMAIL)
+        self.exploration1 = self.save_new_valid_exploration(
+            self.EXP_ID_1, self.editor_id, end_state_name='End')
+
+    def test_interactions_by_exploration_id_handler(self) -> None:
+        self.login(feconf.ADMIN_EMAIL_ADDRESS, is_super_admin=True)
+
+        payload = {
+            'exp_id': self.EXP_ID_1
+        }
+
+        response = self.get_json(
+            '/interactions', params=payload)
+        interaction_ids = sorted(
+            interaction['id'] for interaction in response['interactions'])
+        self.assertEqual(
+            sorted(interaction_ids), ['EndExploration', 'TextInput'])
+
+    def test_handler_with_invalid_exploration_id_raise_error(self) -> None:
+        self.login(feconf.ADMIN_EMAIL_ADDRESS, is_super_admin=True)
+
+        payload = {
+            'exp_id': 'invalid' 
+        }
+
+        response = self.get_json(
+            '/interactions', params=payload,
+            expected_status_int=400)
+        self.assertEqual(response['error'], 'Exploration does not exist.')
+
+    def test_handler_with_without_exploration_id_in_payload_raise_error(self) -> None: # pylint: disable=line-too-long
+        self.login(feconf.ADMIN_EMAIL_ADDRESS, is_super_admin=True)
+        response = self.get_json(
+            '/interactions', params={},
+            expected_status_int=400)
+        self.assertEqual(
+            response['error'], 'Missing key in handler args: exp_id.')