Repository: oppia/oppia

Issue:
Contributor dashboard shows opportunities for content that is not part of a classroom topic
<!--
  - 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**
Contributor dashboard shows opportunities for content that is not part of a classroom topic.

**To Reproduce**
Steps to reproduce the behavior:
 1. Create a topic and assign a skill to it.
 2. Provide necessary permissions to submit a question.
 3. Enable "Whether the contributor can suggest questions for skill opportunities." config.
 4. Go to /contributor-dashboard.
 5. Go to the "Submit Question" tab.
 6. Observe that a skill opportunity is shown for the created skill even though it is not attached to a classroom topic. 

**Observed behavior**
Contributor dashboard shows opportunities for content that is not part of a classroom topic.

**Expected behavior**
Contributor dashboard should only show opportunities for content that is part of a classroom topics.


Contributor dashboard shows opportunities for content that is not part of a classroom topic
<!--
  - 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**
Contributor dashboard shows opportunities for content that is not part of a classroom topic.

**To Reproduce**
Steps to reproduce the behavior:
 1. Create a topic and assign a skill to it.
 2. Provide necessary permissions to submit a question.
 3. Enable "Whether the contributor can suggest questions for skill opportunities." config.
 4. Go to /contributor-dashboard.
 5. Go to the "Submit Question" tab.
 6. Observe that a skill opportunity is shown for the created skill even though it is not attached to a classroom topic. 

**Observed behavior**
Contributor dashboard shows opportunities for content that is not part of a classroom topic.

**Expected behavior**
Contributor dashboard should only show opportunities for content that is part of a classroom topics.




Discussion:
This may be WAI: If a topic isn't part of a classroom yet, the creator of the topic might still want practice questions before they release the topic as part of a classroom. 
Ah -- as lesson lead, just confirming that it's not WAI. We want to focus attention on lessons that are part of a classroom, and I confirm that all curated lessons will be part of an existing classroom! (Otherwise they wouldn't be discoverable anyway.)
Gotcha, thanks for clarifying!
@sagangwee Hi, when do you expect to create a PR fixing this?
@vojtechjelinek We should be able to fix this by the May release cut. 
@sagangwee Since this is a blocking bug would it be possible to fix this in few days and include it in this release?
This may be WAI: If a topic isn't part of a classroom yet, the creator of the topic might still want practice questions before they release the topic as part of a classroom. 
Ah -- as lesson lead, just confirming that it's not WAI. We want to focus attention on lessons that are part of a classroom, and I confirm that all curated lessons will be part of an existing classroom! (Otherwise they wouldn't be discoverable anyway.)
Gotcha, thanks for clarifying!
@sagangwee Hi, when do you expect to create a PR fixing this?
@vojtechjelinek We should be able to fix this by the May release cut. 
@sagangwee Since this is a blocking bug would it be possible to fix this in few days and include it in this release?

Resolving patch:
diff --git a/core/controllers/contributor_dashboard.py b/core/controllers/contributor_dashboard.py
--- a/core/controllers/contributor_dashboard.py
+++ b/core/controllers/contributor_dashboard.py
@@ -111,13 +111,22 @@ def _get_skill_opportunities_with_corresponding_topic_name(self, cursor):
                     this batch. If False, there are no further results after
                     this batch.
         """
-        topics_with_skills = topic_fetchers.get_all_topics_with_skills()
+        # We want to focus attention on lessons that are part of a classroom.
+        # See issue #12221.
+        classroom_topic_ids = []
+        for classroom_dict in config_domain.CLASSROOM_PAGES_DATA.value:
+            classroom_topic_ids.extend(classroom_dict['topic_ids'])
+        classroom_topics = topic_fetchers.get_topics_by_ids(classroom_topic_ids)
+        classroom_topics_with_skills = [
+            topic for topic in classroom_topics
+            if topic and topic.get_all_skill_ids()
+        ]
         skill_opportunities, cursor, more = (
             opportunity_services.get_skill_opportunities(cursor))
         id_to_skill_opportunity_dict = {
             opp.id: opp.to_dict() for opp in skill_opportunities}
         opportunities = []
-        for topic in topics_with_skills:
+        for topic in classroom_topics_with_skills:
             for skill_id in topic.get_all_skill_ids():
                 if len(opportunities) == constants.OPPORTUNITIES_PAGE_SIZE:
                     break
diff --git a/core/domain/topic_fetchers.py b/core/domain/topic_fetchers.py
--- a/core/domain/topic_fetchers.py
+++ b/core/domain/topic_fetchers.py
@@ -246,22 +246,6 @@ def get_topic_by_url_fragment(url_fragment):
     return topic
 
 
-def get_all_topics_with_skills():
-    """Returns a list of topics with linked skills.
-
-    Returns:
-        list(Topic). A list of topics with skills.
-    """
-    all_topic_models = topic_models.TopicModel.get_all()
-    topics_with_skills = []
-    for topic_model in all_topic_models:
-        if topic_model:
-            topic = get_topic_from_model(topic_model)
-            if topic.get_all_skill_ids():
-                topics_with_skills.append(topic)
-    return topics_with_skills
-
-
 def get_all_topics():
     """Returns all the topics present in the datastore.
 


Test patch:
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
@@ -78,8 +78,9 @@ def setUp(self):
         for exp in explorations:
             self.publish_exploration(self.owner_id, exp.id)
 
+        topic_id = '0'
         topic = topic_domain.Topic.create_default_topic(
-            '0', 'topic', 'abbrev', 'description')
+            topic_id, 'topic', 'abbrev', 'description')
         topic.thumbnail_filename = 'thumbnail.svg'
         topic.thumbnail_bg_color = '#C6DCDA'
         topic.subtopics = [
@@ -89,7 +90,7 @@ def setUp(self):
                 '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)
+        topic_services.publish_topic(topic_id, self.admin_id)
 
         self.skill_id_0 = 'skill_id_0'
         self.skill_id_1 = 'skill_id_1'
@@ -98,7 +99,17 @@ def setUp(self):
             self.save_new_skill(
                 skill_id, self.admin_id, description='skill_description')
             topic_services.add_uncategorized_skill(
-                self.admin_id, '0', skill_id)
+                self.admin_id, topic_id, skill_id)
+
+        # Add skill opportunity topic to a classroom.
+        config_services.set_property(
+            self.admin_id, 'classroom_pages_data', [{
+                'name': 'math',
+                'url_fragment': 'math-one',
+                'topic_ids': [topic_id],
+                'course_details': '',
+                'topic_list_intro': ''
+            }])
 
         self.expected_skill_opportunity_dict_0 = {
             'id': self.skill_id_0,
@@ -195,6 +206,21 @@ def test_get_skill_opportunity_data(self):
         self.assertTrue(
             isinstance(response['next_cursor'], python_utils.BASESTRING))
 
+    def test_get_skill_opportunity_data_does_not_return_non_classroom_topics(
+            self):
+        config_services.revert_property(
+            self.admin_id, 'classroom_pages_data')
+
+        response = self.get_json(
+            '%s/skill' % feconf.CONTRIBUTOR_OPPORTUNITIES_DATA_URL,
+            params={})
+
+        self.assertEqual(
+            response['opportunities'], [])
+        self.assertFalse(response['more'])
+        self.assertTrue(
+            isinstance(response['next_cursor'], python_utils.BASESTRING))
+
     def test_get_translation_opportunity_data(self):
         response = self.get_json(
             '%s/translation' % feconf.CONTRIBUTOR_OPPORTUNITIES_DATA_URL,
diff --git a/core/domain/topic_fetchers_test.py b/core/domain/topic_fetchers_test.py
--- a/core/domain/topic_fetchers_test.py
+++ b/core/domain/topic_fetchers_test.py
@@ -191,12 +191,6 @@ def test_get_topics_by_id(self):
         self.assertIsNone(topics[1])
         self.assertEqual(len(topics), 2)
 
-    def test_get_all_topics_with_skills(self):
-        expected_topic = self.topic.to_dict()
-        topics = topic_fetchers.get_all_topics_with_skills()
-        self.assertEqual(topics[0].to_dict(), expected_topic)
-        self.assertEqual(len(topics), 1)
-
     def test_get_all_topic_rights_of_user(self):
         topic_services.assign_role(
             self.user_admin, self.user_a,
diff --git a/core/tests/protractor_desktop/contributorDashboard.js b/core/tests/protractor_desktop/contributorDashboard.js
--- a/core/tests/protractor_desktop/contributorDashboard.js
+++ b/core/tests/protractor_desktop/contributorDashboard.js
@@ -76,6 +76,12 @@ describe('Contributor dashboard page', function() {
     await topicsAndSkillsDashboardPage.get();
     await topicsAndSkillsDashboardPage.createTopic(
       TOPIC_NAMES[0], 'community-topic-one', 'Topic description 1', false);
+    const URL = await browser.getCurrentUrl();
+    // Example URL: http://localhost:8181/topic_editor/jT9z3iLnFjsQ#/
+    const TOPIC_ID_URL_PART = URL.split('/')[4];
+    // We have to remove the ending "#".
+    const TOPIC_ID = TOPIC_ID_URL_PART.substring(
+      0, TOPIC_ID_URL_PART.length - 1);
     await workflow.createSkillAndAssignTopic(
       SKILL_DESCRIPTIONS[0], REVIEW_MATERIALS[0], TOPIC_NAMES[0]);
     await topicsAndSkillsDashboardPage.get();
@@ -85,6 +91,17 @@ describe('Contributor dashboard page', function() {
     // Allow user1 to review question suggestions.
     await adminPage.get();
     await adminPage.assignQuestionReviewer('user1');
+
+    // Add topic to classroom to make it available for question contributions.
+    await adminPage.editConfigProperty(
+      'The details for each classroom page.',
+      'List',
+      async function(elem) {
+        elem = await elem.editItem(0, 'Dictionary');
+        elem = await elem.editEntry(4, 'List');
+        elem = await elem.addItem('Unicode');
+        await elem.setValue(TOPIC_ID);
+      });
     await users.logout();
   });