Repository: oppia/oppia

Issue:
Invalid URL in practice-tab of topic editor preview
<!--
  - Thanks for taking the time to report a bug in the Oppia project.
  - Before filing a new issue, please do a quick search to check that it hasn't
  - already been filed on the [issue tracker](https://github.com/oppia/oppia/issues)._
  -->

**Describe the bug**
The practice-tab of the topic editor preview throws an invalid URL error

**To Reproduce**
Steps to reproduce the behavior:
1. Create a new topic, with subtopic and skill assigned to it
2. Go to preview tab
3. Go to practice tab
4. Select subtopic and press start

**Observed behavior**
Invalid url error is thrown

**Expected behavior**
A preview should have been shown

**Screenshots**
If applicable, add screenshots to help explain your problem.
![Screenshot from 2020-08-15 08-52-47](https://user-images.githubusercontent.com/30462390/90304434-55605b80-ded5-11ea-93e8-43dd48ce86f8.png)


**Desktop (please complete the following information; delete this section if the issue does not arise on desktop):**
 - OS: Linux
 - Browser Chrome
 - Version 84


**Additional context**
Add any other context about the problem here.

Invalid URL in practice-tab of topic editor preview
<!--
  - Thanks for taking the time to report a bug in the Oppia project.
  - Before filing a new issue, please do a quick search to check that it hasn't
  - already been filed on the [issue tracker](https://github.com/oppia/oppia/issues)._
  -->

**Describe the bug**
The practice-tab of the topic editor preview throws an invalid URL error

**To Reproduce**
Steps to reproduce the behavior:
1. Create a new topic, with subtopic and skill assigned to it
2. Go to preview tab
3. Go to practice tab
4. Select subtopic and press start

**Observed behavior**
Invalid url error is thrown

**Expected behavior**
A preview should have been shown

**Screenshots**
If applicable, add screenshots to help explain your problem.
![Screenshot from 2020-08-15 08-52-47](https://user-images.githubusercontent.com/30462390/90304434-55605b80-ded5-11ea-93e8-43dd48ce86f8.png)


**Desktop (please complete the following information; delete this section if the issue does not arise on desktop):**
 - OS: Linux
 - Browser Chrome
 - Version 84


**Additional context**
Add any other context about the problem here.



Discussion:
Hi @orthodoxparadox, changelog labels should not be used on issues. I’m removing the label. You can learn more about labels <a href="https://github.com/oppia/oppia/wiki/Contributing-code-to-Oppia#labeling-issues-and-pull-requests">here</a>. Thanks!
@kevintab95 Could you take a look at this? Since I think you were working on the urls recently! Thanks!
@Showtim3 would you be able to take a look at this since it is related to editor preview? 
Taking this up..
Hi @orthodoxparadox, changelog labels should not be used on issues. I’m removing the label. You can learn more about labels <a href="https://github.com/oppia/oppia/wiki/Contributing-code-to-Oppia#labeling-issues-and-pull-requests">here</a>. Thanks!
@kevintab95 Could you take a look at this? Since I think you were working on the urls recently! Thanks!
@Showtim3 would you be able to take a look at this since it is related to editor preview? 
Taking this up..

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
@@ -1025,6 +1025,11 @@ def validate(self, strict=False):
                         'Subtopic with title %s does not have any skills '
                         'linked.' % subtopic.title)
 
+        if strict:
+            if len(self.subtopics) == 0:
+                raise utils.ValidationError(
+                    'Topic should have at least 1 subtopic.')
+
         if not self.are_subtopic_url_fragments_unique():
             raise utils.ValidationError(
                 'Subtopic url fragments are not unique across '


Test patch:
diff --git a/core/controllers/acl_decorators_test.py b/core/controllers/acl_decorators_test.py
--- a/core/controllers/acl_decorators_test.py
+++ b/core/controllers/acl_decorators_test.py
@@ -2186,11 +2186,15 @@ def setUp(self):
         self.save_new_story(
             self.story_id, self.admin_id, self.topic_id,
             url_fragment=self.story_url_fragment)
+        subtopic_1 = topic_domain.Subtopic.create_default_subtopic(
+            1, 'Subtopic Title 1')
+        subtopic_1.skill_ids = ['skill_id_1']
+        subtopic_1.url_fragment = 'sub-one-frag'
         self.save_new_topic(
             self.topic_id, self.admin_id, name='Name',
             description='Description', canonical_story_ids=[self.story_id],
             additional_story_ids=[], uncategorized_skill_ids=[],
-            subtopics=[], next_subtopic_id=1)
+            subtopics=[subtopic_1], next_subtopic_id=2)
 
     def test_cannot_access_non_existent_story(self):
         with self.swap(self, 'testapp', self.mock_testapp):
@@ -2463,11 +2467,15 @@ def setUp(self):
         ))
 
         self.topic_id = topic_services.get_new_topic_id()
+        subtopic_1 = topic_domain.Subtopic.create_default_subtopic(
+            1, 'Subtopic Title 1')
+        subtopic_1.skill_ids = ['skill_id_1']
+        subtopic_1.url_fragment = 'sub-one-frag'
         self.save_new_topic(
             self.topic_id, self.admin_id, name='Name',
             description='Description', canonical_story_ids=[],
             additional_story_ids=[], uncategorized_skill_ids=[],
-            subtopics=[], next_subtopic_id=1)
+            subtopics=[subtopic_1], next_subtopic_id=2)
 
     def test_cannot_access_non_existent_topic(self):
         with self.swap(self, 'testapp', self.mock_testapp):
diff --git a/core/controllers/admin_test.py b/core/controllers/admin_test.py
--- a/core/controllers/admin_test.py
+++ b/core/controllers/admin_test.py
@@ -353,6 +353,12 @@ def test_regenerate_topic_related_opportunities_action(self):
             topic_id, 'topic', 'abbrev', 'description')
         topic.thumbnail_filename = 'thumbnail.svg'
         topic.thumbnail_bg_color = '#C6DCDA'
+        topic.subtopics = [
+            topic_domain.Subtopic(
+                1, 'Title', ['skill_id_1'], 'image.svg',
+                constants.ALLOWED_THUMBNAIL_BG_COLORS['subtopic'][0],
+                'dummy-subtopic-three')]
+        topic.next_subtopic_id = 2
         topic_services.save_new_topic(owner_id, topic)
         topic_services.publish_topic(topic_id, self.admin_id)
 
diff --git a/core/controllers/classroom_test.py b/core/controllers/classroom_test.py
--- a/core/controllers/classroom_test.py
+++ b/core/controllers/classroom_test.py
@@ -68,6 +68,13 @@ def test_get(self):
         public_topic.thumbnail_filename = 'Topic.svg'
         public_topic.thumbnail_bg_color = (
             constants.ALLOWED_THUMBNAIL_BG_COLORS['topic'][0])
+        public_topic.subtopics = [
+            topic_domain.Subtopic(
+                1, 'Title', ['skill_id_1', 'skill_id_2', 'skill_id_3'],
+                'image.svg',
+                constants.ALLOWED_THUMBNAIL_BG_COLORS['subtopic'][0],
+                'dummy-subtopic-three')]
+        public_topic.next_subtopic_id = 2
         topic_services.save_new_topic(admin_id, public_topic)
         topic_services.publish_topic(topic_id_2, admin_id)
 
diff --git a/core/controllers/contributor_dashboard_test.py b/core/controllers/contributor_dashboard_test.py
--- a/core/controllers/contributor_dashboard_test.py
+++ b/core/controllers/contributor_dashboard_test.py
@@ -17,6 +17,7 @@
 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 config_services
 from core.domain import exp_domain
 from core.domain import exp_services
@@ -73,6 +74,12 @@ def setUp(self):
             '0', 'topic', 'abbrev', 'description')
         topic.thumbnail_filename = 'thumbnail.svg'
         topic.thumbnail_bg_color = '#C6DCDA'
+        topic.subtopics = [
+            topic_domain.Subtopic(
+                1, 'Title', ['skill_id_3'], 'image.svg',
+                constants.ALLOWED_THUMBNAIL_BG_COLORS['subtopic'][0],
+                'dummy-subtopic-three')]
+        topic.next_subtopic_id = 2
         topic_services.save_new_topic(self.owner_id, topic)
         topic_services.publish_topic('0', self.admin_id)
 
@@ -357,6 +364,12 @@ def setUp(self):
             '0', 'topic', 'abbrev', 'description')
         topic.thumbnail_filename = 'thumbnail.svg'
         topic.thumbnail_bg_color = '#C6DCDA'
+        topic.subtopics = [
+            topic_domain.Subtopic(
+                1, 'Title', ['skill_id_1'], 'image.svg',
+                constants.ALLOWED_THUMBNAIL_BG_COLORS['subtopic'][0],
+                'dummy-subtopic-three')]
+        topic.next_subtopic_id = 2
         topic_services.save_new_topic(self.owner_id, topic)
         topic_services.publish_topic(topic.id, self.admin_id)
 
diff --git a/core/controllers/review_tests_test.py b/core/controllers/review_tests_test.py
--- a/core/controllers/review_tests_test.py
+++ b/core/controllers/review_tests_test.py
@@ -20,6 +20,7 @@
 from constants import constants
 from core.domain import story_domain
 from core.domain import story_services
+from core.domain import topic_domain
 from core.domain import topic_services
 from core.domain import user_services
 from core.tests import test_utils
@@ -91,12 +92,16 @@ def setUp(self):
             self.story_id_2, 'Private Story Title', 'Description',
             self.topic_id, self.story_url_fragment_2)
         story_services.save_new_story(self.admin_id, self.story_2)
+        subtopic_1 = topic_domain.Subtopic.create_default_subtopic(
+            1, 'Subtopic Title 1')
+        subtopic_1.skill_ids = ['skill_id_1']
+        subtopic_1.url_fragment = 'sub-one-frag'
         self.save_new_topic(
             self.topic_id, 'user', name='Topic',
             description='A new topic',
             canonical_story_ids=[self.story_id_1, self.story_id_3],
             additional_story_ids=[], uncategorized_skill_ids=[],
-            subtopics=[], next_subtopic_id=0)
+            subtopics=[subtopic_1], next_subtopic_id=2)
         topic_services.publish_topic(self.topic_id, self.admin_id)
         topic_services.publish_story(
             self.topic_id, self.story_id_1, self.admin_id)
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
@@ -22,6 +22,7 @@
 from core.domain import story_domain
 from core.domain import story_services
 from core.domain import summary_services
+from core.domain import topic_domain
 from core.domain import topic_services
 from core.domain import user_services
 from core.tests import test_utils
@@ -129,11 +130,15 @@ def setUp(self):
         story.story_contents.initial_node_id = 'node_2'
         story.story_contents.next_node_id = 'node_4'
         story_services.save_new_story(self.admin_id, story)
+        subtopic_1 = topic_domain.Subtopic.create_default_subtopic(
+            1, 'Subtopic Title 1')
+        subtopic_1.skill_ids = ['skill_id_1']
+        subtopic_1.url_fragment = 'sub-one-frag'
         self.save_new_topic(
             self.TOPIC_ID, 'user', name='Topic',
             description='A new topic', canonical_story_ids=[story.id],
             additional_story_ids=[], uncategorized_skill_ids=[],
-            subtopics=[], next_subtopic_id=0)
+            subtopics=[subtopic_1], next_subtopic_id=2)
         topic_services.publish_topic(self.TOPIC_ID, self.admin_id)
         topic_services.publish_story(
             self.TOPIC_ID, self.STORY_ID, self.admin_id)
diff --git a/core/controllers/suggestion_test.py b/core/controllers/suggestion_test.py
--- a/core/controllers/suggestion_test.py
+++ b/core/controllers/suggestion_test.py
@@ -1114,6 +1114,12 @@ def setUp(self):
             self.TOPIC_ID, 'topic', 'abbrev', 'description')
         topic.thumbnail_filename = 'thumbnail.svg'
         topic.thumbnail_bg_color = '#C6DCDA'
+        topic.subtopics = [
+            topic_domain.Subtopic(
+                1, 'Title', ['skill_id_3'], 'image.svg',
+                constants.ALLOWED_THUMBNAIL_BG_COLORS['subtopic'][0],
+                'dummy-subtopic-three')]
+        topic.next_subtopic_id = 2
         topic_services.save_new_topic(self.owner_id, topic)
         topic_services.publish_topic(self.TOPIC_ID, self.admin_id)
 
@@ -1297,6 +1303,12 @@ def setUp(self):
             self.TOPIC_ID, 'topic', 'abbrev', 'description')
         topic.thumbnail_filename = 'thumbnail.svg'
         topic.thumbnail_bg_color = '#C6DCDA'
+        topic.subtopics = [
+            topic_domain.Subtopic(
+                1, 'Title', ['skill_id_3'], 'image.svg',
+                constants.ALLOWED_THUMBNAIL_BG_COLORS['subtopic'][0],
+                'dummy-subtopic-three')]
+        topic.next_subtopic_id = 2
         topic_services.save_new_topic(self.owner_id, topic)
         topic_services.publish_topic(self.TOPIC_ID, self.admin_id)
 
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
@@ -266,12 +266,19 @@ def test_get_with_five_or_more_questions(self):
         number_of_questions = 6
         self.topic_id = 'new_topic'
         self.skill_id_1 = skill_services.get_new_skill_id()
+        self.skill_id_2 = skill_services.get_new_skill_id()
         self.topic = topic_domain.Topic.create_default_topic(
             self.topic_id, 'new_topic', 'new-topic', 'description')
         self.topic.uncategorized_skill_ids.append(self.skill_id_1)
         self.topic.thumbnail_filename = 'Image.svg'
         self.topic.thumbnail_bg_color = (
             constants.ALLOWED_THUMBNAIL_BG_COLORS['topic'][0])
+        subtopic_1 = topic_domain.Subtopic.create_default_subtopic(
+            1, 'Subtopic Title 1')
+        subtopic_1.skill_ids = [self.skill_id_2]
+        subtopic_1.url_fragment = 'sub-one-frag'
+        self.topic.subtopics = [subtopic_1]
+        self.topic.next_subtopic_id = 2
         topic_services.save_new_topic(self.admin_id, self.topic)
         topic_services.publish_topic(self.topic_id, self.admin_id)
         self.save_new_skill(
@@ -291,12 +298,14 @@ def test_get_with_five_or_more_questions(self):
             'canonical_story_dicts': [],
             'additional_story_dicts': [],
             'uncategorized_skill_ids': [self.skill_id_1],
-            'subtopics': [],
+            'subtopics': [subtopic_1.to_dict()],
             'degrees_of_mastery': {
-                self.skill_id_1: None
+                self.skill_id_1: None,
+                self.skill_id_2: None
             },
             'skill_descriptions': {
-                self.skill_id_1: 'Skill Description 1'
+                self.skill_id_1: 'Skill Description 1',
+                self.skill_id_2: None
             },
             'train_tab_should_be_displayed': True
         }
@@ -307,12 +316,19 @@ def test_get_with_twenty_or_more_questions(self):
         number_of_questions = 50
         self.topic_id = 'new_topic'
         self.skill_id_1 = skill_services.get_new_skill_id()
+        self.skill_id_2 = skill_services.get_new_skill_id()
         self.topic = topic_domain.Topic.create_default_topic(
             self.topic_id, 'new_topic', 'new-topic', 'description')
         self.topic.uncategorized_skill_ids.append(self.skill_id_1)
         self.topic.thumbnail_filename = 'Image.svg'
         self.topic.thumbnail_bg_color = (
             constants.ALLOWED_THUMBNAIL_BG_COLORS['topic'][0])
+        subtopic_1 = topic_domain.Subtopic.create_default_subtopic(
+            1, 'Subtopic Title 1')
+        subtopic_1.skill_ids = [self.skill_id_2]
+        subtopic_1.url_fragment = 'sub-one-frag'
+        self.topic.subtopics = [subtopic_1]
+        self.topic.next_subtopic_id = 2
         topic_services.save_new_topic(self.admin_id, self.topic)
         topic_services.publish_topic(self.topic_id, self.admin_id)
         self.save_new_skill(
@@ -332,12 +348,14 @@ def test_get_with_twenty_or_more_questions(self):
             'canonical_story_dicts': [],
             'additional_story_dicts': [],
             'uncategorized_skill_ids': [self.skill_id_1],
-            'subtopics': [],
+            'subtopics': [subtopic_1.to_dict()],
             'degrees_of_mastery': {
-                self.skill_id_1: None
+                self.skill_id_1: None,
+                self.skill_id_2: None
             },
             'skill_descriptions': {
-                self.skill_id_1: 'Skill Description 1'
+                self.skill_id_1: 'Skill Description 1',
+                self.skill_id_2: None
             },
             'train_tab_should_be_displayed': True
         }
@@ -358,6 +376,12 @@ def test_get_with_twenty_or_more_questions_with_multiple_skills(self):
         self.topic.thumbnail_filename = 'Image.svg'
         self.topic.thumbnail_bg_color = (
             constants.ALLOWED_THUMBNAIL_BG_COLORS['topic'][0])
+        subtopic_1 = topic_domain.Subtopic.create_default_subtopic(
+            1, 'Subtopic Title 1')
+        subtopic_1.skill_ids = ['skill_id_1']
+        subtopic_1.url_fragment = 'sub-one-frag'
+        self.topic.subtopics = [subtopic_1]
+        self.topic.next_subtopic_id = 2
         topic_services.save_new_topic(self.admin_id, self.topic)
         topic_services.publish_topic(self.topic_id, self.admin_id)
         for i in python_utils.RANGE(number_of_skills):
@@ -400,6 +424,12 @@ def test_get_with_lesser_questions_with_fifty_or_more_skills(self):
         self.topic.thumbnail_filename = 'Image.svg'
         self.topic.thumbnail_bg_color = (
             constants.ALLOWED_THUMBNAIL_BG_COLORS['topic'][0])
+        subtopic_1 = topic_domain.Subtopic.create_default_subtopic(
+            1, 'Subtopic Title 1')
+        subtopic_1.skill_ids = ['skill_id_1']
+        subtopic_1.url_fragment = 'sub-one-frag'
+        self.topic.subtopics = [subtopic_1]
+        self.topic.next_subtopic_id = 2
         topic_services.save_new_topic(self.admin_id, self.topic)
         topic_services.publish_topic(self.topic_id, self.admin_id)
         for i in python_utils.RANGE(number_of_skills):
@@ -444,6 +474,12 @@ def test_get_with_more_questions_with_fifty_or_more_skills(self):
         self.topic.thumbnail_filename = 'Image.svg'
         self.topic.thumbnail_bg_color = (
             constants.ALLOWED_THUMBNAIL_BG_COLORS['topic'][0])
+        subtopic_1 = topic_domain.Subtopic.create_default_subtopic(
+            1, 'Subtopic Title 1')
+        subtopic_1.skill_ids = ['skill_id_1']
+        subtopic_1.url_fragment = 'sub-one-frag'
+        self.topic.subtopics = [subtopic_1]
+        self.topic.next_subtopic_id = 2
         topic_services.save_new_topic(self.admin_id, self.topic)
         topic_services.publish_topic(self.topic_id, self.admin_id)
         for i in python_utils.RANGE(number_of_skills):
diff --git a/core/domain/opportunity_jobs_one_off_test.py b/core/domain/opportunity_jobs_one_off_test.py
--- a/core/domain/opportunity_jobs_one_off_test.py
+++ b/core/domain/opportunity_jobs_one_off_test.py
@@ -21,6 +21,7 @@
 
 import ast
 
+from constants import constants
 from core.domain import caching_services
 from core.domain import exp_domain
 from core.domain import exp_services
@@ -29,6 +30,8 @@
 from core.domain import skill_services
 from core.domain import story_domain
 from core.domain import story_services
+from core.domain import subtopic_page_domain
+from core.domain import subtopic_page_services
 from core.domain import topic_domain
 from core.domain import topic_services
 from core.platform import models
@@ -76,6 +79,23 @@ def setUp(self):
             self.topic_id_1, 'topic', 'abbrev-one', 'description')
         topic_1.thumbnail_filename = 'thumbnail.svg'
         topic_1.thumbnail_bg_color = '#C6DCDA'
+        topic_1.subtopics = [
+            topic_domain.Subtopic(
+                1, 'Title', ['skill_id_1'], 'image.svg',
+                constants.ALLOWED_THUMBNAIL_BG_COLORS['subtopic'][0],
+                'dummy-subtopic-three')]
+        topic_1.next_subtopic_id = 2
+        subtopic_page = (
+            subtopic_page_domain.SubtopicPage.create_default_subtopic_page(
+                1, self.topic_id_1))
+        subtopic_page_services.save_subtopic_page(
+            self.owner_id, subtopic_page, 'Added subtopic',
+            [topic_domain.TopicChange({
+                'cmd': topic_domain.CMD_ADD_SUBTOPIC,
+                'subtopic_id': 1,
+                'title': 'Sample'
+            })]
+        )
         topic_services.save_new_topic(self.owner_id, topic_1)
         topic_services.publish_topic(self.topic_id_1, self.admin_id)
 
@@ -83,6 +103,23 @@ def setUp(self):
             self.topic_id_2, 'topic2', 'abbrev-two', 'description')
         topic_2.thumbnail_filename = 'thumbnail.svg'
         topic_2.thumbnail_bg_color = '#C6DCDA'
+        topic_2.subtopics = [
+            topic_domain.Subtopic(
+                1, 'Title', ['skill_id_2'], 'image.svg',
+                constants.ALLOWED_THUMBNAIL_BG_COLORS['subtopic'][0],
+                'dummy-subtopic-three')]
+        subtopic_page = (
+            subtopic_page_domain.SubtopicPage.create_default_subtopic_page(
+                1, self.topic_id_2))
+        subtopic_page_services.save_subtopic_page(
+            self.owner_id, subtopic_page, 'Added subtopic',
+            [topic_domain.TopicChange({
+                'cmd': topic_domain.CMD_ADD_SUBTOPIC,
+                'subtopic_id': 1,
+                'title': 'Sample'
+            })]
+        )
+        topic_2.next_subtopic_id = 2
         topic_services.save_new_topic(self.owner_id, topic_2)
         topic_services.publish_topic(self.topic_id_2, self.admin_id)
 
diff --git a/core/domain/opportunity_services_test.py b/core/domain/opportunity_services_test.py
--- a/core/domain/opportunity_services_test.py
+++ b/core/domain/opportunity_services_test.py
@@ -31,6 +31,8 @@
 from core.domain import skill_services
 from core.domain import story_domain
 from core.domain import story_services
+from core.domain import subtopic_page_domain
+from core.domain import subtopic_page_services
 from core.domain import suggestion_services
 from core.domain import topic_domain
 from core.domain import topic_services
@@ -93,6 +95,23 @@ def setUp(self):
             self.TOPIC_ID, 'topic', 'abbrev', 'description')
         topic.thumbnail_filename = 'thumbnail.svg'
         topic.thumbnail_bg_color = '#C6DCDA'
+        topic.subtopics = [
+            topic_domain.Subtopic(
+                1, 'Title', ['skill_id_1'], 'image.svg',
+                constants.ALLOWED_THUMBNAIL_BG_COLORS['subtopic'][0],
+                'dummy-subtopic-url')]
+        topic.next_subtopic_id = 2
+        subtopic_page = (
+            subtopic_page_domain.SubtopicPage.create_default_subtopic_page(
+                1, self.TOPIC_ID))
+        subtopic_page_services.save_subtopic_page(
+            self.owner_id, subtopic_page, 'Added subtopic',
+            [topic_domain.TopicChange({
+                'cmd': topic_domain.CMD_ADD_SUBTOPIC,
+                'subtopic_id': 1,
+                'title': 'Sample'
+            })]
+        )
         topic_services.save_new_topic(self.owner_id, topic)
         topic_services.publish_topic(self.TOPIC_ID, self.admin_id)
 
@@ -741,6 +760,12 @@ def setUp(self):
             self.TOPIC_ID, 'topic', 'abbrev', 'description')
         topic.thumbnail_filename = 'thumbnail.svg'
         topic.thumbnail_bg_color = '#C6DCDA'
+        topic.subtopics = [
+            topic_domain.Subtopic(
+                1, 'Title', ['skill_id_1'], 'image.svg',
+                constants.ALLOWED_THUMBNAIL_BG_COLORS['subtopic'][0],
+                'dummy-subtopic-url')]
+        topic.next_subtopic_id = 2
         topic_services.save_new_topic(self.owner_id, topic)
         topic_services.publish_topic(self.TOPIC_ID, self.admin_id)
 
diff --git a/core/domain/prod_validators_test.py b/core/domain/prod_validators_test.py
--- a/core/domain/prod_validators_test.py
+++ b/core/domain/prod_validators_test.py
@@ -51,6 +51,7 @@
 from core.domain import story_services
 from core.domain import subscription_services
 from core.domain import subtopic_page_domain
+from core.domain import subtopic_page_services
 from core.domain import suggestion_services
 from core.domain import topic_domain
 from core.domain import topic_services
@@ -2125,6 +2126,23 @@ def setUp(self):
             self.TOPIC_ID, 'topic', 'abbrev', 'description')
         topic.thumbnail_filename = 'thumbnail.svg'
         topic.thumbnail_bg_color = '#C6DCDA'
+        topic.subtopics = [
+            topic_domain.Subtopic(
+                1, 'Title', ['skill_id_2'], 'image.svg',
+                constants.ALLOWED_THUMBNAIL_BG_COLORS['subtopic'][0],
+                'dummy-subtopic-three')]
+        subtopic_page = (
+            subtopic_page_domain.SubtopicPage.create_default_subtopic_page(
+                1, self.TOPIC_ID))
+        subtopic_page_services.save_subtopic_page(
+            self.owner_id, subtopic_page, 'Added subtopic',
+            [topic_domain.TopicChange({
+                'cmd': topic_domain.CMD_ADD_SUBTOPIC,
+                'subtopic_id': 1,
+                'title': 'Sample'
+            })]
+        )
+        topic.next_subtopic_id = 2
         topic_services.save_new_topic(self.owner_id, topic)
         topic_services.publish_topic(self.TOPIC_ID, self.admin_id)
 
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
@@ -489,6 +489,10 @@ def test_subtopic_strict_validation(self):
         self._assert_strict_validation_error(
             'Subtopic with title Title does not have any skills linked')
 
+        self.topic.subtopics = []
+        self._assert_strict_validation_error(
+            'Topic should have at least 1 subtopic.')
+
     def test_subtopic_title_validation(self):
         self.topic.subtopics[0].title = 1
         self._assert_validation_error('Expected subtopic title to be a string')
@@ -605,7 +609,7 @@ def test_validation_fails_with_empty_url_fragment(self):
             utils.ValidationError, validation_message):
             self.topic.validate()
 
-    def test_validation_fails_with_lenghty_url_fragment(self):
+    def test_validation_fails_with_lengthy_url_fragment(self):
         self.topic.url_fragment = 'a' * 25
         url_fragment_char_limit = constants.MAX_CHARS_IN_TOPIC_URL_FRAGMENT
         validation_message = (
diff --git a/core/domain/voiceover_services_test.py b/core/domain/voiceover_services_test.py
--- a/core/domain/voiceover_services_test.py
+++ b/core/domain/voiceover_services_test.py
@@ -17,6 +17,7 @@
 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 exp_domain
 from core.domain import exp_services
 from core.domain import opportunity_services
@@ -76,6 +77,12 @@ def setUp(self):
             self.TOPIC_ID, 'topic', 'abbrev', 'description')
         topic.thumbnail_filename = 'thumbnail.svg'
         topic.thumbnail_bg_color = '#C6DCDA'
+        topic.subtopics = [
+            topic_domain.Subtopic(
+                1, 'Title', ['skill_id_1'], 'image.svg',
+                constants.ALLOWED_THUMBNAIL_BG_COLORS['subtopic'][0],
+                'dummy-subtopic-three')]
+        topic.next_subtopic_id = 2
         topic_services.save_new_topic(self.owner_id, topic)
         topic_services.publish_topic(self.TOPIC_ID, self.admin_id)
 
diff --git a/core/tests/protractor_desktop/classroomPageFileUploadFeatures.js b/core/tests/protractor_desktop/classroomPageFileUploadFeatures.js
--- a/core/tests/protractor_desktop/classroomPageFileUploadFeatures.js
+++ b/core/tests/protractor_desktop/classroomPageFileUploadFeatures.js
@@ -78,7 +78,26 @@ describe('Classroom page functionality', function() {
       await classroomPage.get('math');
       await classroomPage.expectNumberOfTopicsToBe(0);
       await topicsAndSkillsDashboardPage.get();
+      (await
+      topicsAndSkillsDashboardPage.createSkillWithDescriptionAndExplanation(
+        'Skill 1', 'Concept card explanation', false));
+      await topicsAndSkillsDashboardPage.get();
+      await topicsAndSkillsDashboardPage.navigateToSkillsTab();
+      await topicsAndSkillsDashboardPage.assignSkillWithIndexToTopic(0, 0);
+      await topicsAndSkillsDashboardPage.get();
       await topicsAndSkillsDashboardPage.navigateToTopicWithIndex(0);
+      await topicEditorPage.addSubtopic(
+        'Subtopic 1', 'subtopic-one', '../data/test2_svg.svg',
+        'Subtopic content');
+      await topicEditorPage.saveTopic('Added subtopic.');
+
+      await topicEditorPage.navigateToTopicEditorTab();
+      await topicEditorPage.navigateToReassignModal();
+
+      await topicEditorPage.dragSkillToSubtopic('Skill 1', 0);
+      await topicEditorPage.saveRearrangedSkills();
+      await topicEditorPage.saveTopic('Added skill to subtopic.');
+
       await topicEditorPage.publishTopic();
       await classroomPage.get('math');
       await classroomPage.expectNumberOfTopicsToBe(1);
diff --git a/core/tests/protractor_utils/TopicEditorPage.js b/core/tests/protractor_utils/TopicEditorPage.js
--- a/core/tests/protractor_utils/TopicEditorPage.js
+++ b/core/tests/protractor_utils/TopicEditorPage.js
@@ -122,6 +122,8 @@ var TopicEditorPage = function() {
   var dragAndDrop = async function(fromElement, toElement) {
     await browser.executeScript(dragAndDropScript, fromElement, toElement);
   };
+  var saveRearrangedSkillsButton = element(
+    by.css('.protractor-save-rearrange-skills'));
 
   this.get = async function(topicId) {
     await browser.get(EDITOR_URL_PREFIX + topicId);
@@ -293,6 +295,11 @@ var TopicEditorPage = function() {
     await dragAndDrop(toMove, target);
   };
 
+  this.saveRearrangedSkills = async function() {
+    await action.click(
+      'Save rearranged skills modal', saveRearrangedSkillsButton);
+  };
+
   this.navigateToReassignModal = async function() {
     await waitFor.elementToBeClickable(reassignSkillButton,
       'Reassign skill button taking too long to be clickable');