Repository: oppia/oppia

Issue:
Bio in the profile page has to be limited to certain length
Opening this as an issue in light of limiting max length on various input and text area at Oppia. One such example being the Bio in the Preference page. 

**How to produce**
1. Go to Preference Page and edit the bio.
2. Put in a really long bio, around 8000 characters. 
3. Wait for the page to save. 
4. Have a look at Profile page

![screenshot from 2017-03-27 08-56-53](https://cloud.githubusercontent.com/assets/24438869/24339751/57d10010-12cb-11e7-9070-ab74c7bb24c0.png)

**Expected behavior:**
The Bio should be limited so that it cannot affect the site's design


Discussion:
@seanlip It can be solved with `maxlength="x"` in text area 
So, what should be the length of the bio. 
160 would be good? Need your opinion on this.
Perhaps approximately 3/4 of the way down the first paragraph in your screenshot?

I'm not sure, though. Perhaps you could try some possibilities and provide screenshots + a recommendation. I think a reasonable rule of thumb is for the bio card to not extend past two rows of created lesson cards.
@seanlip  
That gives about `92 `characters precisely. Could round off to `100` characters,
**Screenshot**
![screenshot from 2017-03-27 11-13-20](https://cloud.githubusercontent.com/assets/24438869/24342418/6adc87d4-12de-11e7-9b6a-0a742aa39350.png)

Hmm, that's a bit small. Can you show a screenshot that includes two rows of explorations?
@seanlip Sure. I think something between 150 to 200 chars will do.

**Bio description length in the screenshot below:** `200` characters 

![screenshot from 2017-03-27 20-27-54](https://cloud.githubusercontent.com/assets/24438869/24362947/f8fd10a4-132b-11e7-8739-99058b53113f.png)

So you can make it longer, right? Until the left column matches the height of the right column?
@seanlip  Yes, Sure. I guess that will be around 400 characters. Shall I make a PR on it? 
You could, but I encourage you to think more holistically about the parts of the system before doing so.

- The width has to be limited on the Preferences page as well, so that the user is prevented from entering something that is too long.
- How do you propose to handle bio strings in the production server that are already too long? One possible approach is to write a MapReduce job that iterates over all users and checks to see whether there are currently any such strings in production, so that we know whether this is an issue we need to consider or not.
Hi @shaz13 are you still working on this?
@jacobdavis11 Currently busy with exams. Unassigning myself so that interested contributors can work on it.
@seanlip I need help with this issue. Since this issue was open I learned from basics about Map, Reduce functions in Python. Also, gone format of jobs in the codebase. 

My observations are; 
1. First, we need to declare our new job in the `job_registry.py`
2. Then, have to code the MapReduce `job in user_job_one_off.py`
3. It appears in the job section in the admin panel and ready to run.

An algorithm with respect to this issue: 
```
def bio_limit(bio.user_id):
    if len(bio.user_id) > 240:
       return True

map (bio_limit, bio.user_id)
 
```
I am ready to work really hard on it till I get it right. I am quite a newbie in the backend world. Please do guide me through it.
Yep, go ahead and write a one-off backend job that iterates through all users and have map() yield something like (len(bio), user_id) (but just skip bios with length less than a certain number). That way we can get a mapping of len(bio) to the user_ids with a bio of that length. We can then run that to see what the current state of things is, and make the change accordingly. The only catch is that this needs to be in develop by June 3 if you'd like it to be part of the upcoming release.

Also, here's an [example](https://github.com/oppia/oppia/pull/3375) by @Arunabh98 that you can crib from. The reason I suggest having user_id here instead of just "1" is so that you can (later) look at the overly-long bios and see whether their length is warranted.
Thanks! @seanlip 
I would like to take this slow and learn things deeply. However, will push a commit by Saturday for the review.
@seanlip I am feeling all well now :innocent:
So, coming to this issue - As we have MR Job ready, after it runs on the production server we shall set the limit? I guess that is possible in the next release. 

Additionally, the user bio breaks on long single text. This issue's PR will also address that.

![screenshot from 2017-06-20 02-49-27](https://user-images.githubusercontent.com/24438869/27306439-3b6aa5d6-5563-11e7-9e0e-79820bde14a9.png)


Glad to hear you're feeling better, @shaz13!

Yes, I've made a note to run your job in production in the next release, so let's defer this issue until then. Thanks!
Update: we've decided on a max length of 2000 chars.

Resolving patch:
diff --git a/core/controllers/profile.py b/core/controllers/profile.py
--- a/core/controllers/profile.py
+++ b/core/controllers/profile.py
@@ -186,7 +186,12 @@ def put(self):
         data = self.payload.get('data')
 
         if update_type == 'user_bio':
-            user_services.update_user_bio(self.user_id, data)
+            if len(data) > feconf.MAX_BIO_LENGTH_IN_CHARS:
+                raise self.InvalidInputException(
+                    'User bio exceeds maximum character limit: %s'
+                    % feconf.MAX_BIO_LENGTH_IN_CHARS)
+            else:
+                user_services.update_user_bio(self.user_id, data)
         elif update_type == 'subject_interests':
             user_services.update_subject_interests(self.user_id, data)
         elif update_type == 'preferred_language_codes':
diff --git a/feconf.py b/feconf.py
--- a/feconf.py
+++ b/feconf.py
@@ -90,6 +90,9 @@
     TRAINING_JOB_STATUS_PENDING
 ]
 
+# The maximum number of characters allowed for userbio length.
+MAX_BIO_LENGTH_IN_CHARS = 2000
+
 ALLOWED_TRAINING_JOB_STATUS_CHANGES = {
     TRAINING_JOB_STATUS_COMPLETE: [],
     TRAINING_JOB_STATUS_NEW: [TRAINING_JOB_STATUS_PENDING],


Test patch:
diff --git a/core/controllers/profile_test.py b/core/controllers/profile_test.py
--- a/core/controllers/profile_test.py
+++ b/core/controllers/profile_test.py
@@ -598,3 +598,45 @@ def test_save_site_language_no_user(self):
         self.put_json(feconf.SITE_LANGUAGE_DATA_URL, {
             'site_language_code': 'es',
         }, csrf_token)
+
+
+class LongUserBioHandlerTests(test_utils.GenericTestBase):
+    USERNAME_A = 'a'
+    EMAIL_A = 'a@example.com'
+    USERNAME_B = 'b'
+    EMAIL_B = 'b@example.com'
+
+    def test_userbio_within_limit(self):
+        self.signup(self.EMAIL_A, self.USERNAME_A)
+        self.login(self.EMAIL_A)
+        response = self.testapp.get('/preferences')
+        self.assertEqual(response.status_int, 200)
+        csrf_token = self.get_csrf_token_from_response(response)
+        self.put_json('/preferenceshandler/data', {
+            'update_type': 'user_bio',
+            'data': 'I am within 2000 char limit',
+        }, csrf_token)
+        preferences = self.get_json('/preferenceshandler/data')
+        self.assertIsNotNone(preferences)
+        self.assertEqual(
+            preferences['user_bio'], 'I am within 2000 char limit')
+        self.logout()
+
+    def test_user_bio_exceeds_limit(self):
+        self.signup(self.EMAIL_B, self.USERNAME_B)
+        self.login(self.EMAIL_B)
+        response = self.testapp.get('/preferences')
+        self.assertEqual(response.status_int, 200)
+        csrf_token = self.get_csrf_token_from_response(response)
+        user_bio_response = self.put_json(
+            '/preferenceshandler/data', {
+                'update_type': 'user_bio',
+                'data': 'I am not within 2000 char limit' * 200
+            },
+            csrf_token=csrf_token,
+            expect_errors=True,
+            expected_status_int=400)
+        self.assertEqual(user_bio_response['code'], 400)
+        self.assertIn('User bio exceeds maximum character limit: 2000',
+                      user_bio_response['error'])
+        self.logout()