We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Topic prerequisite skill checking is broken and prevents topic being published
**Describe the bug**

In a topic, chapters with prerequisite skills outside the topic make the topic unpublishable, because "this skill was not taught in any chapter before it". This behaviour is wrong because the validation error should only be triggered for skills that have been assigned to the topic currently being edited.

**To Reproduce**
Steps to reproduce the behavior:
 1. Create topic P (the prerequisite topic) and topic M (the main topic).
 2. Create skill S1 and assign it to P. Create skill S2 and assign it to M.
 3. Create a chapter in topic M and assign it a prerequisite skill of S1, and an acquired skill of S2.
 4. Save all changes.
 5. Refresh the page and try to publish the topic. This is not possible because of a validation error: "The skill with id XYZ was specified as a prerequisite for Chapter Name but was not taught in any chapter before it.". See screenshot below:
 
![Screenshot from 2022-02-24 18-00-50](https://user-images.githubusercontent.com/10575562/155502391-ff753a66-243d-43b9-9778-bf6c094118d8.png)

**Observed behavior**
The topic cannot be published, and further changes to the story/chapter cannot be saved, because the prerequisite skills include ones from outside the topic.

**Expected behavior**
The topic should be publishable. The validation message should **only** occur for skills that have been assigned to the main topic M, and not to other topics.


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `core/controllers/story_editor.py`
Content:
```
1 # Copyright 2018 The Oppia Authors. All Rights Reserved.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 #      http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS-IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14 
15 """Controllers for the story editor."""
16 
17 from __future__ import annotations
18 
19 from core import feconf
20 from core import utils
21 from core.constants import constants
22 from core.controllers import acl_decorators
23 from core.controllers import base
24 from core.domain import classroom_services
25 from core.domain import skill_services
26 from core.domain import story_domain
27 from core.domain import story_fetchers
28 from core.domain import story_services
29 from core.domain import topic_fetchers
30 from core.domain import topic_services
31 
32 
33 class StoryEditorPage(base.BaseHandler):
34     """The editor page for a single story."""
35 
36     URL_PATH_ARGS_SCHEMAS = {
37         'story_id': {
38             'schema': {
39                 'type': 'basestring'
40             },
41             'validators': [{
42                 'id': 'has_length',
43                 'value': constants.STORY_ID_LENGTH
44             }]
45         }
46     }
47     HANDLER_ARGS_SCHEMAS = {
48         'GET': {}
49     }
50 
51     @acl_decorators.can_edit_story
52     def get(self, _):
53         """Handles GET requests."""
54 
55         self.render_template('story-editor-page.mainpage.html')
56 
57 
58 class EditableStoryDataHandler(base.BaseHandler):
59     """A data handler for stories which support writing."""
60 
61     GET_HANDLER_ERROR_RETURN_TYPE = feconf.HANDLER_TYPE_JSON
62 
63     def _require_valid_version(self, version_from_payload, story_version):
64         """Check that the payload version matches the given story
65         version.
66         """
67         if version_from_payload is None:
68             raise base.BaseHandler.InvalidInputException(
69                 'Invalid POST request: a version must be specified.')
70 
71         if version_from_payload != story_version:
72             raise base.BaseHandler.InvalidInputException(
73                 'Trying to update version %s of story from version %s, '
74                 'which is too old. Please reload the page and try again.'
75                 % (story_version, version_from_payload))
76 
77     @acl_decorators.can_edit_story
78     def get(self, story_id):
79         """Populates the data on the individual story page."""
80         story = story_fetchers.get_story_by_id(story_id, strict=False)
81         topic_id = story.corresponding_topic_id
82         topic = topic_fetchers.get_topic_by_id(topic_id, strict=False)
83         skill_ids = topic.get_all_skill_ids()
84         for node in story.story_contents.nodes:
85             for skill_id in node.prerequisite_skill_ids:
86                 if skill_id not in skill_ids:
87                     skill_ids.append(skill_id)
88 
89         skill_summaries = skill_services.get_multi_skill_summaries(skill_ids)
90         skill_summary_dicts = [summary.to_dict() for summary in skill_summaries]
91         classroom_url_fragment = (
92             classroom_services.get_classroom_url_fragment_for_topic_id(
93                 topic.id))
94 
95         for story_reference in topic.canonical_story_references:
96             if story_reference.story_id == story_id:
97                 story_is_published = story_reference.story_is_published
98 
99         self.values.update({
100             'story': story.to_dict(),
101             'topic_name': topic.name,
102             'story_is_published': story_is_published,
103             'skill_summaries': skill_summary_dicts,
104             'topic_url_fragment': topic.url_fragment,
105             'classroom_url_fragment': classroom_url_fragment
106         })
107 
108         self.render_json(self.values)
109 
110     @acl_decorators.can_edit_story
111     def put(self, story_id):
112         """Updates properties of the given story."""
113         story = story_fetchers.get_story_by_id(story_id, strict=False)
114 
115         version = self.payload.get('version')
116         self._require_valid_version(version, story.version)
117 
118         commit_message = self.payload.get('commit_message')
119 
120         if commit_message is None:
121             raise self.InvalidInputException(
122                 'Expected a commit message but received none.')
123 
124         if len(commit_message) > constants.MAX_COMMIT_MESSAGE_LENGTH:
125             raise self.InvalidInputException(
126                 'Commit messages must be at most %s characters long.'
127                 % constants.MAX_COMMIT_MESSAGE_LENGTH)
128 
129         change_dicts = self.payload.get('change_dicts')
130         change_list = [
131             story_domain.StoryChange(change_dict)
132             for change_dict in change_dicts
133         ]
134         try:
135             # Update the Story and its corresponding TopicSummary.
136             topic_services.update_story_and_topic_summary(
137                 self.user_id, story_id, change_list, commit_message,
138                 story.corresponding_topic_id)
139         except utils.ValidationError as e:
140             raise self.InvalidInputException(e)
141 
142         story_dict = story_fetchers.get_story_by_id(story_id).to_dict()
143 
144         self.values.update({
145             'story': story_dict
146         })
147 
148         self.render_json(self.values)
149 
150     @acl_decorators.can_delete_story
151     def delete(self, story_id):
152         """Handles Delete requests."""
153         story_services.delete_story(self.user_id, story_id)
154         self.render_json(self.values)
155 
156 
157 class StoryPublishHandler(base.BaseHandler):
158     """A data handler for publishing and unpublishing stories."""
159 
160     GET_HANDLER_ERROR_RETURN_TYPE = feconf.HANDLER_TYPE_JSON
161     URL_PATH_ARGS_SCHEMAS = {
162         'story_id': {
163             'schema': {
164                 'type': 'basestring'
165             },
166             'validators': [{
167                 'id': 'has_length',
168                 'value': constants.STORY_ID_LENGTH
169             }]
170         }
171     }
172     HANDLER_ARGS_SCHEMAS = {
173         'PUT': {
174             'new_story_status_is_public': {
175                 'schema': {
176                     'type': 'bool'
177                 },
178             }
179         }
180     }
181 
182     @acl_decorators.can_edit_story
183     def put(self, story_id):
184         """Published/unpublished given story."""
185         story = story_fetchers.get_story_by_id(story_id, strict=False)
186         topic_id = story.corresponding_topic_id
187 
188         new_story_status_is_public = self.normalized_payload.get(
189             'new_story_status_is_public')
190 
191         if new_story_status_is_public:
192             topic_services.publish_story(topic_id, story_id, self.user_id)
193         else:
194             topic_services.unpublish_story(topic_id, story_id, self.user_id)
195 
196         self.render_json(self.values)
197 
198 
199 class ValidateExplorationsHandler(base.BaseHandler):
200     """A data handler for validating the explorations in a story."""
201 
202     GET_HANDLER_ERROR_RETURN_TYPE = feconf.HANDLER_TYPE_JSON
203 
204     @acl_decorators.can_edit_story
205     def get(self, _):
206         """Handler that receives a list of exploration IDs, checks whether the
207         corresponding explorations are supported on mobile and returns the
208         validation error messages (if any).
209         """
210         comma_separated_exp_ids = self.request.get('comma_separated_exp_ids')
211         if not comma_separated_exp_ids:
212             raise self.InvalidInputException(
213                 'Expected comma_separated_exp_ids parameter to be present.')
214         exp_ids = comma_separated_exp_ids.split(',')
215         validation_error_messages = (
216             story_services.validate_explorations_for_story(exp_ids, False))
217         self.values.update({
218             'validation_error_messages': validation_error_messages
219         })
220         self.render_json(self.values)
221 
222 
223 class StoryUrlFragmentHandler(base.BaseHandler):
224     """A data handler for checking if a story with given url fragment exists.
225     """
226 
227     GET_HANDLER_ERROR_RETURN_TYPE = feconf.HANDLER_TYPE_JSON
228     URL_PATH_ARGS_SCHEMAS = {
229         'story_url_fragment': constants.SCHEMA_FOR_STORY_URL_FRAGMENTS
230     }
231     HANDLER_ARGS_SCHEMAS = {
232         'GET': {}
233     }
234 
235     @acl_decorators.open_access
236     def get(self, story_url_fragment):
237         """Handler that receives a story url fragment and checks whether
238         a story with the same url fragment exists or not.
239         """
240         self.values.update({
241             'story_url_fragment_exists': (
242                 story_services.does_story_exist_with_url_fragment(
243                     story_url_fragment))
244         })
245         self.render_json(self.values)
246
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/core/controllers/story_editor.py b/core/controllers/story_editor.py
--- a/core/controllers/story_editor.py
+++ b/core/controllers/story_editor.py
@@ -81,10 +81,6 @@
         topic_id = story.corresponding_topic_id
         topic = topic_fetchers.get_topic_by_id(topic_id, strict=False)
         skill_ids = topic.get_all_skill_ids()
-        for node in story.story_contents.nodes:
-            for skill_id in node.prerequisite_skill_ids:
-                if skill_id not in skill_ids:
-                    skill_ids.append(skill_id)
 
         skill_summaries = skill_services.get_multi_skill_summaries(skill_ids)
         skill_summary_dicts = [summary.to_dict() for summary in skill_summaries]

{"golden_diff": "diff --git a/core/controllers/story_editor.py b/core/controllers/story_editor.py\n--- a/core/controllers/story_editor.py\n+++ b/core/controllers/story_editor.py\n@@ -81,10 +81,6 @@\n         topic_id = story.corresponding_topic_id\n         topic = topic_fetchers.get_topic_by_id(topic_id, strict=False)\n         skill_ids = topic.get_all_skill_ids()\n-        for node in story.story_contents.nodes:\n-            for skill_id in node.prerequisite_skill_ids:\n-                if skill_id not in skill_ids:\n-                    skill_ids.append(skill_id)\n \n         skill_summaries = skill_services.get_multi_skill_summaries(skill_ids)\n         skill_summary_dicts = [summary.to_dict() for summary in skill_summaries]\n", "issue": "Topic prerequisite skill checking is broken and prevents topic being published\n**Describe the bug**\r\n\r\nIn a topic, chapters with prerequisite skills outside the topic make the topic unpublishable, because \"this skill was not taught in any chapter before it\". This behaviour is wrong because the validation error should only be triggered for skills that have been assigned to the topic currently being edited.\r\n\r\n**To Reproduce**\r\nSteps to reproduce the behavior:\r\n 1. Create topic P (the prerequisite topic) and topic M (the main topic).\r\n 2. Create skill S1 and assign it to P. Create skill S2 and assign it to M.\r\n 3. Create a chapter in topic M and assign it a prerequisite skill of S1, and an acquired skill of S2.\r\n 4. Save all changes.\r\n 5. Refresh the page and try to publish the topic. This is not possible because of a validation error: \"The skill with id XYZ was specified as a prerequisite for Chapter Name but was not taught in any chapter before it.\". See screenshot below:\r\n \r\n![Screenshot from 2022-02-24 18-00-50](https://user-images.githubusercontent.com/10575562/155502391-ff753a66-243d-43b9-9778-bf6c094118d8.png)\r\n\r\n**Observed behavior**\r\nThe topic cannot be published, and further changes to the story/chapter cannot be saved, because the prerequisite skills include ones from outside the topic.\r\n\r\n**Expected behavior**\r\nThe topic should be publishable. The validation message should **only** occur for skills that have been assigned to the main topic M, and not to other topics.\r\n\n", "before_files": [{"content": "# Copyright 2018 The Oppia Authors. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS-IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"Controllers for the story editor.\"\"\"\n\nfrom __future__ import annotations\n\nfrom core import feconf\nfrom core import utils\nfrom core.constants import constants\nfrom core.controllers import acl_decorators\nfrom core.controllers import base\nfrom core.domain import classroom_services\nfrom core.domain import skill_services\nfrom core.domain import story_domain\nfrom core.domain import story_fetchers\nfrom core.domain import story_services\nfrom core.domain import topic_fetchers\nfrom core.domain import topic_services\n\n\nclass StoryEditorPage(base.BaseHandler):\n    \"\"\"The editor page for a single story.\"\"\"\n\n    URL_PATH_ARGS_SCHEMAS = {\n        'story_id': {\n            'schema': {\n                'type': 'basestring'\n            },\n            'validators': [{\n                'id': 'has_length',\n                'value': constants.STORY_ID_LENGTH\n            }]\n        }\n    }\n    HANDLER_ARGS_SCHEMAS = {\n        'GET': {}\n    }\n\n    @acl_decorators.can_edit_story\n    def get(self, _):\n        \"\"\"Handles GET requests.\"\"\"\n\n        self.render_template('story-editor-page.mainpage.html')\n\n\nclass EditableStoryDataHandler(base.BaseHandler):\n    \"\"\"A data handler for stories which support writing.\"\"\"\n\n    GET_HANDLER_ERROR_RETURN_TYPE = feconf.HANDLER_TYPE_JSON\n\n    def _require_valid_version(self, version_from_payload, story_version):\n        \"\"\"Check that the payload version matches the given story\n        version.\n        \"\"\"\n        if version_from_payload is None:\n            raise base.BaseHandler.InvalidInputException(\n                'Invalid POST request: a version must be specified.')\n\n        if version_from_payload != story_version:\n            raise base.BaseHandler.InvalidInputException(\n                'Trying to update version %s of story from version %s, '\n                'which is too old. Please reload the page and try again.'\n                % (story_version, version_from_payload))\n\n    @acl_decorators.can_edit_story\n    def get(self, story_id):\n        \"\"\"Populates the data on the individual story page.\"\"\"\n        story = story_fetchers.get_story_by_id(story_id, strict=False)\n        topic_id = story.corresponding_topic_id\n        topic = topic_fetchers.get_topic_by_id(topic_id, strict=False)\n        skill_ids = topic.get_all_skill_ids()\n        for node in story.story_contents.nodes:\n            for skill_id in node.prerequisite_skill_ids:\n                if skill_id not in skill_ids:\n                    skill_ids.append(skill_id)\n\n        skill_summaries = skill_services.get_multi_skill_summaries(skill_ids)\n        skill_summary_dicts = [summary.to_dict() for summary in skill_summaries]\n        classroom_url_fragment = (\n            classroom_services.get_classroom_url_fragment_for_topic_id(\n                topic.id))\n\n        for story_reference in topic.canonical_story_references:\n            if story_reference.story_id == story_id:\n                story_is_published = story_reference.story_is_published\n\n        self.values.update({\n            'story': story.to_dict(),\n            'topic_name': topic.name,\n            'story_is_published': story_is_published,\n            'skill_summaries': skill_summary_dicts,\n            'topic_url_fragment': topic.url_fragment,\n            'classroom_url_fragment': classroom_url_fragment\n        })\n\n        self.render_json(self.values)\n\n    @acl_decorators.can_edit_story\n    def put(self, story_id):\n        \"\"\"Updates properties of the given story.\"\"\"\n        story = story_fetchers.get_story_by_id(story_id, strict=False)\n\n        version = self.payload.get('version')\n        self._require_valid_version(version, story.version)\n\n        commit_message = self.payload.get('commit_message')\n\n        if commit_message is None:\n            raise self.InvalidInputException(\n                'Expected a commit message but received none.')\n\n        if len(commit_message) > constants.MAX_COMMIT_MESSAGE_LENGTH:\n            raise self.InvalidInputException(\n                'Commit messages must be at most %s characters long.'\n                % constants.MAX_COMMIT_MESSAGE_LENGTH)\n\n        change_dicts = self.payload.get('change_dicts')\n        change_list = [\n            story_domain.StoryChange(change_dict)\n            for change_dict in change_dicts\n        ]\n        try:\n            # Update the Story and its corresponding TopicSummary.\n            topic_services.update_story_and_topic_summary(\n                self.user_id, story_id, change_list, commit_message,\n                story.corresponding_topic_id)\n        except utils.ValidationError as e:\n            raise self.InvalidInputException(e)\n\n        story_dict = story_fetchers.get_story_by_id(story_id).to_dict()\n\n        self.values.update({\n            'story': story_dict\n        })\n\n        self.render_json(self.values)\n\n    @acl_decorators.can_delete_story\n    def delete(self, story_id):\n        \"\"\"Handles Delete requests.\"\"\"\n        story_services.delete_story(self.user_id, story_id)\n        self.render_json(self.values)\n\n\nclass StoryPublishHandler(base.BaseHandler):\n    \"\"\"A data handler for publishing and unpublishing stories.\"\"\"\n\n    GET_HANDLER_ERROR_RETURN_TYPE = feconf.HANDLER_TYPE_JSON\n    URL_PATH_ARGS_SCHEMAS = {\n        'story_id': {\n            'schema': {\n                'type': 'basestring'\n            },\n            'validators': [{\n                'id': 'has_length',\n                'value': constants.STORY_ID_LENGTH\n            }]\n        }\n    }\n    HANDLER_ARGS_SCHEMAS = {\n        'PUT': {\n            'new_story_status_is_public': {\n                'schema': {\n                    'type': 'bool'\n                },\n            }\n        }\n    }\n\n    @acl_decorators.can_edit_story\n    def put(self, story_id):\n        \"\"\"Published/unpublished given story.\"\"\"\n        story = story_fetchers.get_story_by_id(story_id, strict=False)\n        topic_id = story.corresponding_topic_id\n\n        new_story_status_is_public = self.normalized_payload.get(\n            'new_story_status_is_public')\n\n        if new_story_status_is_public:\n            topic_services.publish_story(topic_id, story_id, self.user_id)\n        else:\n            topic_services.unpublish_story(topic_id, story_id, self.user_id)\n\n        self.render_json(self.values)\n\n\nclass ValidateExplorationsHandler(base.BaseHandler):\n    \"\"\"A data handler for validating the explorations in a story.\"\"\"\n\n    GET_HANDLER_ERROR_RETURN_TYPE = feconf.HANDLER_TYPE_JSON\n\n    @acl_decorators.can_edit_story\n    def get(self, _):\n        \"\"\"Handler that receives a list of exploration IDs, checks whether the\n        corresponding explorations are supported on mobile and returns the\n        validation error messages (if any).\n        \"\"\"\n        comma_separated_exp_ids = self.request.get('comma_separated_exp_ids')\n        if not comma_separated_exp_ids:\n            raise self.InvalidInputException(\n                'Expected comma_separated_exp_ids parameter to be present.')\n        exp_ids = comma_separated_exp_ids.split(',')\n        validation_error_messages = (\n            story_services.validate_explorations_for_story(exp_ids, False))\n        self.values.update({\n            'validation_error_messages': validation_error_messages\n        })\n        self.render_json(self.values)\n\n\nclass StoryUrlFragmentHandler(base.BaseHandler):\n    \"\"\"A data handler for checking if a story with given url fragment exists.\n    \"\"\"\n\n    GET_HANDLER_ERROR_RETURN_TYPE = feconf.HANDLER_TYPE_JSON\n    URL_PATH_ARGS_SCHEMAS = {\n        'story_url_fragment': constants.SCHEMA_FOR_STORY_URL_FRAGMENTS\n    }\n    HANDLER_ARGS_SCHEMAS = {\n        'GET': {}\n    }\n\n    @acl_decorators.open_access\n    def get(self, story_url_fragment):\n        \"\"\"Handler that receives a story url fragment and checks whether\n        a story with the same url fragment exists or not.\n        \"\"\"\n        self.values.update({\n            'story_url_fragment_exists': (\n                story_services.does_story_exist_with_url_fragment(\n                    story_url_fragment))\n        })\n        self.render_json(self.values)\n", "path": "core/controllers/story_editor.py"}], "after_files": [{"content": "# Copyright 2018 The Oppia Authors. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS-IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"Controllers for the story editor.\"\"\"\n\nfrom __future__ import annotations\n\nfrom core import feconf\nfrom core import utils\nfrom core.constants import constants\nfrom core.controllers import acl_decorators\nfrom core.controllers import base\nfrom core.domain import classroom_services\nfrom core.domain import skill_services\nfrom core.domain import story_domain\nfrom core.domain import story_fetchers\nfrom core.domain import story_services\nfrom core.domain import topic_fetchers\nfrom core.domain import topic_services\n\n\nclass StoryEditorPage(base.BaseHandler):\n    \"\"\"The editor page for a single story.\"\"\"\n\n    URL_PATH_ARGS_SCHEMAS = {\n        'story_id': {\n            'schema': {\n                'type': 'basestring'\n            },\n            'validators': [{\n                'id': 'has_length',\n                'value': constants.STORY_ID_LENGTH\n            }]\n        }\n    }\n    HANDLER_ARGS_SCHEMAS = {\n        'GET': {}\n    }\n\n    @acl_decorators.can_edit_story\n    def get(self, _):\n        \"\"\"Handles GET requests.\"\"\"\n\n        self.render_template('story-editor-page.mainpage.html')\n\n\nclass EditableStoryDataHandler(base.BaseHandler):\n    \"\"\"A data handler for stories which support writing.\"\"\"\n\n    GET_HANDLER_ERROR_RETURN_TYPE = feconf.HANDLER_TYPE_JSON\n\n    def _require_valid_version(self, version_from_payload, story_version):\n        \"\"\"Check that the payload version matches the given story\n        version.\n        \"\"\"\n        if version_from_payload is None:\n            raise base.BaseHandler.InvalidInputException(\n                'Invalid POST request: a version must be specified.')\n\n        if version_from_payload != story_version:\n            raise base.BaseHandler.InvalidInputException(\n                'Trying to update version %s of story from version %s, '\n                'which is too old. Please reload the page and try again.'\n                % (story_version, version_from_payload))\n\n    @acl_decorators.can_edit_story\n    def get(self, story_id):\n        \"\"\"Populates the data on the individual story page.\"\"\"\n        story = story_fetchers.get_story_by_id(story_id, strict=False)\n        topic_id = story.corresponding_topic_id\n        topic = topic_fetchers.get_topic_by_id(topic_id, strict=False)\n        skill_ids = topic.get_all_skill_ids()\n\n        skill_summaries = skill_services.get_multi_skill_summaries(skill_ids)\n        skill_summary_dicts = [summary.to_dict() for summary in skill_summaries]\n        classroom_url_fragment = (\n            classroom_services.get_classroom_url_fragment_for_topic_id(\n                topic.id))\n\n        for story_reference in topic.canonical_story_references:\n            if story_reference.story_id == story_id:\n                story_is_published = story_reference.story_is_published\n\n        self.values.update({\n            'story': story.to_dict(),\n            'topic_name': topic.name,\n            'story_is_published': story_is_published,\n            'skill_summaries': skill_summary_dicts,\n            'topic_url_fragment': topic.url_fragment,\n            'classroom_url_fragment': classroom_url_fragment\n        })\n\n        self.render_json(self.values)\n\n    @acl_decorators.can_edit_story\n    def put(self, story_id):\n        \"\"\"Updates properties of the given story.\"\"\"\n        story = story_fetchers.get_story_by_id(story_id, strict=False)\n\n        version = self.payload.get('version')\n        self._require_valid_version(version, story.version)\n\n        commit_message = self.payload.get('commit_message')\n\n        if commit_message is None:\n            raise self.InvalidInputException(\n                'Expected a commit message but received none.')\n\n        if len(commit_message) > constants.MAX_COMMIT_MESSAGE_LENGTH:\n            raise self.InvalidInputException(\n                'Commit messages must be at most %s characters long.'\n                % constants.MAX_COMMIT_MESSAGE_LENGTH)\n\n        change_dicts = self.payload.get('change_dicts')\n        change_list = [\n            story_domain.StoryChange(change_dict)\n            for change_dict in change_dicts\n        ]\n        try:\n            # Update the Story and its corresponding TopicSummary.\n            topic_services.update_story_and_topic_summary(\n                self.user_id, story_id, change_list, commit_message,\n                story.corresponding_topic_id)\n        except utils.ValidationError as e:\n            raise self.InvalidInputException(e)\n\n        story_dict = story_fetchers.get_story_by_id(story_id).to_dict()\n\n        self.values.update({\n            'story': story_dict\n        })\n\n        self.render_json(self.values)\n\n    @acl_decorators.can_delete_story\n    def delete(self, story_id):\n        \"\"\"Handles Delete requests.\"\"\"\n        story_services.delete_story(self.user_id, story_id)\n        self.render_json(self.values)\n\n\nclass StoryPublishHandler(base.BaseHandler):\n    \"\"\"A data handler for publishing and unpublishing stories.\"\"\"\n\n    GET_HANDLER_ERROR_RETURN_TYPE = feconf.HANDLER_TYPE_JSON\n    URL_PATH_ARGS_SCHEMAS = {\n        'story_id': {\n            'schema': {\n                'type': 'basestring'\n            },\n            'validators': [{\n                'id': 'has_length',\n                'value': constants.STORY_ID_LENGTH\n            }]\n        }\n    }\n    HANDLER_ARGS_SCHEMAS = {\n        'PUT': {\n            'new_story_status_is_public': {\n                'schema': {\n                    'type': 'bool'\n                },\n            }\n        }\n    }\n\n    @acl_decorators.can_edit_story\n    def put(self, story_id):\n        \"\"\"Published/unpublished given story.\"\"\"\n        story = story_fetchers.get_story_by_id(story_id, strict=False)\n        topic_id = story.corresponding_topic_id\n\n        new_story_status_is_public = self.normalized_payload.get(\n            'new_story_status_is_public')\n\n        if new_story_status_is_public:\n            topic_services.publish_story(topic_id, story_id, self.user_id)\n        else:\n            topic_services.unpublish_story(topic_id, story_id, self.user_id)\n\n        self.render_json(self.values)\n\n\nclass ValidateExplorationsHandler(base.BaseHandler):\n    \"\"\"A data handler for validating the explorations in a story.\"\"\"\n\n    GET_HANDLER_ERROR_RETURN_TYPE = feconf.HANDLER_TYPE_JSON\n\n    @acl_decorators.can_edit_story\n    def get(self, _):\n        \"\"\"Handler that receives a list of exploration IDs, checks whether the\n        corresponding explorations are supported on mobile and returns the\n        validation error messages (if any).\n        \"\"\"\n        comma_separated_exp_ids = self.request.get('comma_separated_exp_ids')\n        if not comma_separated_exp_ids:\n            raise self.InvalidInputException(\n                'Expected comma_separated_exp_ids parameter to be present.')\n        exp_ids = comma_separated_exp_ids.split(',')\n        validation_error_messages = (\n            story_services.validate_explorations_for_story(exp_ids, False))\n        self.values.update({\n            'validation_error_messages': validation_error_messages\n        })\n        self.render_json(self.values)\n\n\nclass StoryUrlFragmentHandler(base.BaseHandler):\n    \"\"\"A data handler for checking if a story with given url fragment exists.\n    \"\"\"\n\n    GET_HANDLER_ERROR_RETURN_TYPE = feconf.HANDLER_TYPE_JSON\n    URL_PATH_ARGS_SCHEMAS = {\n        'story_url_fragment': constants.SCHEMA_FOR_STORY_URL_FRAGMENTS\n    }\n    HANDLER_ARGS_SCHEMAS = {\n        'GET': {}\n    }\n\n    @acl_decorators.open_access\n    def get(self, story_url_fragment):\n        \"\"\"Handler that receives a story url fragment and checks whether\n        a story with the same url fragment exists or not.\n        \"\"\"\n        self.values.update({\n            'story_url_fragment_exists': (\n                story_services.does_story_exist_with_url_fragment(\n                    story_url_fragment))\n        })\n        self.render_json(self.values)\n", "path": "core/controllers/story_editor.py"}]}