Repository: oppia/oppia

Issue:
Creator dashboard: remember the user's list/grid preference.
Each time I go to my creator dashboard, I want to view the explorations in a list. However, Oppia does not remember this setting and keeps showing it to me as a grid. This becomes irritating.

I think it's fine to have grid be the default, but it would be nice if when I clicked the "list" option my preferences are remembered the next time I access the page.

/cc @526avijitgupta 


Discussion:
I would like to work on this issue. I propose to save this user preference in user object or something like that on server side. Any other suggestions?  
Assigned you; thanks!

I think there is an existing model for user preferences; you could probably add a field to it.
Hi @thedeveloperr -- How's it going ?
@thedeveloperr ping?
Deassigning @thedeveloperr due to lack of response.
Hi @Slayslot, would you be up for taking a look at this?
Hi @seanlip, would love to.
OK, thanks @Slayslot! 
Hi @seanlip, I just realised this has the tag of `loc: backend`. I'm afraid I mostly work on frontend.
Actually, this seems mislabelled -- it's more a full-stack thing. I've updated the labels accordingly.

That said, full-stack does have a backend component, so let me deassign you from this one and find you a new one. Thanks for letting me know!
Hi all, i'll work on this bug. I'm reading the documentation for the moment.
what about create a new entry called preferred_display_exploration on UserSettingsModel class? Then doing to GET request on CreatorDashboardHandler class?
Yes, that sounds fine; maybe `creator_dashboard_display_preference` might be a better name.
Hey @tvallois -- are you still working on this?
Deassigning @tvallois due to lack of response.
Howdy @domevit Any update on this?
I'm still working on this.

Resolving patch:
diff --git a/core/controllers/creator_dashboard.py b/core/controllers/creator_dashboard.py
--- a/core/controllers/creator_dashboard.py
+++ b/core/controllers/creator_dashboard.py
@@ -233,15 +233,27 @@ def _round_average_ratings(rating):
 
             subscribers_list.append(subscriber_summary)
 
+        user_settings = user_services.get_user_settings(
+            self.user_id, strict=False)
+        creator_dashboard_display_pref = (
+            user_settings.creator_dashboard_display_pref)
+
         self.values.update({
             'explorations_list': exp_summary_dicts,
             'collections_list': collection_summary_dicts,
             'dashboard_stats': dashboard_stats,
             'last_week_stats': last_week_stats,
-            'subscribers_list': subscribers_list
+            'subscribers_list': subscribers_list,
+            'display_preference': creator_dashboard_display_pref,
         })
         self.render_json(self.values)
 
+    @acl_decorators.can_access_creator_dashboard
+    def post(self):
+        creator_dashboard_display_pref = self.payload.get('display_preference')
+        user_services.update_user_creator_dashboard_display(
+            self.user_id, creator_dashboard_display_pref)
+
 
 class NotificationsHandler(base.BaseHandler):
     """Provides data about unseen notifications."""
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
@@ -79,11 +79,12 @@ def __init__(
             last_agreed_to_terms=None, last_started_state_editor_tutorial=None,
             last_logged_in=None, last_created_an_exploration=None,
             last_edited_an_exploration=None, profile_picture_data_url=None,
-            default_dashboard=None, user_bio='', subject_interests=None,
-            first_contribution_msec=None, preferred_language_codes=None,
-            preferred_site_language_code=None,
-            preferred_audio_language_code=None
-    ):
+            default_dashboard=None,
+            creator_dashboard_display_pref=(
+                constants.ALLOWED_CREATOR_DASHBOARD_DISPLAY_PREFS['CARD']),
+            user_bio='', subject_interests=None, first_contribution_msec=None,
+            preferred_language_codes=None, preferred_site_language_code=None,
+            preferred_audio_language_code=None):
         """Constructs a UserSettings domain object.
 
         Args:
@@ -128,6 +129,7 @@ def __init__(
         self.last_created_an_exploration = last_created_an_exploration
         self.profile_picture_data_url = profile_picture_data_url
         self.default_dashboard = default_dashboard
+        self.creator_dashboard_display_pref = creator_dashboard_display_pref
         self.user_bio = user_bio
         self.subject_interests = (
             subject_interests if subject_interests else [])
@@ -170,6 +172,16 @@ def validate(self):
         if self.role not in role_services.PARENT_ROLES:
             raise utils.ValidationError('Role %s does not exist.' % self.role)
 
+        if not isinstance(self.creator_dashboard_display_pref, basestring):
+            raise utils.ValidationError(
+                'Expected dashboard display preference to be a string, '
+                'received %s' % self.creator_dashboard_display_pref)
+        if (self.creator_dashboard_display_pref not in
+                constants.ALLOWED_CREATOR_DASHBOARD_DISPLAY_PREFS.values()):
+            raise utils.ValidationError(
+                '%s is not a valid value for the dashboard display '
+                'preferences.' % (self.creator_dashboard_display_pref))
+
     @property
     def truncated_email(self):
         """Returns truncated email by replacing last two characters before @
@@ -381,6 +393,8 @@ def get_users_settings(user_ids):
                     model.last_created_an_exploration),
                 profile_picture_data_url=model.profile_picture_data_url,
                 default_dashboard=model.default_dashboard,
+                creator_dashboard_display_pref=(
+                    model.creator_dashboard_display_pref),
                 user_bio=model.user_bio,
                 subject_interests=model.subject_interests,
                 first_contribution_msec=model.first_contribution_msec,
@@ -589,6 +603,8 @@ def _save_user_settings(user_settings):
             user_settings.last_created_an_exploration),
         profile_picture_data_url=user_settings.profile_picture_data_url,
         default_dashboard=user_settings.default_dashboard,
+        creator_dashboard_display_pref=(
+            user_settings.creator_dashboard_display_pref),
         user_bio=user_settings.user_bio,
         subject_interests=user_settings.subject_interests,
         first_contribution_msec=user_settings.first_contribution_msec,
@@ -771,6 +787,21 @@ def update_user_default_dashboard(user_id, default_dashboard):
     _save_user_settings(user_settings)
 
 
+def update_user_creator_dashboard_display(
+        user_id, creator_dashboard_display_pref):
+    """Updates the creator dashboard preference of user with given user id.
+
+    Args:
+        user_id: str. The user id.
+        creator_dashboard_display_pref: str. The creator dashboard preference
+            the user wants.
+    """
+    user_settings = get_user_settings(user_id, strict=True)
+    user_settings.creator_dashboard_display_pref = (
+        creator_dashboard_display_pref)
+    _save_user_settings(user_settings)
+
+
 def update_subject_interests(user_id, subject_interests):
     """Updates subject_interests of user with given user_id.
 
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
@@ -63,6 +63,11 @@ class UserSettingsModel(base_models.BaseModel):
         choices=[
             constants.DASHBOARD_TYPE_LEARNER,
             constants.DASHBOARD_TYPE_CREATOR])
+    # The preferred dashboard display preference.
+    creator_dashboard_display_pref = ndb.StringProperty(
+        default=constants.ALLOWED_CREATOR_DASHBOARD_DISPLAY_PREFS['CARD'],
+        indexed=True,
+        choices=constants.ALLOWED_CREATOR_DASHBOARD_DISPLAY_PREFS.values())
     # User specified biography (to be shown on their profile page).
     user_bio = ndb.TextProperty(indexed=False)
     # Subject interests specified by the user.


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
@@ -18,6 +18,7 @@
 import logging
 import os
 
+from constants import constants
 from core.domain import collection_services
 from core.domain import event_services
 from core.domain import exp_services
@@ -367,6 +368,26 @@ def test_get_user_ids_by_role(self):
                 feconf.ROLE_ID_BANNED_USER)),
             set(['test3', 'test4']))
 
+    def test_update_user_creator_dashboard_display(self):
+        user_id = 'test_id'
+        username = 'testname'
+        user_email = 'test@email.com'
+
+        user_services.create_new_user(user_id, user_email)
+        user_services.set_username(user_id, username)
+
+        user_setting = user_services.get_user_settings(user_id)
+        self.assertEqual(
+            user_setting.creator_dashboard_display_pref,
+            constants.ALLOWED_CREATOR_DASHBOARD_DISPLAY_PREFS['CARD'])
+
+        user_services.update_user_creator_dashboard_display(
+            user_id, constants.ALLOWED_CREATOR_DASHBOARD_DISPLAY_PREFS['LIST'])
+        user_setting = user_services.get_user_settings(user_id)
+        self.assertEqual(
+            user_setting.creator_dashboard_display_pref,
+            constants.ALLOWED_CREATOR_DASHBOARD_DISPLAY_PREFS['LIST'])
+
     def test_update_user_role(self):
         user_id = 'test_id'
         username = 'testname'