Repository: oppia/oppia

Issue:
Send emails when an exploration gets new feedback
```
Add an option on the user dashboard to subscribe to feedback threads by email. 
A user who chooses to subscribe would be notified by email of all messages as 
they appear in their dashboard.

Additional possibilities:
 - Add another option in the dashboard to subscribe to all commits to explorations the user is involved with; this would cause them to be notified by email of commits as they are added to their dashboard.
 - Ask the user when they choose their username whether they would like to subscribe to messages by email. This would be desirable as otherwise an option on the dashboard may go unnoticed.
```

Original issue reported on code.google.com by `jacobdav...@gmail.com` on 16 Nov 2014 at 12:39



Discussion:
/cc @jacobdavis11 (original reporter)


Resolving patch:
diff --git a/core/storage/feedback/gae_models.py b/core/storage/feedback/gae_models.py
--- a/core/storage/feedback/gae_models.py
+++ b/core/storage/feedback/gae_models.py
@@ -287,3 +287,24 @@ def get_by_exploration_and_thread_id(cls, exploration_id, thread_id):
         Returns None if it doesn't match anything."""
 
         return cls.get_by_id(cls._get_instance_id(exploration_id, thread_id))
+
+
+class UnsentFeedbackEmailModel(base_models.BaseModel):
+    """Model for storing feedback messages that need to be sent to creators.
+
+    Instances of this model contain information about feedback messages that
+    have been received by the site, but have not yet been sent to creators.
+    The model instances will be deleted once the corresponding email has been
+    sent.
+
+    The id of each model instance is the user_id of the user who should receive
+    the messages."""
+
+    # The list of feedback messages that need to be sent to this user.
+    # Each element in this list is a dict with keys 'exploration_id',
+    # 'thread_id' and 'message_id'; this information is used to retrieve
+    # corresponding FeedbackMessageModel instance.
+    feedback_messages = ndb.JsonProperty(repeated=True, indexed=True)
+    # The number of failed attempts that have been made (so far) to
+    # send an email to this user.
+    retries = ndb.IntegerProperty(default=0, required=True, indexed=True)
diff --git a/feconf.py b/feconf.py
--- a/feconf.py
+++ b/feconf.py
@@ -182,6 +182,10 @@ def get_empty_ratings():
 # send_role_notification_email() function in email_manager.py and modify them
 # accordingly.
 CAN_SEND_EDITOR_ROLE_EMAILS = False
+# If enabled then emails will be sent to creators for feedback messages.
+CAN_SEND_FEEDBACK_MESSAGE_EMAILS = False
+# Time to wait before sending feedback message emails (currently set to 1 hour).
+DEFAULT_FEEDBACK_MESSAGE_EMAIL_COUNTDOWN_SECS = 3600
 # Whether to send email updates to a user who has not specified a preference.
 DEFAULT_EMAIL_UPDATES_PREFERENCE = False
 # Whether to send an invitation email when the user is granted


Test patch:
diff --git a/core/storage/feedback/gae_models_test.py b/core/storage/feedback/gae_models_test.py
--- a/core/storage/feedback/gae_models_test.py
+++ b/core/storage/feedback/gae_models_test.py
@@ -107,3 +107,25 @@ def test_get_by_exploration_and_thread_id_no_suggestion(self):
                 'invalid_exp_id', 'thread_id1'))
 
         self.assertIsNone(actual_suggestion)
+
+
+class UnsentFeedbackEmailModelTest(test_utils.GenericTestBase):
+    """Tests for FeedbackMessageEmailDataModel class"""
+
+    def test_new_instances_stores_correct_data(self):
+        user_id = 'A'
+        feedback_messages_dict = {
+            'exploration_id': 'ABC123',
+            'thread_id': 'thread_id1',
+            'message_id': 'message_id1'
+        }
+        email_instance = feedback_models.UnsentFeedbackEmailModel(
+            id=user_id, feedback_messages=[feedback_messages_dict])
+        email_instance.put()
+
+        retrieved_instance = (
+            feedback_models.UnsentFeedbackEmailModel.get_by_id(id=user_id))
+
+        self.assertEqual(
+            retrieved_instance.feedback_messages, [feedback_messages_dict])
+        self.assertEqual(retrieved_instance.retries, 0)