Repository: oppia/oppia

Issue:
Allow topic managers to view private viewer pages
**Is your feature request related to a problem? Please describe.**
Currently, there is no 'preview' functionality for new structures viewer pages.

**Describe the solution you'd like**
This solution entails adding a method so that topic managers and admins can access viewer pages even if they are private (like if topic or story is not published).



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
@@ -2330,8 +2330,12 @@ def test_can_access(self, topic_name, **kwargs):
         topic_id = topic.id
         topic_rights = topic_services.get_topic_rights(
             topic_id, strict=False)
+        user_actions_info = user_services.UserActionsInfo(self.user_id)
 
-        if topic_rights.topic_is_published:
+        if (
+                topic_rights.topic_is_published or
+                role_services.ACTION_VISIT_ANY_TOPIC_EDITOR in
+                user_actions_info.actions):
             return handler(self, topic_name, **kwargs)
         else:
             raise self.PageNotFoundException
@@ -2372,6 +2376,7 @@ def test_can_access(self, story_id, **kwargs):
         story_is_published = False
         topic_is_published = False
         topic_id = story.corresponding_topic_id
+        user_actions_info = user_services.UserActionsInfo(self.user_id)
         if topic_id:
             topic = topic_fetchers.get_topic_by_id(topic_id)
             topic_rights = topic_services.get_topic_rights(topic_id)
@@ -2381,7 +2386,10 @@ def test_can_access(self, story_id, **kwargs):
                 if reference.story_id == story_id:
                     story_is_published = reference.story_is_published
 
-        if story_is_published and topic_is_published:
+        if (
+                (story_is_published and topic_is_published) or
+                role_services.ACTION_VISIT_ANY_TOPIC_EDITOR in
+                user_actions_info.actions):
             return handler(self, story_id, **kwargs)
         else:
             raise self.PageNotFoundException
@@ -2419,8 +2427,13 @@ def test_can_access(self, topic_name, subtopic_id, **kwargs):
         if topic is None:
             raise self.PageNotFoundException
 
+        user_actions_info = user_services.UserActionsInfo(self.user_id)
         topic_rights = topic_services.get_topic_rights(topic.id)
-        if topic_rights is None or not topic_rights.topic_is_published:
+
+        if (
+                (topic_rights is None or not topic_rights.topic_is_published)
+                and role_services.ACTION_VISIT_ANY_TOPIC_EDITOR not in
+                user_actions_info.actions):
             raise self.PageNotFoundException
 
         subtopic_is_present = any(
diff --git a/core/controllers/resources.py b/core/controllers/resources.py
--- a/core/controllers/resources.py
+++ b/core/controllers/resources.py
@@ -53,6 +53,11 @@ class AssetDevHandler(base.BaseHandler):
     """
 
     _SUPPORTED_TYPES = ['image', 'audio']
+    _SUPPORTED_PAGE_CONTEXTS = [
+        feconf.ENTITY_TYPE_EXPLORATION, feconf.ENTITY_TYPE_SKILL,
+        feconf.ENTITY_TYPE_TOPIC, feconf.ENTITY_TYPE_STORY,
+        feconf.ENTITY_TYPE_QUESTION, feconf.ENTITY_TYPE_SUBTOPIC
+    ]
 
     GET_HANDLER_ERROR_RETURN_TYPE = feconf.HANDLER_TYPE_JSON
 
@@ -87,19 +92,16 @@ def get(self, page_context, page_identifier, asset_type, encoded_filename):
                 b'Content-Type'] = python_utils.convert_to_bytes(
                     '%s/%s' % (asset_type, file_format))
 
+            if page_context not in self._SUPPORTED_PAGE_CONTEXTS:
+                raise self.InvalidInputException
+
             if page_context == feconf.ENTITY_TYPE_SUBTOPIC:
                 entity_type = feconf.ENTITY_TYPE_TOPIC
                 topic = topic_fetchers.get_topic_by_name(page_identifier)
                 entity_id = topic.id
-            elif (
-                    page_context == feconf.ENTITY_TYPE_EXPLORATION or
-                    page_context == feconf.ENTITY_TYPE_SKILL or
-                    page_context == feconf.ENTITY_TYPE_TOPIC or
-                    page_context == feconf.ENTITY_TYPE_STORY):
+            else:
                 entity_type = page_context
                 entity_id = page_identifier
-            else:
-                raise self.InvalidInputException
 
             fs = fs_domain.AbstractFileSystem(
                 fs_domain.DatastoreBackedFileSystem(entity_type, entity_id))


Test patch:
diff --git a/core/controllers/story_viewer_test.py b/core/controllers/story_viewer_test.py
--- a/core/controllers/story_viewer_test.py
+++ b/core/controllers/story_viewer_test.py
@@ -124,6 +124,29 @@ def test_any_user_can_access_story_viewer_page(self):
             self.get_html_response(
                 '%s/%s' % (feconf.STORY_VIEWER_URL_PREFIX, self.STORY_ID_1))
 
+    def test_accessibility_of_unpublished_story_viewer_page(self):
+        topic_services.unpublish_story(
+            self.TOPIC_ID, self.STORY_ID_1, self.admin_id)
+        with self.swap(constants, 'ENABLE_NEW_STRUCTURE_PLAYERS', True):
+            self.get_html_response(
+                '%s/%s' % (feconf.STORY_VIEWER_URL_PREFIX, self.STORY_ID_1),
+                expected_status_int=404)
+            self.login(self.ADMIN_EMAIL)
+            self.get_html_response(
+                '%s/%s' % (feconf.STORY_VIEWER_URL_PREFIX, self.STORY_ID_1))
+            self.logout()
+
+    def test_accessibility_of_story_viewer_in_unpublished_topic(self):
+        topic_services.unpublish_topic(self.TOPIC_ID, self.admin_id)
+        with self.swap(constants, 'ENABLE_NEW_STRUCTURE_PLAYERS', True):
+            self.get_html_response(
+                '%s/%s' % (feconf.STORY_VIEWER_URL_PREFIX, self.STORY_ID_1),
+                expected_status_int=404)
+            self.login(self.ADMIN_EMAIL)
+            self.get_html_response(
+                '%s/%s' % (feconf.STORY_VIEWER_URL_PREFIX, self.STORY_ID_1))
+            self.logout()
+
     def test_get_fails_when_new_structures_not_enabled(self):
         with self.swap(constants, 'ENABLE_NEW_STRUCTURE_PLAYERS', False):
             self.get_html_response(
diff --git a/core/controllers/subtopic_viewer_test.py b/core/controllers/subtopic_viewer_test.py
--- a/core/controllers/subtopic_viewer_test.py
+++ b/core/controllers/subtopic_viewer_test.py
@@ -29,8 +29,6 @@
 
 class BaseSubtopicViewerControllerTests(test_utils.GenericTestBase):
 
-    user_id = 'user_id'
-
     def setUp(self):
         super(BaseSubtopicViewerControllerTests, self).setUp()
         self.signup(self.ADMIN_EMAIL, self.ADMIN_USERNAME)
@@ -43,7 +41,18 @@ def setUp(self):
             subtopic_page_domain.SubtopicPage.create_default_subtopic_page(
                 self.subtopic_id, self.topic_id))
         subtopic_page_services.save_subtopic_page(
-            self.user_id, self.subtopic_page, 'Added subtopic',
+            self.admin_id, self.subtopic_page, 'Added subtopic',
+            [topic_domain.TopicChange({
+                'cmd': topic_domain.CMD_ADD_SUBTOPIC,
+                'subtopic_id': self.subtopic_id,
+                'title': 'Sample'
+            })]
+        )
+        subtopic_page_2 = (
+            subtopic_page_domain.SubtopicPage.create_default_subtopic_page(
+                self.subtopic_id, 'topic_id_2'))
+        subtopic_page_services.save_subtopic_page(
+            self.admin_id, subtopic_page_2, 'Added subtopic',
             [topic_domain.TopicChange({
                 'cmd': topic_domain.CMD_ADD_SUBTOPIC,
                 'subtopic_id': self.subtopic_id,
@@ -84,7 +93,7 @@ def setUp(self):
             state_domain.RecordedVoiceovers.from_dict(
                 self.recorded_voiceovers_dict))
         subtopic_page_services.save_subtopic_page(
-            self.user_id, self.subtopic_page, 'Updated page contents',
+            self.admin_id, self.subtopic_page, 'Updated page contents',
             [subtopic_page_domain.SubtopicPageChange({
                 'cmd': subtopic_page_domain.CMD_UPDATE_SUBTOPIC_PAGE_PROPERTY,
                 'subtopic_id': self.subtopic_id,
@@ -103,12 +112,18 @@ def test_any_user_can_access_subtopic_viewer_page(self):
                 '%s/%s/%s' % (feconf.SUBTOPIC_VIEWER_URL_PREFIX, 'Name', '1'))
 
 
-    def test_no_user_can_access_subtopic_viewer_page_of_unpublished_topic(self):
+    def test_accessibility_of_subtopic_viewer_page_of_unpublished_topic(
+            self):
         with self.swap(constants, 'ENABLE_NEW_STRUCTURE_PLAYERS', True):
             self.get_html_response(
                 '%s/%s/%s' % (
                     feconf.SUBTOPIC_VIEWER_URL_PREFIX, 'Private_Name', '1'),
                 expected_status_int=404)
+            self.login(self.ADMIN_EMAIL)
+            self.get_html_response(
+                '%s/%s/%s' % (
+                    feconf.SUBTOPIC_VIEWER_URL_PREFIX, 'Private_Name', '1'))
+            self.logout()
 
 
     def test_get_fails_when_new_structures_not_enabled(self):
diff --git a/core/controllers/topic_viewer_test.py b/core/controllers/topic_viewer_test.py
--- a/core/controllers/topic_viewer_test.py
+++ b/core/controllers/topic_viewer_test.py
@@ -87,12 +87,17 @@ def test_any_user_can_access_topic_viewer_page(self):
                 '%s/%s' % (feconf.TOPIC_VIEWER_URL_PREFIX, 'public_topic_name'))
 
 
-    def test_no_user_can_access_unpublished_topic_viewer_page(self):
+    def test_accessibility_of_unpublished_topic_viewer_page(self):
         with self.swap(constants, 'ENABLE_NEW_STRUCTURE_PLAYERS', True):
             self.get_html_response(
                 '%s/%s' % (
                     feconf.TOPIC_VIEWER_URL_PREFIX, 'private_topic_name'),
                 expected_status_int=404)
+            self.login(self.ADMIN_EMAIL)
+            self.get_html_response(
+                '%s/%s' % (
+                    feconf.TOPIC_VIEWER_URL_PREFIX, 'private_topic_name'))
+            self.logout()
 
 
     def test_get_fails_when_new_structures_not_enabled(self):