Repository: oppia/oppia

Issue:
Show contributor icons on the gallery cards.
We'd really like to show contributor avatar pictures on the gallery cards! Thanks to work done by @maitbayev in computing the relative contributions by each contributor, this is now finally possible. 

The avatar pictures should appear in order of the number of commits made. More specifically, the most prominent contributor (or the creator of the exploration) should appear by default, but if the picture is hovered over, the top five contributors should be displayed. Clicking on an avatar would take the user to that person's profile page, and would show their username in a tooltip. When the avatar is not hovered over, but there are multiple contributors, a little "+n" should be displayed at the bottom right of the avatar in order to indicate that there are more contributors than just the person shown.

All this is illustrated in the following mock:

![gallery-contribs](https://cloud.githubusercontent.com/assets/10575562/12670394/f3f28baa-c61c-11e5-914e-10c1c6369775.png)

The relevant files to modify are core/templates/dev/head/components/exploration_summary_tile_directive.html and core/templates/dev/head/components/ExplorationSummaryTileDirective.js.



Discussion:
/cc @kevinlee12 

@maitbayev, @kevinlee12 -- this is a fairly important project, since it'd be really nice to make it clear that there are many people behind these explorations. Would either of you be interested in taking it up?

Hi @seanlip 

I am interested in taking it, but I am a little bit afraid of it, because never worked with front-end (html, css), except reading front-end code base of the Oppia and fixing some bugs, literally it is all of my experience. May I try this in this weekends? 

Yep, that sounds fine! I think you'll have to modify the tile definition in core/domain/summary_services.py and then use that information in the frontend to show the contributors dropdown in the exploration_summary_tile.html directive. Let us know if you run into any issues!

Hi @seanlip,
If the profile hovered over, top 5 users will be shown. But when will they be hidden?

When the mouse moves out from the vertical strip containing those users.


Resolving patch:
diff --git a/core/domain/summary_services.py b/core/domain/summary_services.py
--- a/core/domain/summary_services.py
+++ b/core/domain/summary_services.py
@@ -23,6 +23,16 @@
 import utils
 
 
+def get_human_readable_contributors_summary(contributors_summary):
+    contributor_ids = contributors_summary.keys()
+    contributor_usernames = user_services.get_human_readable_user_ids(
+        contributor_ids)
+    return {
+        contributor_usernames[ind]: contributors_summary[contributor_ids[ind]]
+        for ind in xrange(len(contributor_ids))
+    }
+
+
 def get_displayable_exp_summary_dicts_matching_ids(exploration_ids):
     """Given a list of exploration ids, filters the list for
     explorations that are currently non-private and not deleted,
@@ -50,6 +60,9 @@ def get_displayable_exp_summary_dicts_matching_ids(exploration_ids):
                 'status': exploration_summary.status,
                 'ratings': exploration_summary.ratings,
                 'community_owned': exploration_summary.community_owned,
+                'human_readable_contributors_summary':
+                    get_human_readable_contributors_summary(
+                        exploration_summary.contributors_summary),
                 'contributor_names': user_services.get_human_readable_user_ids(
                     exploration_summary.contributor_ids),
                 'tags': exploration_summary.tags,


Test patch:
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
@@ -98,6 +98,13 @@ def setUp(self):
         rights_manager.publish_exploration(self.albert_id, self.EXP_ID_3)
         exp_services.delete_exploration(self.albert_id, self.EXP_ID_3)
 
+    def test_get_human_readable_contributors_summary(self):
+        contributors_summary = {self.albert_id: 10, self.bob_id: 13}
+        self.assertEqual({
+            self.ALBERT_NAME: 10, self.BOB_NAME: 13
+        }, summary_services.get_human_readable_contributors_summary(
+            contributors_summary))
+
     def test_get_displayable_exp_summary_dicts_matching_ids(self):
         # A list of exp_id's are passed in:
         # EXP_ID_1 -- private exploration
@@ -108,27 +115,22 @@ def test_get_displayable_exp_summary_dicts_matching_ids(self):
         displayable_summaries = (
             summary_services.get_displayable_exp_summary_dicts_matching_ids(
                 [self.EXP_ID_1, self.EXP_ID_2, self.EXP_ID_3]))
-
-        self.assertEqual(len(displayable_summaries), 1)
-        self.assertEqual(
-            displayable_summaries[0]['id'], self.EXP_ID_2)
-        self.assertEqual(
-            displayable_summaries[0]['status'],
-            rights_manager.ACTIVITY_STATUS_PUBLIC)
-        self.assertEqual(
-            displayable_summaries[0]['community_owned'], False)
-        self.assertEqual(
-            displayable_summaries[0]['language_code'],
-            feconf.DEFAULT_LANGUAGE_CODE)
-        self.assertEqual(
-            displayable_summaries[0]['category'], 'A category')
-        self.assertEqual(
-            displayable_summaries[0]['ratings'], feconf.get_empty_ratings())
-        self.assertEqual(
-            displayable_summaries[0]['title'], 'Exploration 2 Albert title')
-        self.assertEqual(
-            displayable_summaries[0]['contributor_names'], [self.ALBERT_NAME])
-        self.assertEqual(
-            displayable_summaries[0]['objective'], 'An objective')
-        self.assertEqual(displayable_summaries[0]['num_views'], 0)
+        expected_summary = {
+            'status': u'public',
+            'thumbnail_bg_color': '#05a69a',
+            'community_owned': False,
+            'tags': [],
+            'thumbnail_icon_url': '/images/gallery/thumbnails/Lightbulb.svg',
+            'language_code': feconf.DEFAULT_LANGUAGE_CODE,
+            'human_readable_contributors_summary': {self.ALBERT_NAME: 2},
+            'id': self.EXP_ID_2,
+            'category': u'A category',
+            'ratings': feconf.get_empty_ratings(),
+            'title': u'Exploration 2 Albert title',
+            'num_views': 0,
+            'objective': u'An objective',
+            'contributor_names': [self.ALBERT_NAME]
+        }
         self.assertIn('last_updated_msec', displayable_summaries[0])
+        self.assertDictContainsSubset(expected_summary,
+                                      displayable_summaries[0])