Repository: oppia/oppia

Issue:
While creating a new skill from the topic editor, the create new skill modal appears twice.
**To Reproduce**
Steps to reproduce the behavior:
 1. Go to topic editor.
 2. Click on Create Skill.
 3. See that the modal appears twice.
 
While creating a new skill from the topic editor, the create new skill modal appears twice.
**To Reproduce**
Steps to reproduce the behavior:
 1. Go to topic editor.
 2. Click on Create Skill.
 3. See that the modal appears twice.
 


Discussion:

Resolving patch:
diff --git a/core/domain/topic_domain.py b/core/domain/topic_domain.py
--- a/core/domain/topic_domain.py
+++ b/core/domain/topic_domain.py
@@ -59,6 +59,7 @@
 CMD_DELETE_SUBTOPIC = 'delete_subtopic'
 CMD_ADD_CANONICAL_STORY = 'add_canonical_story'
 CMD_DELETE_CANONICAL_STORY = 'delete_canonical_story'
+CMD_REARRANGE_CANONICAL_STORY = 'rearrange_canonical_story'
 CMD_ADD_ADDITIONAL_STORY = 'add_additional_story'
 CMD_DELETE_ADDITIONAL_STORY = 'delete_additional_story'
 CMD_PUBLISH_STORY = 'publish_story'
@@ -135,6 +136,10 @@ class TopicChange(change_domain.BaseChange):
         'name': CMD_DELETE_CANONICAL_STORY,
         'required_attribute_names': ['story_id'],
         'optional_attribute_names': []
+    }, {
+        'name': CMD_REARRANGE_CANONICAL_STORY,
+        'required_attribute_names': ['from_index', 'to_index'],
+        'optional_attribute_names': []
     }, {
         'name': CMD_ADD_ADDITIONAL_STORY,
         'required_attribute_names': ['story_id'],
@@ -708,6 +713,40 @@ def delete_canonical_story(self, story_id):
                 'The story_id %s is not present in the canonical '
                 'story references list of the topic.' % story_id)
 
+    def rearrange_canonical_story(self, from_index, to_index):
+        """Rearranges or moves a canonical story to another position.
+
+        Args:
+            from_index: int. The index of canonical story to move.
+            to_index: int. The index at which to insert the moved canonical
+                story.
+
+        Raises:
+            Exception. Invalid input.
+        """
+        if not isinstance(from_index, int):
+            raise Exception('Expected from_index value to be a number, '
+                            'received %s' % from_index)
+
+        if not isinstance(to_index, int):
+            raise Exception('Expected to_index value to be a number, '
+                            'received %s' % to_index)
+
+        if from_index == to_index:
+            raise Exception('Expected from_index and to_index values '
+                            'to be different.')
+
+        if (from_index >= len(self.canonical_story_references) or
+                to_index >= len(self.canonical_story_references) or
+                from_index < 0 or to_index < 0):
+            raise Exception('Expected index values to be with-in bounds.')
+
+        canonical_story_reference_to_move = copy.deepcopy(
+            self.canonical_story_references[from_index])
+        del self.canonical_story_references[from_index]
+        self.canonical_story_references.insert(
+            to_index, canonical_story_reference_to_move)
+
     def add_canonical_story(self, story_id):
         """Adds a story to the canonical_story_references list.
 
diff --git a/core/domain/topic_services.py b/core/domain/topic_services.py
--- a/core/domain/topic_services.py
+++ b/core/domain/topic_services.py
@@ -282,6 +282,9 @@ def apply_change_list(topic_id, change_list):
                 topic.add_canonical_story(change.story_id)
             elif change.cmd == topic_domain.CMD_DELETE_CANONICAL_STORY:
                 topic.delete_canonical_story(change.story_id)
+            elif change.cmd == topic_domain.CMD_REARRANGE_CANONICAL_STORY:
+                topic.rearrange_canonical_story(
+                    change.from_index, change.to_index)
             elif change.cmd == topic_domain.CMD_ADD_ADDITIONAL_STORY:
                 topic.add_additional_story(change.story_id)
             elif change.cmd == topic_domain.CMD_DELETE_ADDITIONAL_STORY:


Test patch:
diff --git a/core/domain/topic_domain_test.py b/core/domain/topic_domain_test.py
--- a/core/domain/topic_domain_test.py
+++ b/core/domain/topic_domain_test.py
@@ -118,6 +118,84 @@ def test_delete_canonical_story(self):
             ' story references list of the topic.'):
             self.topic.delete_canonical_story('story_id_5')
 
+    def test_rearrange_canonical_story_fail_with_invalid_from_index_value(self):
+        with self.assertRaisesRegexp(
+            Exception, 'Expected from_index value to be a number, '
+                       'received None'):
+            self.topic.rearrange_canonical_story(None, 2)
+
+        with self.assertRaisesRegexp(
+            Exception, 'Expected from_index value to be a number, '
+                       'received a'):
+            self.topic.rearrange_canonical_story('a', 2)
+
+    def test_rearrange_canonical_story_fail_with_invalid_to_index_value(self):
+        with self.assertRaisesRegexp(
+            Exception, 'Expected to_index value to be a number, '
+                       'received None'):
+            self.topic.rearrange_canonical_story(1, None)
+
+        with self.assertRaisesRegexp(
+            Exception, 'Expected to_index value to be a number, '
+                       'received a'):
+            self.topic.rearrange_canonical_story(1, 'a')
+
+    def test_rearrange_canonical_story_fail_with_out_of_bound_indexes(self):
+        with self.assertRaisesRegexp(
+            Exception, 'Expected index values to be with-in bounds.'):
+            self.topic.rearrange_canonical_story(1, 10)
+
+        with self.assertRaisesRegexp(
+            Exception, 'Expected index values to be with-in bounds.'):
+            self.topic.rearrange_canonical_story(-1, 0)
+
+        with self.assertRaisesRegexp(
+            Exception, 'Expected index values to be with-in bounds.'):
+            self.topic.rearrange_canonical_story(10, 0)
+
+        with self.assertRaisesRegexp(
+            Exception, 'Expected index values to be with-in bounds.'):
+            self.topic.rearrange_canonical_story(-1, -10)
+
+    def test_rearrange_canonical_story_fail_with_identical_index_values(self):
+        with self.assertRaisesRegexp(
+            Exception, 'Expected from_index and to_index values to be '
+                       'different.'):
+            self.topic.rearrange_canonical_story(1, 1)
+
+    def test_rearrange_canonical_story(self):
+        self.topic.canonical_story_references = [
+            topic_domain.StoryReference.create_default_story_reference(
+                'story_id_1'),
+            topic_domain.StoryReference.create_default_story_reference(
+                'story_id_2'),
+            topic_domain.StoryReference.create_default_story_reference(
+                'story_id_3')
+        ]
+        canonical_story_ids = self.topic.get_canonical_story_ids()
+
+        self.assertEqual(canonical_story_ids[0], 'story_id_1')
+        self.assertEqual(canonical_story_ids[1], 'story_id_2')
+        self.assertEqual(canonical_story_ids[2], 'story_id_3')
+
+        self.topic.rearrange_canonical_story(1, 0)
+        canonical_story_ids = self.topic.get_canonical_story_ids()
+        self.assertEqual(canonical_story_ids[0], 'story_id_2')
+        self.assertEqual(canonical_story_ids[1], 'story_id_1')
+        self.assertEqual(canonical_story_ids[2], 'story_id_3')
+
+        self.topic.rearrange_canonical_story(2, 1)
+        canonical_story_ids = self.topic.get_canonical_story_ids()
+        self.assertEqual(canonical_story_ids[0], 'story_id_2')
+        self.assertEqual(canonical_story_ids[1], 'story_id_3')
+        self.assertEqual(canonical_story_ids[2], 'story_id_1')
+
+        self.topic.rearrange_canonical_story(2, 0)
+        canonical_story_ids = self.topic.get_canonical_story_ids()
+        self.assertEqual(canonical_story_ids[0], 'story_id_1')
+        self.assertEqual(canonical_story_ids[1], 'story_id_2')
+        self.assertEqual(canonical_story_ids[2], 'story_id_3')
+
     def test_get_all_story_references(self):
         self.topic.canonical_story_references = [
             topic_domain.StoryReference.create_default_story_reference(
diff --git a/core/domain/topic_services_test.py b/core/domain/topic_services_test.py
--- a/core/domain/topic_services_test.py
+++ b/core/domain/topic_services_test.py
@@ -258,6 +258,74 @@ def test_cannot_create_topic_change_class_with_invalid_changelist(self):
                 'new_value': 'New Description'
             })
 
+    def test_cannot_rearrange_story_with_missing_index_values(self):
+        with self.assertRaisesRegexp(
+            Exception, ('The following required attributes are missing: '
+                        'from_index, to_index')):
+            topic_domain.TopicChange({
+                'cmd': topic_domain.CMD_REARRANGE_CANONICAL_STORY,
+            })
+
+    def test_cannot_rearrange_story_with_missing_from_index_value(self):
+        with self.assertRaisesRegexp(
+            Exception, ('The following required attributes are missing: '
+                        'from_index')):
+            topic_domain.TopicChange({
+                'cmd': topic_domain.CMD_REARRANGE_CANONICAL_STORY,
+                'to_index': 1
+            })
+
+    def test_cannot_rearrange_story_with_missing_to_index_value(self):
+        with self.assertRaisesRegexp(
+            Exception, ('The following required attributes are missing: '
+                        'to_index')):
+            topic_domain.TopicChange({
+                'cmd': topic_domain.CMD_REARRANGE_CANONICAL_STORY,
+                'from_index': 1
+            })
+
+    def test_rearrange_canonical_stories_in_topic(self):
+        story_id_new = 'story_id_new'
+        topic_services.add_canonical_story(
+            self.user_id_admin, self.TOPIC_ID, 'story_id_new')
+
+        topic = topic_fetchers.get_topic_by_id(self.TOPIC_ID)
+        self.assertEqual(len(topic.canonical_story_references), 3)
+        self.assertEqual(
+            topic.canonical_story_references[0].story_id, self.story_id_1)
+        self.assertEqual(
+            topic.canonical_story_references[1].story_id, self.story_id_2)
+        self.assertEqual(
+            topic.canonical_story_references[2].story_id, story_id_new)
+
+        changelist = [topic_domain.TopicChange({
+            'cmd': topic_domain.CMD_REARRANGE_CANONICAL_STORY,
+            'from_index': 2,
+            'to_index': 0
+        })]
+
+        topic_services.update_topic_and_subtopic_pages(
+            self.user_id_admin, self.TOPIC_ID, changelist,
+            'Rearranged canonical story on index 2 to index 0.')
+
+        topic = topic_fetchers.get_topic_by_id(self.TOPIC_ID)
+        self.assertEqual(len(topic.canonical_story_references), 3)
+        self.assertEqual(
+            topic.canonical_story_references[0].story_id, story_id_new)
+        self.assertEqual(
+            topic.canonical_story_references[1].story_id, self.story_id_1)
+        self.assertEqual(
+            topic.canonical_story_references[2].story_id, self.story_id_2)
+        topic_commit_log_entry = (
+            topic_models.TopicCommitLogEntryModel.get_commit(self.TOPIC_ID, 4)
+        )
+        self.assertEqual(topic_commit_log_entry.commit_type, 'edit')
+        self.assertEqual(topic_commit_log_entry.topic_id, self.TOPIC_ID)
+        self.assertEqual(topic_commit_log_entry.user_id, self.user_id_admin)
+        self.assertEqual(
+            topic_commit_log_entry.commit_message,
+            'Rearranged canonical story on index 2 to index 0.')
+
     def test_cannot_update_topic_property_with_invalid_changelist(self):
         with self.assertRaisesRegexp(
             Exception, (