Repository: oppia/oppia

Issue:
Create a MR job for getting the current distribution of username lengths in production.
As discussed in PR #3270, we need to get the current distribution of usernames in production to decide the width of the subscription card. Based on the distribution, the optimum length for the subscription card will be decided so that's it's not too short (that almost all usernames are truncated) nor too long.


Discussion:
@jaredsilver @seanlip what exactly are the statistics I should get? Should I get the 90th percentile of username lengths or should I get the distribution of username lengths -- like there are 20 users having a username of length 7. I feel getting the distribution would allow us to make a better decision but I would like to know your views on this?
Distribution is fine, and in fact should be quite straightforward using mapreduce. :)

Resolving patch:
diff --git a/core/domain/user_jobs_one_off.py b/core/domain/user_jobs_one_off.py
--- a/core/domain/user_jobs_one_off.py
+++ b/core/domain/user_jobs_one_off.py
@@ -68,6 +68,25 @@ def reduce(key, version_and_exp_ids):
                     edited_exploration_ids))
 
 
+class UsernameLengthDistributionOneOffJob(jobs.BaseMapReduceJobManager):
+    """One-off job for calculating the distribution of username lengths."""
+
+    @classmethod
+    def entity_classes_to_map_over(cls):
+        return [user_models.UserSettingsModel]
+
+    @staticmethod
+    def map(item):
+        if item.username is not None:
+            yield (len(item.username), 1)
+
+    @staticmethod
+    def reduce(key, stringified_username_counter):
+        username_counter = [
+            ast.literal_eval(v) for v in stringified_username_counter]
+        yield (key, len(username_counter))
+
+
 class DashboardSubscriptionsOneOffJob(jobs.BaseMapReduceJobManager):
     """One-off job for subscribing users to explorations, collections, and
     feedback threads.
diff --git a/core/jobs_registry.py b/core/jobs_registry.py
--- a/core/jobs_registry.py
+++ b/core/jobs_registry.py
@@ -30,6 +30,7 @@
 # on the admin dashboard.
 ONE_OFF_JOB_MANAGERS = [
     user_jobs_one_off.DashboardSubscriptionsOneOffJob,
+    user_jobs_one_off.UsernameLengthDistributionOneOffJob,
     exp_jobs_one_off.IndexAllExplorationsJobManager,
     exp_jobs_one_off.ExpSummariesCreationOneOffJob,
     exp_jobs_one_off.ExplorationValidityJobManager,


Test patch:
diff --git a/core/domain/user_jobs_one_off_test.py b/core/domain/user_jobs_one_off_test.py
--- a/core/domain/user_jobs_one_off_test.py
+++ b/core/domain/user_jobs_one_off_test.py
@@ -17,6 +17,7 @@
 """Tests for user-related one-off computations."""
 
 import datetime
+import re
 
 from core.domain import collection_domain
 from core.domain import collection_services
@@ -145,6 +146,83 @@ def test_for_duplicates(self):
             [self.EXP_ID_2])
 
 
+class UsernameLengthDistributionOneOffJobTests(test_utils.GenericTestBase):
+    """Tests for the one-off username length distribution job."""
+    USER_A_EMAIL = 'a@example.com'
+    USER_A_USERNAME = 'a'
+    USER_B_EMAIL = 'ab@example.com'
+    USER_B_USERNAME = 'ab'
+    USER_C_EMAIL = 'bc@example.com'
+    USER_C_USERNAME = 'bc'
+    USER_D_EMAIL = 'bcd@example.com'
+    USER_D_USERNAME = 'bcd'
+
+    def _run_one_off_job(self):
+        """Runs the one-off MapReduce job."""
+        job_id = (
+            user_jobs_one_off.UsernameLengthDistributionOneOffJob.create_new())
+        user_jobs_one_off.UsernameLengthDistributionOneOffJob.enqueue(job_id)
+        self.assertEqual(
+            self.count_jobs_in_taskqueue(
+                queue_name=taskqueue_services.QUEUE_NAME_DEFAULT),
+            1)
+        self.process_and_flush_pending_tasks()
+        stringified_output = (
+            user_jobs_one_off.UsernameLengthDistributionOneOffJob.get_output(
+                job_id))
+
+        output = {}
+        for stringified_distribution in stringified_output:
+            value = re.findall(r'\d+', stringified_distribution)
+            # output['username length'] = number of users
+            output[value[0]] = int(value[1])
+
+        return output
+
+    def test_null_case(self):
+        """Tests the case when there are no signed up users but there is one
+        default user having the username - 'tmpsuperadm1n'.
+        """
+        output = self._run_one_off_job()
+        # number of users = 1.
+        # length of usernames = 13 (tmpsuperadm1n).
+        self.assertEqual(output['13'], 1)
+
+    def test_single_user_case(self):
+        """Tests the case when there is only one signed up user and a default
+        user - 'tmpsuperadm1n'.
+        """
+        self.signup(self.USER_A_EMAIL, self.USER_A_USERNAME)
+        output = self._run_one_off_job()
+        # number of users = 2.
+        # length of usernames = 13 (tmpsuperadm1n), 1 (a).
+        self.assertEqual(output['13'], 1)
+        self.assertEqual(output['1'], 1)
+
+    def test_multiple_users_case(self):
+        """Tests the case when there are multiple signed up users and a
+        default user - 'tmpsuperadm1n'.
+        """
+        self.signup(self.USER_A_EMAIL, self.USER_A_USERNAME)
+        self.signup(self.USER_B_EMAIL, self.USER_B_USERNAME)
+        output = self._run_one_off_job()
+        # number of users = 3
+        # length of usernames = 13 (tmpsuperadm1n), 2 (ab), 1 (a).
+        self.assertEqual(output['13'], 1)
+        self.assertEqual(output['2'], 1)
+        self.assertEqual(output['1'], 1)
+
+        self.signup(self.USER_C_EMAIL, self.USER_C_USERNAME)
+        self.signup(self.USER_D_EMAIL, self.USER_D_USERNAME)
+        output = self._run_one_off_job()
+        # number of users = 5
+        # length of usernames = 13 (tmpsuperadm1n), 3 (bcd), 2 (ab, bc), 1 (a).
+        self.assertEqual(output['13'], 1)
+        self.assertEqual(output['3'], 1)
+        self.assertEqual(output['2'], 2)
+        self.assertEqual(output['1'], 1)
+
+
 class DashboardSubscriptionsOneOffJobTests(test_utils.GenericTestBase):
     """Tests for the one-off dashboard subscriptions job."""
     EXP_ID_1 = 'exp_id_1'