Repository: oppia/oppia

Issue:
Feedback messages from learners all have the same title
Publish an exploration, go to the learner view and leave some feedback for it. Then go to the editor feedback tab for that exploration. The title of the feedback message will be "(Feedback from a lerner)". If you leave more pieces of feedback, they will all get this same title.

This is the main source of entries in the feedback tab (since people rarely start new feedback threads from the tab itself), so the tab is rather hard to read. I propose instead to use the first few words of the feedback message as the title. (We don't want to require learners to enter a title themselves, as that increases the congnitive overload of leaving feedback.)


Discussion:
Ill work on this issue, though ill need some help with it

@darthgera123 are you still planning to work on this? If you have some specific questions then I'd be happy to address them.
Hi @kartik-25, would you like to have a go at this issue?
Ok, will look into it

Resolving patch:
diff --git a/core/domain/feedback_jobs_one_off.py b/core/domain/feedback_jobs_one_off.py
--- a/core/domain/feedback_jobs_one_off.py
+++ b/core/domain/feedback_jobs_one_off.py
@@ -19,6 +19,7 @@
 import ast
 import logging
 
+from constants import constants
 from core import jobs
 from core.domain import feedback_services
 from core.platform import models
@@ -66,3 +67,40 @@ def reduce(key, stringified_message_ids):
                 'next_message_id': next_message_id,
                 'message_count': len(message_ids)
             })
+
+
+class FeedbackSubjectOneOffJob(jobs.BaseMapReduceOneOffJobManager):
+    """One-off job for updating the feedback subject."""
+
+    DEFAULT_SUBJECT = u'(Feedback from a learner)'
+
+    @classmethod
+    def entity_classes_to_map_over(cls):
+        return [feedback_models.FeedbackThreadModel]
+
+    @staticmethod
+    def map(item):
+        if item.subject != FeedbackSubjectOneOffJob.DEFAULT_SUBJECT:
+            return
+
+        first_message = feedback_services.get_message(
+            item.exploration_id, item.thread_id, 0)
+
+        if not first_message.text:
+            return
+
+        if len(first_message.text) > constants.FEEDBACK_SUBJECT_MAX_CHAR_LIMIT:
+            updated_subject = first_message.text[
+                :constants.FEEDBACK_SUBJECT_MAX_CHAR_LIMIT]
+
+            if ' ' in updated_subject:
+                updated_subject = ' '.join(updated_subject.split(' ')[:-1])
+
+            item.subject = updated_subject + '...'
+        else:
+            item.subject = first_message.text
+        item.put(update_last_updated_time=False)
+
+    @staticmethod
+    def reduce(key, value):
+        pass
diff --git a/core/jobs_registry.py b/core/jobs_registry.py
--- a/core/jobs_registry.py
+++ b/core/jobs_registry.py
@@ -45,6 +45,7 @@
     exp_jobs_one_off.ItemSelectionInteractionOneOffJob,
     exp_jobs_one_off.ViewableExplorationsAuditJob,
     feedback_jobs_one_off.FeedbackThreadMessagesCountOneOffJob,
+    feedback_jobs_one_off.FeedbackSubjectOneOffJob,
     recommendations_jobs_one_off.ExplorationRecommendationsOneOffJob,
     stats_jobs_one_off.RecomputeStatisticsOneOffJob,
     stats_jobs_one_off.GenerateV1StatisticsJob,


Test patch:
diff --git a/core/domain/feedback_jobs_one_off_test.py b/core/domain/feedback_jobs_one_off_test.py
--- a/core/domain/feedback_jobs_one_off_test.py
+++ b/core/domain/feedback_jobs_one_off_test.py
@@ -125,3 +125,132 @@ def test_message_count(self):
         # Check if the quantities have the correct values.
         self.assertEqual(output[0][1]['message_count'], 1)
         self.assertEqual(output[0][1]['next_message_id'], 2)
+
+
+class FeedbackSubjectOneOffJobTest(test_utils.GenericTestBase):
+    """Tests for the one-off feedback subject update job."""
+    EXP_ID_1 = 'eid1'
+
+    EXPECTED_THREAD_DICT1 = {
+        'text': u'a small summary',
+        'subject': u'(Feedback from a learner)'
+    }
+
+    EXPECTED_THREAD_DICT2 = {
+        'text': u'a small text',
+        'subject': u'Some subject'
+    }
+
+    EXPECTED_THREAD_DICT3 = {
+        'text': (
+            u'It has to convert to a substring as it exceeds the '
+            u'character limit.'),
+        'subject': u'(Feedback from a learner)'
+    }
+
+    EXPECTED_THREAD_DICT4 = {
+        'text': (
+            u'Itisjustaverylongsinglewordfortestingget'
+            u'AbbreviatedText.'),
+        'subject': u'(Feedback from a learner)'
+    }
+
+    EXPECTED_THREAD_DICT5 = {
+        'text': '',
+        'subject': u'(Feedback from a learner)'
+    }
+
+    EXPECTED_THREAD_DICT6 = {
+        'text': 'Itisjustaverylongsinglewordfortesting',
+        'subject': u'(Feedback from a learner)'
+    }
+
+    EXPECTED_THREAD_DICT7 = {
+        'text': u'â, ??î or ôu🕧� n☁i✑💴++$-💯 ♓!🇪🚑🌚‼⁉4⃣od; /⏬®;😁☕😁:☝)😁😁😍1!@#',
+        'subject': u'(Feedback from a learner)'
+    }
+
+    USER_EMAIL = 'user@example.com'
+    USER_USERNAME = 'user'
+
+    def setUp(self):
+        super(FeedbackSubjectOneOffJobTest, self).setUp()
+
+        self.signup(self.USER_EMAIL, self.USER_USERNAME)
+        self.user_id = self.get_user_id_from_email(self.USER_EMAIL)
+        self.signup(self.OWNER_EMAIL, self.OWNER_USERNAME)
+        self.owner_id = self.get_user_id_from_email(self.OWNER_EMAIL)
+
+        self.save_new_valid_exploration(
+            self.EXP_ID_1, self.owner_id, title='Bridges in England',
+            category='Architecture', language_code='en')
+
+    def _run_one_off_job(self):
+        """Runs the one-off MapReduce job."""
+        job_id = (
+            feedback_jobs_one_off.FeedbackSubjectOneOffJob.create_new())
+        feedback_jobs_one_off.FeedbackSubjectOneOffJob.enqueue(
+            job_id)
+        self.assertEqual(
+            self.count_jobs_in_taskqueue(
+                taskqueue_services.QUEUE_NAME_ONE_OFF_JOBS), 1)
+        self.process_and_flush_pending_tasks()
+
+    def test_that_job_returns_correct_feedback_subject(self):
+        """Test if the job returns the correct feedback subject."""
+        feedback_services.create_thread(
+            self.EXP_ID_1, 'unused_state_name', self.user_id,
+            self.EXPECTED_THREAD_DICT1['subject'],
+            self.EXPECTED_THREAD_DICT1['text'])
+        feedback_services.create_thread(
+            self.EXP_ID_1, 'unused_state_name', self.user_id,
+            self.EXPECTED_THREAD_DICT2['subject'],
+            self.EXPECTED_THREAD_DICT2['text'])
+        feedback_services.create_thread(
+            self.EXP_ID_1, 'unused_state_name', self.user_id,
+            self.EXPECTED_THREAD_DICT3['subject'],
+            self.EXPECTED_THREAD_DICT3['text'])
+        feedback_services.create_thread(
+            self.EXP_ID_1, 'unused_state_name', self.user_id,
+            self.EXPECTED_THREAD_DICT4['subject'],
+            self.EXPECTED_THREAD_DICT4['text'])
+        feedback_services.create_thread(
+            self.EXP_ID_1, 'unused_state_name', self.user_id,
+            self.EXPECTED_THREAD_DICT5['subject'],
+            self.EXPECTED_THREAD_DICT5['text'])
+        feedback_services.create_thread(
+            self.EXP_ID_1, 'unused_state_name', self.user_id,
+            self.EXPECTED_THREAD_DICT6['subject'],
+            self.EXPECTED_THREAD_DICT6['text'])
+        feedback_services.create_thread(
+            self.EXP_ID_1, 'unused_state_name', self.user_id,
+            self.EXPECTED_THREAD_DICT7['subject'],
+            self.EXPECTED_THREAD_DICT7['text'])
+        threads_old = feedback_services.get_threads(self.EXP_ID_1)
+
+        self._run_one_off_job()
+
+        threads = feedback_services.get_threads(self.EXP_ID_1)
+
+        self.assertEqual(threads[0].subject, u'a small summary')
+        self.assertEqual(threads[1].subject, u'Some subject')
+        self.assertEqual(
+            threads[2].subject,
+            u'It has to convert to a substring as it exceeds...')
+        self.assertEqual(
+            threads[3].subject,
+            u'ItisjustaverylongsinglewordfortestinggetAbbreviate...')
+        self.assertEqual(threads[4].subject, u'(Feedback from a learner)')
+        self.assertEqual(
+            threads[5].subject,
+            u'Itisjustaverylongsinglewordfortesting')
+        self.assertEqual(
+            threads[6].subject,
+            u'â, ??î or ôu🕧� n☁i✑💴++$-💯 ♓!🇪🚑🌚‼⁉4⃣od;...')
+
+        self.assertEqual(threads[0].last_updated, threads_old[0].last_updated)
+        self.assertEqual(threads[1].last_updated, threads_old[1].last_updated)
+        self.assertEqual(threads[2].last_updated, threads_old[2].last_updated)
+        self.assertEqual(threads[3].last_updated, threads_old[3].last_updated)
+        self.assertEqual(threads[4].last_updated, threads_old[4].last_updated)
+        self.assertEqual(threads[5].last_updated, threads_old[5].last_updated)