Repository: oppia/oppia

Issue:
Make it possible for users to opt-out of receiving emails from individual explorations.
Currently, users set their email preferences globally on the Preferences page. However (similar to GitHub), it's likely that users may want to mute emails from particular explorations, similar to how, on GitHub, one can mute notifications for particular issue threads.

It seems like a good idea to have a button on the Settings page of an exploration that allows a user to unsubscribe from email notifications to just that particular exploration, otherwise their only alternative is to unsubscribe from all email notifications in general.



Discussion:
Hi @Slepice1, you should start working on design doc for this issue. To get you started, first identify which emails are being sent per exploration basis (i.e. feedback emails are sent per exploration basis). Then figure out a general way to how to enable / disable emails per exploration. Prepare a google doc for this info. We can discuss there.

Thanks

I have prepared design doc and I'm working on it.
Hi @Slepice1, how's this going?
I have started working on implementation.

Resolving patch:
diff --git a/core/domain/user_services.py b/core/domain/user_services.py
--- a/core/domain/user_services.py
+++ b/core/domain/user_services.py
@@ -913,6 +913,67 @@ def get_email_preferences(user_id):
     }
 
 
+def set_email_preferences_for_exploration(
+        user_id, exploration_id, mute_feedback_notifications=None,
+        mute_suggestion_notifications=None):
+    """Sets mute preferences for exploration with given exploration_id of user
+    with given user_id.
+
+    If no ExplorationUserDataModel exists for this user and exploration,
+    a new one will be created.
+
+    Args:
+        user_id: str. The user id.
+        exploration_id: str. The exploration id.
+        mute_feedback_notifications: bool. Whether the given user has muted
+            feedback emails. Defaults to None.
+        mute_suggestion_notifications: bool. Whether the given user has muted
+            suggestion emails. Defaults to None.
+    """
+    exploration_user_model = user_models.ExplorationUserDataModel.get(
+        user_id, exploration_id)
+    if exploration_user_model is None:
+        exploration_user_model = user_models.ExplorationUserDataModel.create(
+            user_id, exploration_id)
+    if mute_feedback_notifications is not None:
+        exploration_user_model.mute_feedback_notifications = (
+            mute_feedback_notifications)
+    if mute_suggestion_notifications is not None:
+        exploration_user_model.mute_suggestion_notifications = (
+            mute_suggestion_notifications)
+    exploration_user_model.put()
+
+
+def get_email_preferences_for_exploration(user_id, exploration_id):
+    """Gives mute preferences for exploration with given exploration_id of user
+    with given user_id.
+
+    Args:
+        user_id: str. The user id.
+        exploration_id: str. The exploration id.
+
+    Returns:
+        dict. Representing whether the user has muted emails for exploration
+        with given exploration_id. The format of returned value:
+        {
+            'mute_feedback_notifications': value(bool),
+            'mute_suggestion_notifications': value(bool)
+        }
+    """
+    exploration_user_model = user_models.ExplorationUserDataModel.get(
+        user_id, exploration_id)
+    return {
+        'mute_feedback_notifications': (
+            feconf.DEFAULT_FEEDBACK_NOTIFICATIONS_MUTED_PREFERENCE
+            if exploration_user_model is None
+            else exploration_user_model.mute_feedback_notifications),
+        'mute_suggestion_notifications': (
+            feconf.DEFAULT_SUGGESTION_NOTIFICATIONS_MUTED_PREFERENCE
+            if exploration_user_model is None
+            else exploration_user_model.mute_suggestion_notifications)
+    }
+
+
 class UserContributions(object):
     """Value object representing a user's contributions.
 
diff --git a/core/storage/user/gae_models.py b/core/storage/user/gae_models.py
--- a/core/storage/user/gae_models.py
+++ b/core/storage/user/gae_models.py
@@ -243,6 +243,14 @@ class ExplorationUserDataModel(base_models.BaseModel):
     # The exploration version that this change list applied to.
     draft_change_list_exp_version = ndb.IntegerProperty(default=None)
 
+    # The user's preference for receiving suggestion emails for this exploration
+    mute_suggestion_notifications = ndb.BooleanProperty(
+        default=feconf.DEFAULT_SUGGESTION_NOTIFICATIONS_MUTED_PREFERENCE)
+
+    # The user's preference for receiving feedback emails for this exploration
+    mute_feedback_notifications = ndb.BooleanProperty(
+        default=feconf.DEFAULT_FEEDBACK_NOTIFICATIONS_MUTED_PREFERENCE)
+
     @classmethod
     def _generate_id(cls, user_id, exploration_id):
         return '%s.%s' % (user_id, exploration_id)
diff --git a/feconf.py b/feconf.py
--- a/feconf.py
+++ b/feconf.py
@@ -211,6 +211,12 @@ def get_empty_ratings():
 # Whether to send an email when new feedback message is received for
 # an exploration.
 DEFAULT_FEEDBACK_MESSAGE_EMAIL_PREFERENCE = True
+# Whether exploration feedback emails are muted,
+# when the user has not specified a preference.
+DEFAULT_FEEDBACK_NOTIFICATIONS_MUTED_PREFERENCE = False
+# Whether exploration suggestion emails are muted,
+# when the user has not specified a preference.
+DEFAULT_SUGGESTION_NOTIFICATIONS_MUTED_PREFERENCE = False
 # 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/domain/user_services_test.py b/core/domain/user_services_test.py
--- a/core/domain/user_services_test.py
+++ b/core/domain/user_services_test.py
@@ -252,6 +252,72 @@ def test_set_and_get_user_email_preferences(self):
         self.assertEquals(
             email_preferences['can_receive_feedback_message_email'], False)
 
+    def test_set_and_get_user_email_preferences_for_exploration(self):
+        user_id = 'someUser'
+        exploration_id = 'someExploration'
+        username = 'username'
+        user_email = 'user@example.com'
+
+        user_services.get_or_create_user(user_id, user_email)
+        user_services.set_username(user_id, username)
+
+        # When ExplorationUserDataModel is yet to be created, the value
+        # of mute_feedback_notifications and mute_suggestion_notifications
+        # should match the default values.
+        exploration_user_model = (
+            user_services.user_models.ExplorationUserDataModel.get(
+                user_id, exploration_id))
+        self.assertIsNone(exploration_user_model)
+        email_preferences = user_services.get_email_preferences_for_exploration(
+            user_id, exploration_id)
+        self.assertEquals(
+            email_preferences['mute_feedback_notifications'],
+            feconf.DEFAULT_FEEDBACK_NOTIFICATIONS_MUTED_PREFERENCE)
+        self.assertEquals(
+            email_preferences['mute_suggestion_notifications'],
+            feconf.DEFAULT_SUGGESTION_NOTIFICATIONS_MUTED_PREFERENCE)
+
+        # This initializes a ExplorationUserDataModel instance with
+        # the default mute values.
+        user_services.set_email_preferences_for_exploration(
+            user_id, exploration_id,
+            feconf.DEFAULT_FEEDBACK_NOTIFICATIONS_MUTED_PREFERENCE,
+            feconf.DEFAULT_SUGGESTION_NOTIFICATIONS_MUTED_PREFERENCE)
+
+        email_preferences = user_services.get_email_preferences_for_exploration(
+            user_id, exploration_id)
+        self.assertEquals(
+            email_preferences['mute_feedback_notifications'],
+            feconf.DEFAULT_FEEDBACK_NOTIFICATIONS_MUTED_PREFERENCE)
+        self.assertEquals(
+            email_preferences['mute_suggestion_notifications'],
+            feconf.DEFAULT_SUGGESTION_NOTIFICATIONS_MUTED_PREFERENCE)
+
+        # This sets only mute_suggestion_notifications property to True.
+        # mute_feedback_notifications should remain same as before.
+        user_services.set_email_preferences_for_exploration(
+            user_id, exploration_id, mute_suggestion_notifications=True)
+
+        email_preferences = user_services.get_email_preferences_for_exploration(
+            user_id, exploration_id)
+        self.assertEquals(
+            email_preferences['mute_feedback_notifications'],
+            feconf.DEFAULT_FEEDBACK_NOTIFICATIONS_MUTED_PREFERENCE)
+        self.assertEquals(
+            email_preferences['mute_suggestion_notifications'], True)
+
+        # This sets only mute_feedback_notifications property to True.
+        # mute_suggestion_notifications should remain same as before.
+        user_services.set_email_preferences_for_exploration(
+            user_id, exploration_id, mute_feedback_notifications=True)
+
+        email_preferences = user_services.get_email_preferences_for_exploration(
+            user_id, exploration_id)
+        self.assertEquals(
+            email_preferences['mute_feedback_notifications'], True)
+        self.assertEquals(
+            email_preferences['mute_suggestion_notifications'], True)
+
     def test_get_current_date_as_string(self):
         custom_datetimes = [
             datetime.date(2011, 1, 1),