Repository: oppia/oppia

Issue:
Wrap up remaining review comments from PR #9087
This issue tracks wrapping up the [remaining comments](https://github.com/oppia/oppia/pull/9087#pullrequestreview-395727819) by @DubeySandeep on https://github.com/oppia/oppia/pull/9087.
Wrap up remaining review comments from PR #9087
This issue tracks wrapping up the [remaining comments](https://github.com/oppia/oppia/pull/9087#pullrequestreview-395727819) by @DubeySandeep on https://github.com/oppia/oppia/pull/9087.
Wrap up remaining review comments from PR #9087
This issue tracks wrapping up the [remaining comments](https://github.com/oppia/oppia/pull/9087#pullrequestreview-395727819) by @DubeySandeep on https://github.com/oppia/oppia/pull/9087.


Discussion:

Resolving patch:
diff --git a/core/controllers/topics_and_skills_dashboard.py b/core/controllers/topics_and_skills_dashboard.py
--- a/core/controllers/topics_and_skills_dashboard.py
+++ b/core/controllers/topics_and_skills_dashboard.py
@@ -117,7 +117,11 @@ def post(self):
         """Handles POST requests."""
         name = self.payload.get('name')
         abbreviated_name = self.payload.get('abbreviated_name')
-        topic_domain.Topic.require_valid_name(name)
+        try:
+            topic_domain.Topic.require_valid_name(name)
+        except:
+            raise self.InvalidInputException(
+                'Invalid topic name, received %s.' % name)
         new_topic_id = topic_services.get_new_topic_id()
         topic = topic_domain.Topic.create_default_topic(
             new_topic_id, name, abbreviated_name)
diff --git a/core/domain/skill_domain.py b/core/domain/skill_domain.py
--- a/core/domain/skill_domain.py
+++ b/core/domain/skill_domain.py
@@ -582,12 +582,12 @@ def require_valid_description(cls, description):
         if description == '':
             raise utils.ValidationError('Description field should not be empty')
 
-        description_limit = (
+        description_length_limit = (
             android_validation_constants.MAX_CHARS_IN_SKILL_DESCRIPTION)
-        if len(description) > description_limit:
+        if len(description) > description_length_limit:
             raise utils.ValidationError(
                 'Skill description should be less than %d chars, received %s'
-                % (description_limit, description))
+                % (description_length_limit, description))
 
     def validate(self):
         """Validates various properties of the Skill object.
diff --git a/core/domain/story_services.py b/core/domain/story_services.py
--- a/core/domain/story_services.py
+++ b/core/domain/story_services.py
@@ -344,23 +344,34 @@ def _save_story(committer_id, story, commit_message, change_list):
 
     topic = topic_fetchers.get_topic_by_id(
         story.corresponding_topic_id, strict=False)
+    if topic is None:
+        raise utils.ValidationError(
+            'Expected story to only belong to a valid topic, but found no '
+            'topic with ID: %s' % story.corresponding_topic_id)
 
     story_is_published = False
-    if topic is not None:
-        for story_reference in topic.get_all_story_references():
-            if story_reference.story_id == story.id:
-                story_is_published = story_reference.story_is_published
+    story_is_present_in_topic = False
+    for story_reference in topic.get_all_story_references():
+        if story_reference.story_id == story.id:
+            story_is_present_in_topic = True
+            story_is_published = story_reference.story_is_published
+    if not story_is_present_in_topic:
+        raise Exception(
+            'Expected story to belong to the topic %s, but it is '
+            'neither a part of the canonical stories or the additional '
+            'stories of the topic.' % story.corresponding_topic_id)
 
     story.validate()
 
     if story_is_published:
+        exp_ids = []
         for node in story.story_contents.nodes:
             if not node.exploration_id:
                 raise Exception(
                     'Story node with id %s does not contain an '
                     'exploration id.' % node.id)
-        exp_ids = [
-            node.exploration_id for node in story.story_contents.nodes]
+            exp_ids.append(node.exploration_id)
+
         validate_explorations_for_story(exp_ids, True)
 
     # Story model cannot be None as story is passed as parameter here and that
@@ -379,21 +390,6 @@ def _save_story(committer_id, story, commit_message, change_list):
             'which is too old. Please reload the page and try again.'
             % (story_model.version, story.version))
 
-    topic = topic_fetchers.get_topic_by_id(
-        story.corresponding_topic_id, strict=False)
-    if topic is None:
-        raise utils.ValidationError(
-            'Expected story to only belong to a valid topic, but found an '
-            'topic with ID: %s' % story.corresponding_topic_id)
-
-    canonical_story_ids = topic.get_canonical_story_ids()
-    additional_story_ids = topic.get_additional_story_ids()
-    if story.id not in canonical_story_ids + additional_story_ids:
-        raise Exception(
-            'Expected story to belong to the topic %s, but it is '
-            'neither a part of the canonical stories or the additional stories '
-            'of the topic.' % story.corresponding_topic_id)
-
     story_model.description = story.description
     story_model.title = story.title
     story_model.notes = story.notes


Test patch:
diff --git a/core/controllers/topics_and_skills_dashboard_test.py b/core/controllers/topics_and_skills_dashboard_test.py
--- a/core/controllers/topics_and_skills_dashboard_test.py
+++ b/core/controllers/topics_and_skills_dashboard_test.py
@@ -166,6 +166,17 @@ def test_topic_creation(self):
             topic_fetchers.get_topic_by_id(topic_id, strict=False))
         self.logout()
 
+    def test_topic_creation_with_invalid_name(self):
+        self.login(self.ADMIN_EMAIL)
+        csrf_token = self.get_new_csrf_token()
+        payload = {
+            'name': 'Topic name that is too long for validation.',
+            'abbreviated_name': 'name'
+        }
+        self.post_json(
+            self.url, payload, csrf_token=csrf_token, expected_status_int=400)
+        self.logout()
+
 
 class NewSkillHandlerTests(BaseTopicsAndSkillsDashboardTests):
 
diff --git a/core/domain/android_validation_constants_test.py b/core/domain/android_validation_constants_test.py
new file mode 100644
--- /dev/null
+++ b/core/domain/android_validation_constants_test.py
@@ -0,0 +1,48 @@
+# Copyright 2020 The Oppia Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS-IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Tests that the corresponding constants in android_validation_constants and
+constants are the same.
+"""
+
+from __future__ import absolute_import  # pylint: disable=import-only-modules
+from __future__ import unicode_literals  # pylint: disable=import-only-modules
+
+from constants import constants
+from core.domain import android_validation_constants
+from core.tests import test_utils
+
+
+class AndroidValidationConstantsTest(test_utils.GenericTestBase):
+    """Tests verifying the character limits."""
+
+    def test_that_character_limits_in_both_files_are_equal(self):
+        self.assertEqual(
+            android_validation_constants.MAX_CHARS_IN_TOPIC_NAME,
+            constants.MAX_CHARS_IN_TOPIC_NAME)
+        self.assertEqual(
+            android_validation_constants.MAX_CHARS_IN_TOPIC_DESCRIPTION,
+            constants.MAX_CHARS_IN_TOPIC_DESCRIPTION)
+        self.assertEqual(
+            android_validation_constants.MAX_CHARS_IN_SUBTOPIC_TITLE,
+            constants.MAX_CHARS_IN_SUBTOPIC_TITLE)
+        self.assertEqual(
+            android_validation_constants.MAX_CHARS_IN_SKILL_DESCRIPTION,
+            constants.MAX_CHARS_IN_SKILL_DESCRIPTION)
+        self.assertEqual(
+            android_validation_constants.MAX_CHARS_IN_STORY_TITLE,
+            constants.MAX_CHARS_IN_STORY_TITLE)
+        self.assertEqual(
+            android_validation_constants.MAX_CHARS_IN_CHAPTER_TITLE,
+            constants.MAX_CHARS_IN_CHAPTER_TITLE)
diff --git a/core/domain/story_domain_test.py b/core/domain/story_domain_test.py
--- a/core/domain/story_domain_test.py
+++ b/core/domain/story_domain_test.py
@@ -902,11 +902,12 @@ def test_story_contents_export_import(self):
         self.assertEqual(
             story_contents_from_dict.to_dict(), story_contents_dict)
 
-    def test_validate_exploration_id(self):
+    def test_validate_non_str_exploration_id(self):
         self.story.story_contents.nodes[0].exploration_id = 1
         self._assert_validation_error(
             'Expected exploration ID to be a string')
 
+    def test_validate_empty_exploration_id(self):
         self.story.story_contents.nodes[0].exploration_id = ''
         self._assert_validation_error(
             'Expected exploration ID to not be an empty string')
diff --git a/core/domain/story_services_test.py b/core/domain/story_services_test.py
--- a/core/domain/story_services_test.py
+++ b/core/domain/story_services_test.py
@@ -238,7 +238,7 @@ def test_update_story_with_invalid_corresponding_topic_id_value(self):
 
         with self.assertRaisesRegexp(
             Exception, ('Expected story to only belong to a valid topic, but '
-                        'found an topic with ID: %s' % topic_id)):
+                        'found no topic with ID: %s' % topic_id)):
             story_services.update_story(
                 self.USER_ID, story_id, changelist, 'Added node.')