From 0e180bd2c24da8c1cdf8b3e10a54972c9c2fc3bc Mon Sep 17 00:00:00 2001 From: Daniel Friedman Date: Mon, 31 Aug 2015 13:57:44 -0400 Subject: [PATCH] Add membership thumbnails to team cards. TNL-3172 --- lms/djangoapps/teams/serializers.py | 4 +- .../teams/js/collections/team_membership.js | 2 +- .../static/teams/js/models/team_membership.js | 3 +- .../teams/js/spec/views/my_teams_spec.js | 2 +- .../teams/js/spec/views/team_card_spec.js | 122 ++++++++++++++++-- .../teams/static/teams/js/views/team_card.js | 33 +++-- .../team-membership-details.underscore | 9 ++ lms/djangoapps/teams/views.py | 2 +- 8 files changed, 147 insertions(+), 30 deletions(-) create mode 100644 lms/djangoapps/teams/static/teams/templates/team-membership-details.underscore diff --git a/lms/djangoapps/teams/serializers.py b/lms/djangoapps/teams/serializers.py index 79183e464c..b33d373287 100644 --- a/lms/djangoapps/teams/serializers.py +++ b/lms/djangoapps/teams/serializers.py @@ -35,8 +35,8 @@ class UserMembershipSerializer(serializers.ModelSerializer): class Meta(object): """Defines meta information for the ModelSerializer.""" model = CourseTeamMembership - fields = ("user", "date_joined") - read_only_fields = ("date_joined",) + fields = ("user", "date_joined", "last_activity_at") + read_only_fields = ("date_joined", "last_activity_at") class CourseTeamSerializer(serializers.ModelSerializer): diff --git a/lms/djangoapps/teams/static/teams/js/collections/team_membership.js b/lms/djangoapps/teams/static/teams/js/collections/team_membership.js index bfb29bffc4..77f6fa4aa0 100644 --- a/lms/djangoapps/teams/static/teams/js/collections/team_membership.js +++ b/lms/djangoapps/teams/static/teams/js/collections/team_membership.js @@ -14,7 +14,7 @@ this.server_api = _.extend( { - expand: 'team', + expand: 'team,user', username: this.username, course_id: function () { return encodeURIComponent(self.course_id); } }, diff --git a/lms/djangoapps/teams/static/teams/js/models/team_membership.js b/lms/djangoapps/teams/static/teams/js/models/team_membership.js index 7bd5cfdc4a..ce9963fede 100644 --- a/lms/djangoapps/teams/static/teams/js/models/team_membership.js +++ b/lms/djangoapps/teams/static/teams/js/models/team_membership.js @@ -7,11 +7,12 @@ var TeamMembership = Backbone.Model.extend({ defaults: { date_joined: '', + last_activity_at: '', team: null, user: null }, - parse: function (response, options) { + parse: function (response) { response.team = new TeamModel(response.team); return response; } diff --git a/lms/djangoapps/teams/static/teams/js/spec/views/my_teams_spec.js b/lms/djangoapps/teams/static/teams/js/spec/views/my_teams_spec.js index eb0ae73a1a..beb93e4b80 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/views/my_teams_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/views/my_teams_spec.js @@ -65,7 +65,7 @@ define([ requests, TeamSpecHelpers.testContext.teamMembershipsUrl, { - expand : 'team', + expand : 'team,user', username : TeamSpecHelpers.testContext.userInfo.username, course_id : TeamSpecHelpers.testContext.courseID, page : '1', diff --git a/lms/djangoapps/teams/static/teams/js/spec/views/team_card_spec.js b/lms/djangoapps/teams/static/teams/js/spec/views/team_card_spec.js index c83da4b963..6de4f1d9f2 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/views/team_card_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/views/team_card_spec.js @@ -3,21 +3,24 @@ define(['jquery', 'teams/js/views/team_card', 'teams/js/models/team'], function ($, _, TeamCardView, Team) { + 'use strict'; + describe('TeamCardView', function () { var createTeamCardView, view; createTeamCardView = function () { var model = new Team({ - id: 'test-team', - name: 'Test Team', - is_active: true, - course_id: 'test/course/id', - topic_id: 'test-topic', - description: 'A team for testing', - last_activity_at: "2015-08-21T18:53:01.145Z", - country: 'us', - language: 'en' - }), - teamCardClass = TeamCardView.extend({ + id: 'test-team', + name: 'Test Team', + is_active: true, + course_id: 'test/course/id', + topic_id: 'test-topic', + description: 'A team for testing', + last_activity_at: "2015-08-21T18:53:01.145Z", + country: 'us', + language: 'en', + membership: [] + }), + TeamCardClass = TeamCardView.extend({ maxTeamSize: '100', srInfo: { id: 'test-sr-id', @@ -26,7 +29,7 @@ define(['jquery', countries: {us: 'United States of America'}, languages: {en: 'English'} }); - return new teamCardClass({ + return new TeamCardClass({ model: model }); }; @@ -50,6 +53,101 @@ define(['jquery', it('navigates to the associated team page when its action button is clicked', function () { expect(view.$('.action').attr('href')).toEqual('#teams/test-topic/test-team'); }); + + describe('Profile Image Thumbnails', function () { + /** + * Takes an array of objects representing team + * members, each having the keys 'username', + * 'image_url', and 'last_activity', and sets the + * teams membership accordingly and re-renders the + * view. + */ + var setMemberships, expectThumbnailsOrder; + + setMemberships = function (memberships) { + view.model.set({ + membership: _.map(memberships, function (m) { + return { + user: {username: m.username, profile_image: {image_url_small: m.image_url}}, + last_activity_at: m.last_activity + }; + }) + }); + view.render(); + }; + + /** + * Takes an array of objects representing team + * members, each having the keys 'username' and + * 'image_url', and expects that the image thumbnails + * rendered on the team card match, in order, the + * members of the provided list. + */ + expectThumbnailsOrder = function (members) { + var thumbnails = view.$('.item-member-thumb img'); + expect(thumbnails.length).toBe(members.length); + thumbnails.each(function (index, imgEl) { + expect(thumbnails.eq(index).attr('alt')).toBe(members[index].username); + expect(thumbnails.eq(index).attr('src')).toBe(members[index].image_url); + }); + }; + + it('displays no thumbnails for an empty team', function () { + view.model.set({membership: []}); + view.render(); + expect(view.$('.item-member-thumb').length).toBe(0); + }); + + it('displays thumbnails for a nonempty team', function () { + var users = [ + { + username: 'user_1', image_url: 'user_1_image', + last_activity: new Date("2010/1/1").toString() + }, { + username: 'user_2', image_url: 'user_2_image', + last_activity: new Date("2011/1/1").toString() + } + ]; + setMemberships(users); + expectThumbnailsOrder([ + {username: 'user_2', image_url: 'user_2_image'}, + {username: 'user_1', image_url: 'user_1_image'}, + ]); + }); + + it('displays thumbnails and an ellipsis for a team with greater than 5 members', function () { + var users = [ + { + username: 'user_1', image_url: 'user_1_image', + last_activity: new Date("2001/1/1").toString() + }, { + username: 'user_2', image_url: 'user_2_image', + last_activity: new Date("2006/1/1").toString() + }, { + username: 'user_3', image_url: 'user_3_image', + last_activity: new Date("2003/1/1").toString() + }, { + username: 'user_4', image_url: 'user_4_image', + last_activity: new Date("2002/1/1").toString() + }, { + username: 'user_5', image_url: 'user_5_image', + last_activity: new Date("2005/1/1").toString() + }, { + username: 'user_6', image_url: 'user_6_image', + last_activity: new Date("2004/1/1").toString() + } + ]; + setMemberships(users); + expectThumbnailsOrder([ + {username: 'user_2', image_url: 'user_2_image'}, + {username: 'user_5', image_url: 'user_5_image'}, + {username: 'user_6', image_url: 'user_6_image'}, + {username: 'user_3', image_url: 'user_3_image'}, + {username: 'user_4', image_url: 'user_4_image'}, + ]); + expect(view.$('.item-member-thumb').eq(-1)).toHaveText('and others…'); + }); + }); }); } ); diff --git a/lms/djangoapps/teams/static/teams/js/views/team_card.js b/lms/djangoapps/teams/static/teams/js/views/team_card.js index afaf8200e8..2151018d85 100644 --- a/lms/djangoapps/teams/static/teams/js/views/team_card.js +++ b/lms/djangoapps/teams/static/teams/js/views/team_card.js @@ -7,34 +7,43 @@ 'jquery.timeago', 'js/components/card/views/card', 'teams/js/views/team_utils', + 'text!teams/templates/team-membership-details.underscore', 'text!teams/templates/team-country-language.underscore', 'text!teams/templates/team-activity.underscore' - ], function (Backbone, _, gettext, timeago, CardView, TeamUtils, teamCountryLanguageTemplate, teamActivityTemplate) { + ], function ( + Backbone, + _, + gettext, + timeago, + CardView, + TeamUtils, + teamMembershipDetailsTemplate, + teamCountryLanguageTemplate, + teamActivityTemplate + ) { var TeamMembershipView, TeamCountryLanguageView, TeamActivityView, TeamCardView; TeamMembershipView = Backbone.View.extend({ tagName: 'div', className: 'team-members', - template: _.template( - '<%= membership_message %>' + - '' - ), + template: _.template(teamMembershipDetailsTemplate), initialize: function (options) { this.maxTeamSize = options.maxTeamSize; }, render: function () { - var memberships = this.model.get('membership'), + var allMemberships = _(this.model.get('membership')) + .sortBy(function (member) {return new Date(member.last_activity_at);}).reverse(), + displayableMemberships = allMemberships.slice(0, 5), maxMemberCount = this.maxTeamSize; this.$el.html(this.template({ - membership_message: TeamUtils.teamCapacityText(memberships.length, maxMemberCount) + membership_message: TeamUtils.teamCapacityText(allMemberships.length, maxMemberCount), + memberships: displayableMemberships, + has_additional_memberships: displayableMemberships.length < allMemberships.length, + // Translators: "and others" refers to fact that additional members of a team exist that are not displayed. + sr_message: gettext('and others') })); - _.each(memberships, function (membership) { - this.$('list-member-thumbs').append( - '
  • ' + membership.user.username + '
  • ' - ); - }, this); return this; } }); diff --git a/lms/djangoapps/teams/static/teams/templates/team-membership-details.underscore b/lms/djangoapps/teams/static/teams/templates/team-membership-details.underscore new file mode 100644 index 0000000000..ae91aef5f0 --- /dev/null +++ b/lms/djangoapps/teams/static/teams/templates/team-membership-details.underscore @@ -0,0 +1,9 @@ +<%= membership_message %> + diff --git a/lms/djangoapps/teams/views.py b/lms/djangoapps/teams/views.py index 325a076900..9bcb85ea3f 100644 --- a/lms/djangoapps/teams/views.py +++ b/lms/djangoapps/teams/views.py @@ -97,7 +97,7 @@ class TeamsDashboardView(View): team_memberships_page = Paginator(team_memberships, TEAM_MEMBERSHIPS_PER_PAGE).page(1) team_memberships_serializer = PaginatedMembershipSerializer( instance=team_memberships_page, - context={'expand': ('team',)}, + context={'expand': ('team', 'user'), 'request': request}, ) context = {