Repository: oppia/oppia

Issue:
Server error on learner dashboard after creating question suggestion from community dashboard
<!--
  - 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**
Server error on learner dashboard after creating question suggestion from the community dashboard

**To Reproduce**
Steps to reproduce the behavior:
1. Checkout develop
2. Enable community dashboard page from feconf.py
3. Start the server and generate **topics and skills** dummy data from the admin page
4. Go to /community_dashboard
5. Switch to "Submit Question" tab and submit one question
6. Check the learner dashboard page

**Observed behavior**

![](https://lh3.googleusercontent.com/-DoaOi2fx0Z8/XoNJCDFvAdI/AAAAAAAADvo/Jf82cKPJimsZvBvf9DK3gQU5Y6fL1fJGgCK8BGAsYHg/s0/2020-03-31.png)



Discussion:

Resolving patch:
diff --git a/core/controllers/learner_dashboard.py b/core/controllers/learner_dashboard.py
--- a/core/controllers/learner_dashboard.py
+++ b/core/controllers/learner_dashboard.py
@@ -79,7 +79,7 @@ def get(self):
             self.user_id)
         if len(full_thread_ids) > 0:
             thread_summaries, number_of_unread_threads = (
-                feedback_services.get_thread_summaries(
+                feedback_services.get_exp_thread_summaries(
                     self.user_id, full_thread_ids))
         else:
             thread_summaries, number_of_unread_threads = [], 0
diff --git a/core/domain/feedback_services.py b/core/domain/feedback_services.py
--- a/core/domain/feedback_services.py
+++ b/core/domain/feedback_services.py
@@ -411,8 +411,9 @@ def _get_thread_from_model(thread_model):
         thread_model.last_nonempty_message_author_id)
 
 
-def get_thread_summaries(user_id, thread_ids):
-    """Returns a list of summaries corresponding to each of the threads given.
+def get_exp_thread_summaries(user_id, thread_ids):
+    """Returns a list of summaries corresponding to the exploration threads from
+    the given thread ids. Non-exploration threads are not included in the list.
     It also returns the number of threads that are currently not read by the
     user.
 
@@ -427,51 +428,57 @@ def get_thread_summaries(user_id, thread_ids):
             number_of_unread_threads: int. The number of threads not read by the
                 user.
     """
-    thread_user_ids = [
-        feedback_models.GeneralFeedbackThreadUserModel.generate_full_id(
-            user_id, thread_id)
-        for thread_id in thread_ids
+    # We need to fetch the thread models first to filter out the threads which
+    # don't refer to an exploration.
+    exp_thread_models = [
+        model for model in feedback_models.GeneralFeedbackThreadModel.get_multi(
+            thread_ids)
+        if model and model.entity_type == feconf.ENTITY_TYPE_EXPLORATION
     ]
-    thread_exp_ids = [
-        get_exp_id_from_thread_id(thread_id) for thread_id in thread_ids
+
+    exp_thread_user_model_ids = [
+        feedback_models.GeneralFeedbackThreadUserModel.generate_full_id(
+            user_id, model.id)
+        for model in exp_thread_models
     ]
+    exp_model_ids = [model.entity_id for model in exp_thread_models]
 
-    thread_models, thread_user_models, thread_exp_models = (
+    exp_thread_user_models, exp_models = (
         datastore_services.fetch_multiple_entities_by_ids_and_models([
-            ('GeneralFeedbackThreadModel', thread_ids),
-            ('GeneralFeedbackThreadUserModel', thread_user_ids),
-            ('ExplorationModel', thread_exp_ids),
+            ('GeneralFeedbackThreadUserModel', exp_thread_user_model_ids),
+            ('ExplorationModel', exp_model_ids),
         ]))
 
-    threads = [_get_thread_from_model(m) for m in thread_models]
-    flat_last_two_message_models = (
+    threads = [_get_thread_from_model(m) for m in exp_thread_models]
+    flattened_last_two_message_models_of_threads = (
         feedback_models.GeneralFeedbackMessageModel.get_multi(
             itertools.chain.from_iterable(
                 t.get_last_two_message_ids() for t in threads)))
     last_two_message_models_of_threads = [
-        flat_last_two_message_models[i:i + 2]
-        for i in python_utils.RANGE(0, len(flat_last_two_message_models), 2)
+        flattened_last_two_message_models_of_threads[i:i + 2]
+        for i in python_utils.RANGE(
+            0, len(flattened_last_two_message_models_of_threads), 2)
     ]
 
     thread_summaries = []
     number_of_unread_threads = 0
-    for thread, last_two_message_models, user_model, exp_model in (
+    for thread, last_two_message_models, thread_user_model, exp_model in (
             python_utils.ZIP(
-                threads, last_two_message_models_of_threads, thread_user_models,
-                thread_exp_models)):
-        last_message_model, second_last_message_model = last_two_message_models
+                threads, last_two_message_models_of_threads,
+                exp_thread_user_models, exp_models)):
         message_ids_read_by_user = (
-            () if user_model is None else user_model.message_ids_read_by_user)
+            () if thread_user_model is None else
+            thread_user_model.message_ids_read_by_user)
 
-        # The last message is never None because all threads have at least one
-        # message.
+        last_message_model, second_last_message_model = last_two_message_models
+        # We don't need to check if the last message is None because all threads
+        # have at least one message.
         last_message_is_read = (
             last_message_model.message_id in message_ids_read_by_user)
         author_last_message = (
             last_message_model.author_id and
             user_services.get_username(last_message_model.author_id))
-
-        # The second-to-last message, however, can be None.
+        # The second-to-last message, however, might be None.
         second_last_message_is_read = (
             second_last_message_model is not None and
             second_last_message_model.message_id in message_ids_read_by_user)


Test patch:
diff --git a/core/domain/feedback_services_test.py b/core/domain/feedback_services_test.py
--- a/core/domain/feedback_services_test.py
+++ b/core/domain/feedback_services_test.py
@@ -340,7 +340,8 @@ def test_get_thread_summaries(self):
             self.user_id)
         thread_ids.append('exploration.' + self.EXP_ID_3 + '.' + self.THREAD_ID)
         thread_summaries, number_of_unread_threads = (
-            feedback_services.get_thread_summaries(self.user_id, thread_ids))
+            feedback_services.get_exp_thread_summaries(
+                self.user_id, thread_ids))
         exploration_titles = (
             ['Bridges in England', 'Sillat Suomi', 'Leaning tower of Pisa'])
 
@@ -371,7 +372,8 @@ def test_get_thread_summaries(self):
         feedback_services.create_message(
             threads[0].id, self.owner_id, None, None, 'editor message')
         _, number_of_unread_threads = (
-            feedback_services.get_thread_summaries(self.user_id, thread_ids))
+            feedback_services.get_exp_thread_summaries(
+                self.user_id, thread_ids))
 
         # Check if the number of unread messages is equal to 1.
         self.assertEqual(number_of_unread_threads, 1)
@@ -390,13 +392,26 @@ def test_get_thread_summaries_returns_correct_message_count(self):
         thread.message_count = 0
         thread.put()
 
-        thread_summaries, _ = feedback_services.get_thread_summaries(
+        thread_summaries, _ = feedback_services.get_exp_thread_summaries(
             self.owner_id, [thread_id_1, thread_id_2])
 
         self.assertEqual(len(thread_summaries), 2)
         self.assertEqual(thread_summaries[0].total_message_count, 1)
         self.assertEqual(thread_summaries[1].total_message_count, 1)
 
+    def test_get_thread_summaries_only_returns_threads_for_explorations(self):
+        exp_thread_id = feedback_services.create_thread(
+            'exploration', self.EXP_ID_1, None, 'unused subject', 'unused text')
+        skill_thread_id = feedback_services.create_thread(
+            'skill', 'skillid1', None, 'unused subject', 'unused text')
+
+        thread_summaries, _ = feedback_services.get_exp_thread_summaries(
+            self.owner_id, [exp_thread_id, skill_thread_id])
+
+        self.assertEqual(len(thread_summaries), 1)
+        self.assertEqual(
+            thread_summaries[0].exploration_title, 'Bridges in England')
+
     def test_update_messages_read_by_the_user(self):
         feedback_services.create_thread(
             'exploration', self.EXP_ID_1, self.user_id,
diff --git a/core/tests/load_tests/feedback_thread_summaries_test.py b/core/tests/load_tests/feedback_thread_summaries_test.py
--- a/core/tests/load_tests/feedback_thread_summaries_test.py
+++ b/core/tests/load_tests/feedback_thread_summaries_test.py
@@ -79,6 +79,6 @@ def test_get_thread_summaries_load_test(self):
 
         start = time.time()
         # Fetch the summaries of all the threads.
-        feedback_services.get_thread_summaries(self.user_id, thread_ids)
+        feedback_services.get_exp_thread_summaries(self.user_id, thread_ids)
         elapsed_time = time.time() - start
         self.assertLessEqual(elapsed_time, 1.7)