Repository: oppia/oppia

Issue:
When creating story there is no limit on the Description length that can lead to server error
When creating story from the topic editor there is no limit on the Description length, if the added description is to long it leads to server error.
![image](https://user-images.githubusercontent.com/10142938/104477841-df663a80-55c1-11eb-86e2-9bb9079285d5.png)
![image](https://user-images.githubusercontent.com/10142938/104477974-091f6180-55c2-11eb-80c2-2a8fa8fe5f2b.png)
![image](https://user-images.githubusercontent.com/10142938/104477995-0e7cac00-55c2-11eb-9d61-214ab09f50f7.png)

When creating story there is no limit on the Description length that can lead to server error
When creating story from the topic editor there is no limit on the Description length, if the added description is to long it leads to server error.
![image](https://user-images.githubusercontent.com/10142938/104477841-df663a80-55c1-11eb-86e2-9bb9079285d5.png)
![image](https://user-images.githubusercontent.com/10142938/104477974-091f6180-55c2-11eb-80c2-2a8fa8fe5f2b.png)
![image](https://user-images.githubusercontent.com/10142938/104477995-0e7cac00-55c2-11eb-9d61-214ab09f50f7.png)



Discussion:

Resolving patch:
diff --git a/core/domain/story_jobs_one_off.py b/core/domain/story_jobs_one_off.py
--- a/core/domain/story_jobs_one_off.py
+++ b/core/domain/story_jobs_one_off.py
@@ -32,6 +32,23 @@
 (story_models,) = models.Registry.import_models([models.NAMES.story])
 
 
+class DescriptionLengthAuditOneOffJob(jobs.BaseMapReduceOneOffJobManager):
+    """Job that audits and validates description length"""
+
+    @classmethod
+    def entity_classes_to_map_over(cls):
+        return [story_models.StoryModel]
+
+    @staticmethod
+    def map(model_instance):
+        if len(model_instance.description) > 1000:
+            yield (model_instance.corresponding_topic_id, model_instance.id)
+
+    @staticmethod
+    def reduce(key, values):
+        yield ('Topic Id: %s' % key, 'Story Id: %s' % values)
+
+
 class StoryMigrationOneOffJob(jobs.BaseMapReduceOneOffJobManager):
     """A reusable one-time job that may be used to migrate story schema
     versions. This job will load all existing story from the data store
diff --git a/core/jobs_registry.py b/core/jobs_registry.py
--- a/core/jobs_registry.py
+++ b/core/jobs_registry.py
@@ -106,6 +106,7 @@
     stats_jobs_one_off.StatisticsAudit,
     stats_jobs_one_off.StatisticsCustomizationArgsAudit,
     stats_jobs_one_off.WipeExplorationIssuesOneOffJob,
+    story_jobs_one_off.DescriptionLengthAuditOneOffJob,
     story_jobs_one_off.RegenerateStorySummaryOneOffJob,
     story_jobs_one_off.StoryMigrationOneOffJob,
     suggestion_jobs_one_off.QuestionSuggestionMigrationJobManager,
diff --git a/core/storage/story/gae_models.py b/core/storage/story/gae_models.py
--- a/core/storage/story/gae_models.py
+++ b/core/storage/story/gae_models.py
@@ -59,7 +59,7 @@ class StoryModel(base_models.VersionedModel):
     # The thumbnail background color of the story.
     thumbnail_bg_color = datastore_services.StringProperty(indexed=True)
     # A high-level description of the story.
-    description = datastore_services.StringProperty(indexed=True)
+    description = datastore_services.TextProperty(indexed=False)
     # A set of notes, that describe the characters, main storyline, and setting.
     notes = datastore_services.TextProperty(indexed=False)
     # The ISO 639-1 code for the language this story is written in.
@@ -219,7 +219,7 @@ class StorySummaryModel(base_models.BaseModel):
     language_code = (
         datastore_services.StringProperty(required=True, indexed=True))
     # A high-level description of the story.
-    description = datastore_services.StringProperty(required=True, indexed=True)
+    description = datastore_services.TextProperty(required=True, indexed=False)
     # Time when the story model was last updated (not to be
     # confused with last_updated, which is the time when the
     # story *summary* model was last updated).


Test patch:
diff --git a/core/domain/story_jobs_one_off_test.py b/core/domain/story_jobs_one_off_test.py
--- a/core/domain/story_jobs_one_off_test.py
+++ b/core/domain/story_jobs_one_off_test.py
@@ -33,6 +33,63 @@
 (story_models,) = models.Registry.import_models([models.NAMES.story])
 
 
+class DescriptionLengthAuditOneOffJobTests(test_utils.GenericTestBase):
+    """Tests for the one-off description length limit job."""
+
+    ALBERT_EMAIL = 'albert@example.com'
+    ALBERT_NAME = 'albert'
+
+    # Description with length more than 1000.
+    STORY_ID = 'story_id'
+    DESCRIPTION = 'a' * 1001
+
+    def setUp(self):
+        super(DescriptionLengthAuditOneOffJobTests, self).setUp()
+
+        # Setup user who will own the test story.
+        self.signup(self.ALBERT_EMAIL, self.ALBERT_NAME)
+        self.albert_id = self.get_user_id_from_email(self.ALBERT_EMAIL)
+        self.TOPIC_ID = 'topic_id'
+        self.story_id_1 = 'story_id_1'
+        self.story_id_2 = 'story_id_2'
+        self.story_id_3 = 'story_id_3'
+        self.skill_id_1 = 'skill_id_1'
+        self.skill_id_2 = 'skill_id_2'
+        self.save_new_topic(
+            self.TOPIC_ID, self.albert_id, name='Name',
+            description='Description',
+            canonical_story_ids=[self.story_id_1, self.story_id_2],
+            additional_story_ids=[self.story_id_3],
+            uncategorized_skill_ids=[self.skill_id_1, self.skill_id_2],
+            subtopics=[], next_subtopic_id=1)
+        self.process_and_flush_pending_mapreduce_tasks()
+
+    def _run_one_off_job(self):
+        """Runs the one-off MapReduce job."""
+        job_id = (
+            story_jobs_one_off.DescriptionLengthAuditOneOffJob.create_new())
+        story_jobs_one_off.DescriptionLengthAuditOneOffJob.enqueue(job_id)
+        self.process_and_flush_pending_mapreduce_tasks()
+        return story_jobs_one_off.DescriptionLengthAuditOneOffJob.get_output(
+            job_id)
+
+    def test_description_length_limit(self):
+        """Checks description length."""
+
+        # Creating a new story.
+        story = story_domain.Story.create_default_story(
+            self.STORY_ID, 'A title', self.DESCRIPTION, self.TOPIC_ID,
+            'title-one')
+        story_services.save_new_story(self.albert_id, story)
+        topic_services.add_canonical_story(
+            self.albert_id, self.TOPIC_ID, story.id)
+
+        output = self._run_one_off_job()
+        self.assertEqual(
+            [u'[u\'Topic Id: topic_id\', u"Story Id: [\'story_id\']"]'], output
+        )
+
+
 class StoryMigrationOneOffJobTests(test_utils.GenericTestBase):
 
     ALBERT_EMAIL = 'albert@example.com'