Repository: oppia/oppia

Issue:
Autogenerate a default profile picture for each user who signs up (or use their gravatar picture, if available).
Currently, a user who signs up but doesn't have a profile picture ends up using Oppia's default profile avatar, which is a blue silhouette on a white background.

However, now that we're showing profile pictures in the gallery, it seems odd to see a whole bunch of blue silhouettes -- it looks as though one user created all the explorations, where in reality the different silhouettes represent different users.

Thus, it would be nice to autogenerate a default profile picture for users who have not uploaded one yet, such that different users' default profile pictures are distinct. Some ideas:
- a randomized pattern with a particular colour scheme (cf. GitHub for an example)
- the user's initial, on a circular monochromatic background (where a random colour is chosen for each user)

There may be other possibilities too.

Note that this is mainly a development project, though it has some design elements. I would err towards picking a method of autogeneration that's as simple to implement as possible, since it's straightforward for a user to update their profile picture if they do not like the default one.

If you'd like to take up this issue, please reply to this thread explaining how you plan to auto-generate the pictures before implementing! The implementation would need to work for new users, and we will need to do a one-off job to auto-generate pictures for existing users as well. In the first pass, I suggest focusing on just the first of these, since the second is quite straightforward once the infrastructure for the first has been built.



Discussion:
@seanlip , how about using this api: http://avatars.adorable.io/ . Really cool avatars.
I'll implement this if you think this is alright.

@seanlip Can we use Gravatars? (also used by stackoverflow and Github). Gravatar API requires no authentication, and is based around simple HTTP GET requests. All we need is hash of user's email ID. Here's the link : https://en.gravatar.com/ 

Thanks for the suggestions!

Both services seem reasonable, with the caveat that I think we should fetch the pictures just once when the user signs up, and then store the pictures in the user's profile on Oppia (so that they can subsequently change them whenever they want).

@satwik97, if what you say about Gravatar holds, that actually sounds the most sensible. I feel slightly more iffy about Adorable Avatars -- I don't really like the pics they generate, but maybe they look nicer if they're in the form of a circle. It might be worth asking around to see what other people think.

With regards to implementation, I suggest a flow like the following:
- user signs up
- we do a server-side urlfetch to gravatar; if we get an image, we crop/scale it accordingly, validate it (that it's an image with the right file format and has the right dimensions), and save it as their default profile image; if anything fails during this process (404, validation fail, some other error) then we skip this step and move on
- if the previous step failed, we generate a default image (somehow) and save that as their default profile image

@seanlip I found gravatar identicons really monotonous (for the obvious reasons that they are used by github and stackoverflow) and thus suggested adorable avatars.
@satwik97 Are you working on implementing this or should I do this?

@rishabhsethi I think we should let others decide

@seanlip and others, Hello!

AFAIK these are [Identicon](https://en.wikipedia.org/wiki/Identicon). And rather than using gravatar for this, what we can essentially do is create a hash of each user and then generate identicons based on that hash. More than billion combinations would be possible to generate.

A more simple yet effective approach would be to use something like https://jdenticon.com/ .We can query the backend for the hash value and then set it up on the canvas for particular users.

If this sounds good, then I can implement this work-flow.

I've tried to implement a simple fix for this. Here's the [link](https://github.com/oppia/oppia/pull/1555) . I can implement for functionality if required. 

IMO, the avatars created by Jdenticon are quite nice. I like them better than Adorable. However, one concern I have is that you'd need to create a canvas element and draw within it, so I was wondering: on pages with a bunch of avatars, will creating all these canvas elements lead to performance issues? Just wondering if we should create the avatar once during registration and save it in the backend, or if it's fine to regenerate each time based on the username. It'd be nice to do some benchmarking. (In all this, I'm assuming that jdenticon doesn't make external RPCs in order to generate each icon; if it does, then I lean much more strongly in favour of the create-once-during-registration option.)

Also, just to clarify, I meant to use gravatar for getting actual avatars if they're available (this was also proposed before in #1079). We only use the auto-generated ones as a fallback when we have no better source of truth.

Btw, it looks like this can be factored out into two separate projects:
- using gravatar avatars when a new user signs up, if possible
- benchmarking performance of jdenticon or other similar libraries (vs png files served from backend), in order to answer the questions raised above

I like the Jdendicon icons too.

@seanlip You're right!

So, the jdenticon allows both svgs and canvas as rendering elements on the DOM. You might want to have a look at 'How is the performance of canvas vs SVG?' section on the index page of jidenticon website.

Also, we can get gravatars if the user has set up gravatar profile with that e-mail, else we can use jdenticons.

Also, I would suggest rather than storing them in back-end and retrieving them would cause 'extra load' on our servers. Rather than this, I would suggest that with the incoming request to get all the data, we just convert and make the hash of the e-mail on the front-end.

This would be pretty quick and help us saving extraneous calls to-and-fro from the server.

Here is an example snippet of how we can make jidenticons on the front-end. 

```
<script src="js-md5.js"></script>
<script src="jquery.min.js"></script>
<script src="jdenticon.min.js"></script>
<script>

    var valueToHash = "Test";

    $("body").append($("<canvas/>")
        .attr({ "width": 80, "height": 80 })
        .jdenticon(md5(valueToHash)));

</script>
```

This code snippet is directly taken from their examples, we can modify this and I think it would work fine for our use case. Let me know your thoughts. @wxyxinyu @seanlip 

@CuriousLearner -- Hmm I think your snippet disappeared.

@wxyxinyu Apologies for that! Edited that.

I did see the "How is the performance of canvas vs SVG?" section, but I was asking a different question. IMO, we should still benchmark -- just try loading a thousand of these at a time (vs generating them) and looking at the time difference.

I don't think retrieval would cause much extra load on our servers, since we'll be checking for the existence of the profile picture in the datastore anyway. I think the main thing to be concerned about there is how big the images are. If the images are big, or if making canvas elements is quicker, then I agree about hashing the username (not email) on the frontend. But, in order to know this, we should probably do the benchmarking as described above.

@seanlip Is this done?

@umeshksingla Nope, still on this.

NB: @satwik97 has been working on a fix for this (#1572), but if I remember correctly, he's currently quite busy. So, just to check: @satwik97 -- what are your plans for when you can update the PR? (Trying to figure out whether to assign you to this issue, or leave it open.)

Thanks!

@satwik97, are you still working on this?

Hi @seanlip Actually I am not able to get everything working at once , so I'm a bit exhausted by this issue. Sorry to say but could you assign this issue to someone else . I'd like to work for any different issue. 

OK, no worries! Let's keep your PR (#1572) around, though; it could be used as a starting point for whoever would like to take this up.

Would you like to pick another issue from the issue tracker?

Hey @seanlip --
I looked at @satwik97's PR and noticed the use of a default identicon as a fallback in case the gravatar didn't exist. Instead of having a fixed identicon, we could instead use "jdenticon" as mentioned already in this thread. One way to implement this -- 
- In the "Sign up" page, where the user enters his/her username, we can have a hidden canvas element that generates the jdenticon based on the username.
- When the sign up details are submitted, we send this jdenticon data URI along with the username.
- In case the gravatar isn't found, we store this jdenticon instead of a default avatar.

Sounds good ?

Hm, I thought gravatar would generally return an identicon if the gravatar doesn't exist (and we could use that)? IIRC the default identicon was only meant to be a fallback for when the urlfetch to gravatar fails, which I'd expect to happen very rarely. That way, we can avoid including yet another third-party lib in the codebase.

Oh, right. I thought the email should be registered on Gravatar, but it seems to work for any email hash.


Resolving patch:
diff --git a/core/controllers/profile.py b/core/controllers/profile.py
--- a/core/controllers/profile.py
+++ b/core/controllers/profile.py
@@ -281,6 +281,8 @@ def post(self):
         if feconf.CAN_SEND_EMAILS_TO_USERS and not has_ever_registered:
             email_manager.send_post_signup_email(self.user_id)
 
+        user_services.generate_initial_profile_picture(self.user_id)
+
         self.render_json({})
 
 
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
@@ -17,6 +17,8 @@
 """Services for user data."""
 
 import datetime
+import hashlib
+import imghdr
 import logging
 import re
 
@@ -24,10 +26,18 @@
 import feconf
 import utils
 
+from google.appengine.api import urlfetch
+
 current_user_services = models.Registry.import_current_user_services()
 (user_models,) = models.Registry.import_models([models.NAMES.user])
 
 MAX_USERNAME_LENGTH = 50
+# Size (in px) of the gravatar being retrieved.
+GRAVATAR_SIZE_PX = 150
+# Data url for images/avatar/user_blue_72px.png
+# Generated using utils.convert_png_to_data_url
+DEFAULT_IDENTICON_DATA_URL = (
+    'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEwAAABMCAYAAADHl1ErAAAAAXNSR0IArs4c6QAADhtJREFUeAHt%0AXHlwVdUZ/859jyxmIQESyCaglC0iAgkJIntrIpvKphSwY2ttxbFOp9R/cGGqdhykLaMVO2OtoyRS%0ACEKNEpYKyBIVQ1iNkBhNMCtb8shiQpJ3b7/fTW7m5uUlecu9L4nTM5Pce8895zvf93vnnPud833f%0AEdQLKXb5jsC6%2BuZERZbHKaSMYRbGKERxgpQQUkSIIigEbAmFavlfrUKiVhCVcFa%2BIJEvJOlCcNCA%0AnNKMFQ0o58vEfPgmhS5Mn0ot8n2KIs8lIZJJUfy8almIJqbxhRDSIbJKe2s%2BXvWlV/RcrGwqYGGp%0A20bI1LyaeVmjKMrodp4EycGBAy6MjgsrSxozqG7O5GgxcVREeEigNDAwwBpmsUiRKGu3y1caGlts%0AtQ3yjbOFV6sPnypXTuRXBReU2GLqGprHkUKSRlMIUcD3WyUakGbbt7JYyzf6agpgYfe9O8kui/U8%0AnB7UhJIkUTljwrBTTz449mZKUlyCEBTnjTCKQiX7T5ScfGP3Rf9j5ysny7IyTKXHPwYP690WSXnZ%0AtvcXp71pw1ldQwELm59%2BlyzbX%2BbeNL%2Btscb4EYOyNz2ZWD99wtAFnGdxxoQBefbs85f3rHsjJyiv%0AuGo60wsATe51WZJkWW/LWnXGgDZUEoYAFr58x0B7beOLPHGv5XnFIpGoS0mKOfze%2Bpmj/f2smNR9%0Alm42teQ/8vLRgv0nyuZwVwtm1Ows5BZLSMBz1RkrbnjLiNeAhaWmPWgn%2BxYeejwkRMu9idH7tm%2BY%0AE8/z0EhvmfOmPs9/RQ9tOJx3IKc8lUixkqBKC1nW2vat3u0NXY8Bi1%2B%2Bw6%2BktnETD7%2BnwEB4iP/p%0AL/5xf03U4IBZ3jBkdN2K641Hkn/7YWh17c1JoM3D9PW4kIB1eRkrmjxpyyPAeK4aLttbPuAhOIU5%0AaHpm1cTMZ1ffuRT8eMKED%2BooL6Wd%2B2Bj%2BtnFUGeYyVzJYl3Kc9sld9t2W8Dw%2BWkTWuz2fdxQ9ACr%0A9P3Jfy7%2BZuSw0HnuNtwb5Ysqaw4mPJb5k%2BYW%2BVZuv9xqsaRWZ60%2B7w4vbgEWnrJ1hp3kTO5ZYUPC%0AAnK%2B3bYiitWDWHca7O2yrI6U3r5yR8U1W2MiC2%2BzkLS4ev%2BaY67y1a749VQBYLUIZT/AGhUTduS7%0Af68Y39/AgozgGbxDBsgCmSBbT/Jr710CDMMQPYvHf2DC2Mj9p95efA8TCNKI9MNrEGSALJAJskFG%0AV%2BTocUhigrfbWz5jYtH4VdrAMksBdYVnI8vYJ/8q83hhmW0WEy23WKx39/Qh6LaHQXXA1xBgYc5i%0AsBL4/scCFoC3QCbIBhkhK2TGi65St4CpeharDvgaYoJnIv15GHaFQRBkg4w8p02BzF0VRH6XgEGD%0AV5VS1rOgOvTHCb47wfXvIBtkhE4JmSG7/r3%2B3ilg6toQyx1OUEr7i56lF8zde8gIWVEPSz1g4IyG%0AU8CwkMbaEMudNg3eWd0fXR5khcyQXcXAiYSdAMMWDY/ltVhIY23IdXr8kjqh21%2BzRKvMogUYAAtH%0AQToBhv0sbNFg16GvLaQdmTfjGTJDdmCgYuHQSIfe07pTSqewn3V9z6qrvb1F48Crzx6xNTR4QXoE%0A9tN4c2%2ByfufWqudC3VbmAYzNPwZrkf6dL%2B4LSm5Q9vkrVH79B6qs%2BoH8B1goatAtNCIqmOZOiabw%0A4G5VJMNYREdhDD7ae6J0USsmtEwj3t7DYLCwK83f8WbbzauZP7/kq53SxiY7vfmfC5R24Fv6prTr%0ADVEWgqbfEUlPLY2nlKkxGv%2BmXbFzG7H4/eE8g/tZyO92zbDSPoe1WncUgT14X4G189Nimvjobnrh%0AX6e6BQuo8DCho2crafnzB2n%2BMwe4PL5H5iVgACx4wEltli%2B1sXbA%2BGkNcmCwUN%2BY%2BI%2B3WOjZt3Lp%0Al68cpQoefu6m4%2Bcqae7TWfTfk%2BXuVnWrvA4LFRtUVockjKxKc8sJmMJsWWsiON/U9eJvNmXTtk%2B%2B%0AdYt5Z4WZX0p/bjYtmBbn7LURefaw%2BVuvwoQnBliTYCxu7WFskQb1WROjcvliKlibM/IMAQv8siD0%0A643H6etiGx7NSBbYUlXCbRipgKnme859Ysl4jwwDrnKaV2SjDe%2B0tu9qnZ7KsQWch/YxVpt6KunZ%0AexieUVPDSIJjCC86k3lwyikJ0di%2BMS09/3au2iuMbuDr4mpKN2CIO%2BMLVnpgA4yAlVRX1ziV4fOD%0ArwOv2k2bDM4UVvEkXeaMJ0PyXn3/nCF0HIkAE2ADjICVpChiLArBMcSxsJHPmdmXjCTXiVZRRS19%0AVVTdKd%2BIDA0bYCW1%2BWcRvGiMIN4Vjb1flHb1yrD8rM9LDKOlJ6RhA6ww6au%2BD3A50hcy%2Bt5sRRP8%0AFpSYo8zqsBnDPax13oJ/ltEgafSqam5SU7NdezTtWsHrTzOShg2wYtWP3SQ5wZnNjMZA80Z9s1mk%0AO9CtMakdDRtgJcGnFK3C869D6wY%2BRISp7loGUnROKtKkdtqxYawkzQGXdwNUN0nnrHiXGxxoJf40%0Ae0fEhdpRg29xoZT7RTRsgJV%2B8e0%2BJTdqJIwd4kZpz4pOGWN%2BG5Lq2s38wQHXMzZdq2XiAlllgP2%2B%0AaH6yOX4xGjbAinejlVq0CG9l10T3rNT99wwnf96KMyvNuHMoDR0UaAr5dmwYK1YrhAoYXLtNaa2N%0A6DAW5vFF6qLClGZeeHSyKXRBVMMGWLFaoUZYEPzgTWuxjfC6lROI/RgMb2bZ7JGUaOIcqWEDrDDp%0A50MCBA0YLokDQRgx0p%2BdTezH4PDG88dxI8LotaeneU7AhZo6bPK5hwkVMERYuFDX6yLT2JDx99/f%0ATVY2anibYiOCaPuGuayydDB%2BeUu2U30NG2AlCaFcRAmEo3QqaVLGynm30a6X5sHz2uMWksZH0pHX%0AF9CIYeb/zho2CAqTgoMDvoTXCmJ3EI7isQRuVpw9KYqytyykhxk8qASuJoD84mNTKGvjveSLFQQw%0AUeOaGCNE0Flqvs5o8b/9gZ8xwyMmj404NComZJyrzHtbLjTIjxZNv1X9C/S30pXqRrLVdd4lh7Ej%0AOX4oPfHAOHrzD9Np9l1RZMHnygeJ45kOZXxaPJ6byr6WueotdfAjhI73rGdu2ZXnn5oY7QM2OjZx%0Ax8hw%2BvPjCepf2bUfqJz/Llc1qHpb1OBAiosMpoFB5i%2BtOnLV%2BoTgL9ypYYZ8bZ0tOd6QmuUNbCiF%0AMoN9GPM0TCbeXYoZcgvhr48kOyLlVF6AESf1UwV7G88jBbC/ISqsjzDb62wAC9UmydhoAaz6b/tW%0AcIgQul7ntI8woMNCxQZstQOGSFYeqQriDeGI0Ud47jU2gIEae8kmtlZsWllpB6zNO2UXZwcg3rDX%0AOO0jDbdhEIDoXs1zB6y1A4YHhP3iiuBMOJXh3tfJzuZ/qBbfX65nR5UGqmto8TUL2OoqAgZoWMNE%0AY6KTMhOa%2Bt4ehCDfmxjz8c4X5y3UChp5hVk/j63Vpwuu0zdlNVTIrkuFfC1hkOobO%2B//Qw8LD/an%0A26JDaFRsKI2KCWU76kCaOi6CoHYYnZY9d/DjAzllC/lDmFWz75EFevqdFmGIkbbL9hREsiI40yg/%0A11wGhxex9PlXV%2BjEhatUU99ZQdUzpr%2BH08n1mkb1L%2BfiVf0rGs5Lo2nxkXT3HUPZ0S7WawAhsxrF%0Ay6HPwKJDY/zQqYehAPey1%2BDgDxfsSxkPwZPYaTmU7S7BPWDXkWLafayYLlWaaidW2cASK5nBWzJz%0AOD3AG5YebCgqw5dvP4PoXab1Oveu3znK5xQIOPW31DZchL/6M6vv2sn%2B68scK3b1jDlo%2B6Hv6G87%0A8ij/e1M3cbtiQc3HML4vKZbWrbyTpowe3G1Z7SVH7e7cmHZmGXePSmtI4FhnQfVOAQMBNfhdse/C%0AwvzsO/cf6ykapKlZpq0HCmlzxlc%2B6U2akK5c2XJNf3x4At3D29hdJUTrTnz0wxlwOrEIy5Kugum7%0ABAyEtaGJwKVrH63mrSDn0besEdNTmz9XJ%2B6uGOoL%2BbAr/OXJJIoM77jryx%2Bh0iGL0mSENnc1FDX%2B%0AO6gVWqZ2RfQ9I5oLQgj75fxO/q%2BvpJ9TnXTxlevr6cPjlyj5iUx2bb%2BsZ7UesqlgsayQWf/S8b7b%0AHobC3QWYrv3rZ%2BwuXuhIs88/Y4v8vfWz4BvrdoBpj4BBejWE2W4/yupTGMJ%2BD21O/emf3j1t2bTN%0ArYD8PgWkv7/FflvUwE8uFFelMAg2i8Uy05UTBlwCTAWtLUieJ8XA2MiQIxXX6xNYI%2B6XC3Wep%2Br5%0Axz/Jsszij1qDVREprp4s4DJgGmjaMQzcUA5bgaNkRTbH3GxSf5SEVMoxRBUMlrnHMIB//Arounxb%0AjgZZuWWtSzlokmyGkwWv4Bm8QwZ1GLpxZgUYcquHaRLgQ6A/SobJ4IiGpeyc7RE9ja55V/aKEOID%0A5s/3R8loQjkeVsTzwmmeF2oYuFlamT5xFeII/4qh3LMmgR/oWT4/rEgPhONxWEKifUJW4mWikfpy%0Avr5nBbNIkUQeD8BU7lm9fxyWHgDHA9fYQlzHg/0w/6qjuZzqdKwvb/J9PveiAl4Hz%2BE5q%2B8duKYX%0AHjHSjkf6sXkqWyEZK4QFLIQ51iihWrr2CJKCeE6fzm2pax8Grm8e6acHDffth0YSLdF9CCoZvFye%0A55okRU7gIetV1AkPuRJZSCfZUdefezJMYf3v0MhOwHVzLKlQxAWSRJlQlDr%2BzrPcUjjbGwbyBB2m%0ACKH62/K7KwywjWM8b5CQq%2BH9x%2B%2BCSVZiFKH8eI4ldQQOz4jJ/P/Bt86QcSFPPVqZA50Qu4NwFK7i%0A3tHK7HEEJ5reOFr5fwkK97jkk8ywAAAAAElFTkSuQmCC%0A') #pylint: disable=line-too-long
 
 
 class UserSettings(object):
@@ -201,6 +211,47 @@ def get_users_settings(user_ids):
     return result
 
 
+def generate_initial_profile_picture(user_id):
+    """Generates a profile picture for a new user."""
+    user_email = get_email_from_user_id(user_id)
+    user_gravatar = fetch_gravatar(user_email)
+    update_profile_picture_data_url(user_id, user_gravatar)
+
+
+def get_gravatar_url(email):
+    """Returns the gravatar url for the specified email."""
+    return (
+        'https://www.gravatar.com/avatar/%s?d=identicon&s=%s' %
+        (hashlib.md5(email).hexdigest(), GRAVATAR_SIZE_PX))
+
+
+def fetch_gravatar(email):
+    """Returns the gravatar corresponding to the user's email,
+    or an identicon generated from the email if the gravatar doesn't exist.
+
+    Note: if the call to the gravatar service fails,
+    this returns DEFAULT_IDENTICON_DATA_URL and logs an error.
+    """
+    gravatar_url = get_gravatar_url(email)
+    try:
+        result = urlfetch.fetch(
+            gravatar_url,
+            headers={'Content-Type': 'image/png'},
+            follow_redirects=False)
+    except (urlfetch.InvalidURLError, urlfetch.DownloadError):
+        logging.error('Failed to fetch Gravatar from %s' % gravatar_url)
+    else:
+        if result.status_code == 200:
+            if imghdr.what(None, result.content) == 'png':
+                return utils.convert_png_binary_to_data_url(result.content)
+        else:
+            logging.error(
+                '[Status %s] Failed to fetch Gravatar from %s' %
+                (result.status_code, gravatar_url))
+
+    return DEFAULT_IDENTICON_DATA_URL
+
+
 def get_profile_pictures_by_user_ids(user_ids):
     """Gets the profile_picture_data_url from the domain objects
     representing the settings for the given user_ids.
diff --git a/utils.py b/utils.py
--- a/utils.py
+++ b/utils.py
@@ -17,6 +17,7 @@
 import base64
 import datetime
 import hashlib
+import imghdr
 import json
 import os
 import random
@@ -227,14 +228,19 @@ def get_random_choice(alist):
     return alist[index]
 
 
-def convert_png_to_data_url(filepath):
-    """Converts the png file at filepath to a data URL.
+def convert_png_binary_to_data_url(content):
+    """Converts a png image string (represented by 'content') to a data URL."""
+    if imghdr.what(None, content) == 'png':
+        return 'data:image/png;base64,%s' % urllib.quote(
+            content.encode('base64'))
+    else:
+        raise Exception('The given string does not represent a PNG image.')
 
-    This method is currently used only in tests for RTE extensions.
-    """
+
+def convert_png_to_data_url(filepath):
+    """Converts the png file at filepath to a data URL."""
     file_contents = get_file_contents(filepath, raw_bytes=True, mode='rb')
-    return 'data:image/png;base64,%s' % urllib.quote(
-        file_contents.encode('base64'))
+    return convert_png_binary_to_data_url(file_contents)
 
 
 def camelcase_to_hyphenated(camelcase_str):


Test patch:
diff --git a/core/controllers/galleries_test.py b/core/controllers/galleries_test.py
--- a/core/controllers/galleries_test.py
+++ b/core/controllers/galleries_test.py
@@ -118,7 +118,7 @@ def test_gallery_handler_for_created_explorations(self):
 
         self.login(self.ADMIN_EMAIL)
         response_dict = self.get_json(feconf.GALLERY_DATA_URL)
-        self.assertEqual({
+        self.assertDictContainsSubset({
             'is_admin': True,
             'is_moderator': True,
             'is_super_admin': False,
@@ -126,7 +126,6 @@ def test_gallery_handler_for_created_explorations(self):
             'user_email': self.ADMIN_EMAIL,
             'username': self.ADMIN_USERNAME,
             'search_cursor': None,
-            'profile_picture_data_url': None,
             'preferred_language_codes': [feconf.DEFAULT_LANGUAGE_CODE],
         }, response_dict)
 
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
@@ -260,10 +260,10 @@ def test_get_profile_picture_valid_username(self):
         response_dict = self.get_json(
             '%s%s' % (self.PROFILE_PIC_URL, self.USERNAME)
         )
-        # Although the user has a valid username, they have not yet supplied
-        # a profile picture.
-        self.assertIsNone(
-            response_dict['profile_picture_data_url_for_username'])
+        # Every user must have a profile picture.
+        self.assertEqual(
+            response_dict['profile_picture_data_url_for_username'],
+            user_services.DEFAULT_IDENTICON_DATA_URL)
 
 
 class ProfileDataHandlerTests(test_utils.GenericTestBase):
diff --git a/core/domain/summary_services_test.py b/core/domain/summary_services_test.py
--- a/core/domain/summary_services_test.py
+++ b/core/domain/summary_services_test.py
@@ -142,11 +142,13 @@ def test_get_human_readable_contributors_summary(self):
         self.assertEqual({
             self.ALBERT_NAME: {
                 'num_commits': 10,
-                'profile_picture_data_url': None
+                'profile_picture_data_url': (
+                    user_services.DEFAULT_IDENTICON_DATA_URL)
             },
             self.BOB_NAME: {
                 'num_commits': 13,
-                'profile_picture_data_url': None
+                'profile_picture_data_url': (
+                    user_services.DEFAULT_IDENTICON_DATA_URL)
             }
         }, summary_services.get_human_readable_contributors_summary(
             contributors_summary))
@@ -159,7 +161,8 @@ def test_get_human_readable_contributors_summary(self):
             },
             self.USER_D_NAME: {
                 'num_commits': 2,
-                'profile_picture_data_url': None
+                'profile_picture_data_url': (
+                    user_services.DEFAULT_IDENTICON_DATA_URL)
             }
         }, summary_services.get_human_readable_contributors_summary(
             contributors_summary))
@@ -185,7 +188,8 @@ def test_get_displayable_exp_summary_dicts_matching_ids(self):
             'human_readable_contributors_summary': {
                 self.ALBERT_NAME: {
                     'num_commits': 2,
-                    'profile_picture_data_url': None
+                    'profile_picture_data_url': (
+                        user_services.DEFAULT_IDENTICON_DATA_URL)
                 }
             },
             'id': self.EXP_ID_2,
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
@@ -14,6 +14,9 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import logging
+import os
+
 from core.domain import collection_services
 from core.domain import exp_services
 from core.domain import rights_manager
@@ -22,6 +25,7 @@
 import feconf
 import utils
 
+from google.appengine.api import urlfetch
 
 class UserServicesUnitTests(test_utils.GenericTestBase):
     """Test the user services methods."""
@@ -132,6 +136,70 @@ def test_get_user_id_from_username(self):
         self.assertIsNone(
             user_services.get_user_id_from_username('fakeUsername'))
 
+    def test_fetch_gravatar_success(self):
+        user_email = 'user@example.com'
+        expected_gravatar_filepath = os.path.join(
+            'static', 'images', 'avatar', 'gravatar_example.png')
+        with open(expected_gravatar_filepath, 'r') as f:
+            gravatar = f.read()
+        with self.urlfetch_mock(content=gravatar):
+            profile_picture = user_services.fetch_gravatar(user_email)
+            gravatar_data_url = utils.convert_png_to_data_url(
+                expected_gravatar_filepath)
+            self.assertEqual(profile_picture, gravatar_data_url)
+
+    def test_fetch_gravatar_failure_404(self):
+        user_email = 'user@example.com'
+        error_messages = []
+        def log_mock(message):
+            error_messages.append(message)
+
+        gravatar_url = user_services.get_gravatar_url(user_email)
+        expected_error_message = (
+            '[Status 404] Failed to fetch Gravatar from %s' % gravatar_url)
+        logging_error_mock = test_utils.CallCounter(log_mock)
+        urlfetch_counter = test_utils.CallCounter(urlfetch.fetch)
+        urlfetch_mock_ctx = self.urlfetch_mock(status_code=404)
+        log_swap_ctx = self.swap(logging, 'error', logging_error_mock)
+        fetch_swap_ctx = self.swap(urlfetch, 'fetch', urlfetch_counter)
+        with urlfetch_mock_ctx, log_swap_ctx, fetch_swap_ctx:
+            profile_picture = user_services.fetch_gravatar(user_email)
+            self.assertEqual(urlfetch_counter.times_called, 1)
+            self.assertEqual(logging_error_mock.times_called, 1)
+            self.assertEqual(expected_error_message, error_messages[0])
+            self.assertEqual(
+                profile_picture, user_services.DEFAULT_IDENTICON_DATA_URL)
+
+    def test_fetch_gravatar_failure_exception(self):
+        user_email = 'user@example.com'
+        error_messages = []
+        def log_mock(message):
+            error_messages.append(message)
+
+        gravatar_url = user_services.get_gravatar_url(user_email)
+        expected_error_message = (
+            'Failed to fetch Gravatar from %s' % gravatar_url)
+        logging_error_mock = test_utils.CallCounter(log_mock)
+        urlfetch_fail_mock = test_utils.FailingFunction(
+            urlfetch.fetch, urlfetch.InvalidURLError,
+            test_utils.FailingFunction.INFINITY)
+        log_swap_ctx = self.swap(logging, 'error', logging_error_mock)
+        fetch_swap_ctx = self.swap(urlfetch, 'fetch', urlfetch_fail_mock)
+        with log_swap_ctx, fetch_swap_ctx:
+            profile_picture = user_services.fetch_gravatar(user_email)
+            self.assertEqual(logging_error_mock.times_called, 1)
+            self.assertEqual(expected_error_message, error_messages[0])
+            self.assertEqual(
+                profile_picture, user_services.DEFAULT_IDENTICON_DATA_URL)
+
+    def test_default_identicon_data_url(self):
+        identicon_filepath = os.path.join(
+            'static', 'images', 'avatar', 'user_blue_72px.png')
+        identicon_data_url = utils.convert_png_to_data_url(
+            identicon_filepath)
+        self.assertEqual(
+            identicon_data_url, user_services.DEFAULT_IDENTICON_DATA_URL)
+
     def test_set_and_get_user_email_preferences(self):
         user_id = 'someUser'
         username = 'username'
diff --git a/core/tests/test_utils.py b/core/tests/test_utils.py
--- a/core/tests/test_utils.py
+++ b/core/tests/test_utils.py
@@ -39,6 +39,9 @@
 import main
 import utils
 
+from google.appengine.api import apiproxy_stub
+from google.appengine.api import apiproxy_stub_map
+
 (exp_models,) = models.Registry.import_models([models.NAMES.exploration])
 current_user_services = models.Registry.import_current_user_services()
 
@@ -59,6 +62,33 @@ def empty_environ():
     os.environ['DEFAULT_VERSION_HOSTNAME'] = '%s:%s' % (
         os.environ['HTTP_HOST'], os.environ['SERVER_PORT'])
 
+class URLFetchServiceMock(apiproxy_stub.APIProxyStub):
+    """Mock for google.appengine.api.urlfetch"""
+
+    def __init__(self, service_name='urlfetch'):
+        super(URLFetchServiceMock, self).__init__(service_name)
+        self.return_values = {}
+        self.response = None
+        self.request = None
+
+    def set_return_values(self, content='', status_code=200, headers=None):
+        self.return_values['content'] = content
+        self.return_values['status_code'] = status_code
+        self.return_values['headers'] = headers
+
+    def _Dynamic_Fetch(self, request, response): # pylint: disable=invalid-name
+        return_values = self.return_values
+        response.set_content(return_values.get('content', ''))
+        response.set_statuscode(return_values.get('status_code', 200))
+        for header_key, header_value in return_values.get(
+                'headers', {}).items():
+            new_header = response.add_header()
+            new_header.set_key(header_key)
+            new_header.set_value(header_value)
+
+        self.request = request
+        self.response = response
+
 
 class TestBase(unittest.TestCase):
     """Base class for all tests."""
@@ -250,19 +280,22 @@ def get_csrf_token_from_response(self, response):
     def signup(self, email, username):
         """Complete the signup process for the user with the given username."""
         self.login(email)
-
-        response = self.testapp.get(feconf.SIGNUP_URL)
-        self.assertEqual(response.status_int, 200)
-        csrf_token = self.get_csrf_token_from_response(response)
-        response = self.testapp.post(feconf.SIGNUP_DATA_URL, {
-            'csrf_token': csrf_token,
-            'payload': json.dumps({
-                'username': username,
-                'agreed_to_terms': True
+        # Signup uses a custom urlfetch mock (URLFetchServiceMock), instead
+        # of the stub provided by testbed. This custom mock is disabled
+        # immediately once the signup is complete. This is done to avoid external 
+        # calls being made to Gravatar when running the backend tests.
+        with self.urlfetch_mock():
+            response = self.testapp.get(feconf.SIGNUP_URL)
+            self.assertEqual(response.status_int, 200)
+            csrf_token = self.get_csrf_token_from_response(response)
+            response = self.testapp.post(feconf.SIGNUP_DATA_URL, {
+                'csrf_token': csrf_token,
+                'payload': json.dumps({
+                    'username': username,
+                    'agreed_to_terms': True
+                })
             })
-        })
-        self.assertEqual(response.status_int, 200)
-
+            self.assertEqual(response.status_int, 200)
         self.logout()
 
     def set_config_property(self, config_obj, new_config_value):
@@ -564,6 +597,38 @@ def tearDown(self):
     def _get_all_queue_names(self):
         return [q['name'] for q in self.taskqueue_stub.GetQueues()]
 
+    @contextlib.contextmanager
+    def urlfetch_mock(
+            self, content='', status_code=200, headers=None):
+        """Enables the custom urlfetch mock (URLFetchServiceMock) within the
+        context of a 'with' statement.
+
+        This mock is currently used for signup to prevent external HTTP
+        requests to fetch the Gravatar profile picture for new users while the
+        backend tests are being run.
+
+        args:
+          - content: Response content or body.
+          - status_code: Response status code.
+          - headers: Response headers.
+        """
+        if headers is None:
+            response_headers = {}
+        else:
+            response_headers = headers
+        self.testbed.init_urlfetch_stub(enable=False)
+        urlfetch_mock = URLFetchServiceMock()
+        apiproxy_stub_map.apiproxy.RegisterStub('urlfetch', urlfetch_mock)
+        urlfetch_mock.set_return_values(
+            content=content, status_code=status_code, headers=response_headers)
+        try:
+            yield
+        finally:
+            # Disables the custom mock
+            self.testbed.init_urlfetch_stub(enable=False)
+            # Enables the testbed urlfetch mock
+            self.testbed.init_urlfetch_stub()
+
     def count_jobs_in_taskqueue(self, queue_name=None):
         """Counts the jobs in the given queue. If queue_name is None,
         defaults to counting the jobs in all queues available.