Repository: oppia/oppia

Issue:
Introduce "Learner" Role for profiles
See [Design doc](https://docs.google.com/document/d/1sQagy3irkjB1Kt5sdb_KkOgxODt2m2ya0ZCoSJNlY4A/edit?usp=sharing) for **Full Remote Profile Support** Project!

Implement new user role called **Learner** for "Profiles". Upon addition of this role, the roles hierarchy would become:

![hierarchy](https://user-images.githubusercontent.com/32328861/88064584-1c102680-cb89-11ea-9d62-7e8611c04f9a.jpg)

Resources: [Instructions for editing roles or actions](https://github.com/oppia/oppia/wiki/Instructions-for-editing-roles-or-actions)

**Target PR date**: 21 July, 2020
**Target Completion date**: 23 July, 2020


Discussion:

Resolving patch:
diff --git a/core/domain/role_services.py b/core/domain/role_services.py
--- a/core/domain/role_services.py
+++ b/core/domain/role_services.py
@@ -86,6 +86,9 @@
 ACTION_VISIT_ANY_TOPIC_EDITOR = 'VISIT_ANY_TOPIC_EDITOR'
 
 # Users can be updated to the following list of role IDs via admin interface.
+#
+# NOTE: LEARNER role should not be updated to any other role, hence do not
+#   add it to the following list.
 UPDATABLE_ROLES = [
     feconf.ROLE_ID_ADMIN,
     feconf.ROLE_ID_BANNED_USER,
@@ -96,6 +99,9 @@
 ]
 
 # Users can be viewed by following list of role IDs via admin interface.
+#
+# NOTE: Do not include LEARNER role in this list as it does not represent
+#   role for a separate user account, but rather a profile within the account.
 VIEWABLE_ROLES = [
     feconf.ROLE_ID_ADMIN,
     feconf.ROLE_ID_BANNED_USER,
@@ -111,6 +117,7 @@
     feconf.ROLE_ID_COLLECTION_EDITOR: 'collection editor',
     feconf.ROLE_ID_EXPLORATION_EDITOR: 'exploration editor',
     feconf.ROLE_ID_GUEST: 'guest',
+    feconf.ROLE_ID_LEARNER: 'learner',
     feconf.ROLE_ID_MODERATOR: 'moderator',
     feconf.ROLE_ID_TOPIC_MANAGER: 'topic manager'
 }
@@ -136,8 +143,9 @@
     feconf.ROLE_ID_ADMIN: [feconf.ROLE_ID_MODERATOR],
     feconf.ROLE_ID_BANNED_USER: [feconf.ROLE_ID_GUEST],
     feconf.ROLE_ID_COLLECTION_EDITOR: [feconf.ROLE_ID_EXPLORATION_EDITOR],
-    feconf.ROLE_ID_EXPLORATION_EDITOR: [feconf.ROLE_ID_GUEST],
+    feconf.ROLE_ID_EXPLORATION_EDITOR: [feconf.ROLE_ID_LEARNER],
     feconf.ROLE_ID_GUEST: [],
+    feconf.ROLE_ID_LEARNER: [feconf.ROLE_ID_GUEST],
     feconf.ROLE_ID_MODERATOR: [feconf.ROLE_ID_TOPIC_MANAGER],
     feconf.ROLE_ID_TOPIC_MANAGER: [feconf.ROLE_ID_COLLECTION_EDITOR]
 }
@@ -181,11 +189,9 @@
     ],
     feconf.ROLE_ID_EXPLORATION_EDITOR: [
         ACTION_ACCESS_CREATOR_DASHBOARD,
-        ACTION_ACCESS_LEARNER_DASHBOARD,
         ACTION_CREATE_EXPLORATION,
         ACTION_DELETE_OWNED_PRIVATE_ACTIVITY,
         ACTION_EDIT_OWNED_ACTIVITY,
-        ACTION_FLAG_EXPLORATION,
         ACTION_SUBSCRIBE_TO_USERS,
         ACTION_MANAGE_ACCOUNT,
         ACTION_MODIFY_ROLES_FOR_OWNED_ACTIVITY,
@@ -197,6 +203,10 @@
     feconf.ROLE_ID_GUEST: [
         ACTION_PLAY_ANY_PUBLIC_ACTIVITY,
     ],
+    feconf.ROLE_ID_LEARNER: [
+        ACTION_FLAG_EXPLORATION,
+        ACTION_ACCESS_LEARNER_DASHBOARD,
+    ],
     feconf.ROLE_ID_MODERATOR: [
         ACTION_ACCESS_MODERATOR_PAGE,
         ACTION_DELETE_ANY_PUBLIC_ACTIVITY,
diff --git a/feconf.py b/feconf.py
--- a/feconf.py
+++ b/feconf.py
@@ -913,6 +913,7 @@ def get_empty_ratings():
 # Following are the constants for the role IDs.
 ROLE_ID_GUEST = 'GUEST'
 ROLE_ID_BANNED_USER = 'BANNED_USER'
+ROLE_ID_LEARNER = 'LEARNER'
 ROLE_ID_EXPLORATION_EDITOR = 'EXPLORATION_EDITOR'
 ROLE_ID_COLLECTION_EDITOR = 'COLLECTION_EDITOR'
 ROLE_ID_TOPIC_MANAGER = 'TOPIC_MANAGER'


Test patch:
diff --git a/core/controllers/acl_decorators_test.py b/core/controllers/acl_decorators_test.py
--- a/core/controllers/acl_decorators_test.py
+++ b/core/controllers/acl_decorators_test.py
@@ -36,6 +36,7 @@
 import webtest
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class PlayExplorationDecoratorTests(test_utils.GenericTestBase):
     """Tests for play exploration decorator."""
 
@@ -113,6 +114,7 @@ def test_logged_in_user_cannot_access_not_owned_exploration(self):
         self.logout()
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class PlayCollectionDecoratorTests(test_utils.GenericTestBase):
     """Tests for play collection decorator."""
 
@@ -201,6 +203,7 @@ def test_cannot_access_collection_with_invalid_collection_id(self):
         self.logout()
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class EditCollectionDecoratorTests(test_utils.GenericTestBase):
     """Tests for can_edit_collection decorator."""
 
@@ -310,6 +313,7 @@ def test_admin_can_edit_any_private_collection(self):
         self.logout()
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class CreateExplorationDecoratorTests(test_utils.GenericTestBase):
     """Tests for can_create_exploration decorator."""
 
@@ -358,6 +362,7 @@ def test_guest_is_redirected_when_using_html_handler(self):
         self.assertEqual(response.status_int, 302)
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class CreateCollectionDecoratorTests(test_utils.GenericTestBase):
     """Tests for can_create_collection decorator."""
 
@@ -415,6 +420,7 @@ def test_admins_can_create_collection(self):
         self.logout()
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class AccessCreatorDashboardTests(test_utils.GenericTestBase):
     """Tests for can_access_creator_dashboard decorator."""
 
@@ -451,6 +457,7 @@ def test_normal_user_can_access_editor_dashboard(self):
         self.assertEqual(response['success'], True)
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class CommentOnFeedbackThreadTests(test_utils.GenericTestBase):
     """Tests for can_comment_on_feedback_thread decorator."""
 
@@ -565,6 +572,7 @@ def test_admin_can_comment_on_feeback_for_private_exploration(self):
         self.logout()
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class CreateFeedbackThreadTests(test_utils.GenericTestBase):
     """Tests for can_create_feedback_thread decorator."""
 
@@ -647,6 +655,7 @@ def test_admin_can_create_feeback_for_private_exploration(self):
         self.logout()
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class ViewFeedbackThreadTests(test_utils.GenericTestBase):
     """Tests for can_view_feedback_thread decorator."""
 
@@ -733,6 +742,7 @@ def test_admin_can_view_feeback_for_private_exploration(self):
         self.logout()
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class ManageEmailDashboardTests(test_utils.GenericTestBase):
     """Tests for can_manage_email_dashboard decorator."""
 
@@ -782,6 +792,7 @@ def test_admin_can_access_email_dashboard(self):
         self.logout()
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class RateExplorationTests(test_utils.GenericTestBase):
     """Tests for can_rate_exploration decorator."""
 
@@ -817,6 +828,7 @@ def test_normal_user_can_give_rating(self):
         self.logout()
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class AccessModeratorPageTests(test_utils.GenericTestBase):
     username = 'user'
     user_email = 'user@example.com'
@@ -852,6 +864,7 @@ def test_admin_can_access_moderator_page(self):
         self.logout()
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class FlagExplorationTests(test_utils.GenericTestBase):
     """Tests for can_flag_exploration decorator."""
 
@@ -887,6 +900,7 @@ def test_normal_user_can_flag_exploration(self):
         self.logout()
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class SubscriptionToUsersTests(test_utils.GenericTestBase):
     """Tests for can_subscribe_to_users decorator."""
 
@@ -920,6 +934,7 @@ def test_normal_user_can_subscribe_to_users(self):
         self.logout()
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class SendModeratorEmailsTests(test_utils.GenericTestBase):
 
     username = 'user'
@@ -956,6 +971,7 @@ def test_admin_can_send_moderator_emails(self):
         self.logout()
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class VoiceoverExplorationTests(test_utils.GenericTestBase):
     """Tests for can_voiceover_exploration decorator."""
 
@@ -1090,6 +1106,7 @@ def test_user_without_voice_artist_role_of_exploration_cannot_voiceover_private_
         self.logout()
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class EditExplorationTests(test_utils.GenericTestBase):
     """Tests for can_edit_exploration decorator."""
 
@@ -1177,6 +1194,7 @@ def test_admin_can_edit_private_exploration(self):
         self.logout()
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class ManageOwnAccountTests(test_utils.GenericTestBase):
     """Tests for decorator can_manage_own_account."""
 
@@ -1216,6 +1234,7 @@ def test_normal_user_can_manage_preferences(self):
         self.logout()
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class UploadExplorationTests(test_utils.GenericTestBase):
     """Tests for can_upload_exploration decorator."""
 
@@ -1260,6 +1279,7 @@ def test_guest_cannot_upload_explorations(self):
             'You must be logged in to access this resource.')
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class DeleteExplorationTests(test_utils.GenericTestBase):
     """Tests for can_delete_exploration decorator."""
 
@@ -1342,6 +1362,7 @@ def test_moderator_cannot_delete_private_exploration(self):
         self.logout()
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class SuggestChangesToExplorationTests(test_utils.GenericTestBase):
     """Tests for can_suggest_changes_to_exploration decorator."""
 
@@ -1383,6 +1404,7 @@ def test_normal_user_can_suggest_changes(self):
         self.logout()
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class SuggestChangesDecoratorsTests(test_utils.GenericTestBase):
     """Tests for can_suggest_changes decorator."""
 
@@ -1422,6 +1444,7 @@ def test_normal_user_can_suggest_changes(self):
         self.logout()
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class ResubmitSuggestionDecoratorsTests(test_utils.GenericTestBase):
     """Tests for can_resubmit_suggestion decorator."""
 
@@ -1486,6 +1509,7 @@ def test_non_author_cannot_resubmit_suggestion(self):
         self.logout()
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class DecoratorForAcceptingSuggestionTests(test_utils.GenericTestBase):
     """Tests for get_decorator_for_accepting_suggestion decorator."""
 
@@ -1571,6 +1595,7 @@ def test_viewer_cannot_accept_suggestion(self):
         self.logout()
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class PublishExplorationTests(test_utils.GenericTestBase):
     """Tests for can_publish_exploration decorator."""
 
@@ -1645,6 +1670,7 @@ def test_admin_can_publish_any_exploration(self):
         self.assertEqual(response['exploration_id'], self.private_exp_id)
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class ModifyExplorationRolesTests(test_utils.GenericTestBase):
     """Tests for can_modify_exploration_roles decorator."""
 
@@ -1694,6 +1720,7 @@ def test_admin_can_modify_roles_of_any_exploration(self):
         self.logout()
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class CollectionPublishStatusTests(test_utils.GenericTestBase):
     """Tests can_publish_collection and can_unpublish_collection decorators."""
 
@@ -1810,6 +1837,7 @@ def test_admin_cannot_publish_already_published_collection(self):
         self.logout()
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class AccessLearnerDashboardDecoratorTests(test_utils.GenericTestBase):
     """Tests the decorator can_access_learner_dashboard."""
 
@@ -1848,6 +1876,7 @@ def test_exploration_editor_can_access_learner_dashboard(self):
         self.logout()
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class EditTopicDecoratorTests(test_utils.GenericTestBase):
     """Tests the decorator can_edit_topic."""
 
@@ -1921,6 +1950,7 @@ def test_normal_user_cannot_edit_topic(self):
         self.logout()
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class EditStoryDecoratorTests(test_utils.GenericTestBase):
     """Tests the decorator can_edit_story."""
 
@@ -2006,6 +2036,7 @@ def test_normal_user_cannot_edit_story(self):
         self.logout()
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class AddStoryToTopicTests(test_utils.GenericTestBase):
     """Tests for decorator can_add_new_story_to_topic."""
 
@@ -2105,6 +2136,7 @@ def test_guest_cannot_add_story_to_topic(self):
             'You must be logged in to access this resource.')
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class StoryViewerTests(test_utils.GenericTestBase):
     """Tests for decorator can_access_story_viewer_page."""
 
@@ -2166,6 +2198,7 @@ def test_can_access_story_when_story_and_topic_are_published(self):
                 '/mock_story/%s' % self.story_id, expected_status_int=200)
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class CreateSkillTests(test_utils.GenericTestBase):
     """Tests for decorator can_create_skill."""
 
@@ -2220,6 +2253,7 @@ def test_guest_cannot_add_create_skill(self):
             'You must be logged in to access this resource.')
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class ManageQuestionSkillStatusTests(test_utils.GenericTestBase):
     """Tests for decorator can_manage_question_skill_status."""
 
@@ -2285,6 +2319,7 @@ def test_guest_cannot_manage_question_skill_status(self):
             'You must be logged in to access this resource.')
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class CreateTopicTests(test_utils.GenericTestBase):
     """Tests for decorator can_create_topic."""
 
@@ -2338,6 +2373,7 @@ def test_guest_cannot_create_topic(self):
             'You must be logged in to access this resource.')
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class ManageRightsForTopicTests(test_utils.GenericTestBase):
     """Tests for decorator can_manage_rights_for_topic."""
 
@@ -2396,6 +2432,7 @@ def test_guest_cannot_manage_rights(self):
             'You must be logged in to access this resource.')
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class ChangeTopicPublicationStatusTests(test_utils.GenericTestBase):
     """Tests for decorator can_change_topic_publication_status."""
 
@@ -2471,6 +2508,7 @@ def test_guest_cannot_change_topic_publication_status(self):
             'You must be logged in to access this resource.')
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class PerformCronTaskTests(test_utils.GenericTestBase):
     """Tests for decorator can_perform_cron_tasks."""
 
@@ -2515,6 +2553,7 @@ def test_normal_user_cannot_perform_cron_tasks(self):
         self.logout()
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class EditSkillDecoratorTests(test_utils.GenericTestBase):
     """Tests permissions for accessing the skill editor."""
 
@@ -2589,6 +2628,7 @@ def test_normal_user_can_not_edit_public_skill(self):
                 '/mock_edit_skill/%s' % self.skill_id, expected_status_int=401)
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class EditQuestionDecoratorTests(test_utils.GenericTestBase):
     """Tests the decorator can_edit_question."""
 
@@ -2672,6 +2712,7 @@ def test_any_user_cannot_edit_question(self):
         self.logout()
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class PlayQuestionDecoratorTests(test_utils.GenericTestBase):
     """Tests the decorator can_play_question."""
 
@@ -2704,6 +2745,7 @@ def test_can_play_question_with_valid_question_id(self):
             self.assertEqual(response['question_id'], self.question_id)
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class PlayEntityDecoratorTests(test_utils.GenericTestBase):
     """Test the decorator can_play_entity."""
 
@@ -2793,6 +2835,7 @@ def test_cannot_play_entity_for_invalid_entity(self):
                 'fake_entity_type', 'fake_entity_id'), expected_status_int=404)
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class EditEntityDecoratorTests(test_utils.GenericTestBase):
     username = 'banneduser'
     user_email = 'user@example.com'
@@ -2932,6 +2975,7 @@ def test_cannot_edit_entity_invalid_entity(self):
                 'invalid_entity_type', 'q_id'), expected_status_int=404)
 
 
+# TODO(#10110): Add tests to verify the learner role has correct permissions.
 class SaveExplorationTests(test_utils.GenericTestBase):
     """Tests for can_save_exploration decorator."""