From a56e6bf743f91efdf2c10f12cceeb9a9b59c1d65 Mon Sep 17 00:00:00 2001 From: muzaffaryousaf Date: Fri, 31 Jul 2015 18:36:48 +0500 Subject: [PATCH] Team details page. TNL-1906 --- common/test/acceptance/pages/lms/teams.py | 60 ++++++ .../test/acceptance/tests/lms/test_teams.py | 179 +++++++++++++++++- .../teams/static/teams/js/collections/team.js | 1 + .../teams/js/spec/views/team_profile_spec.js | 171 +++++++++++++++-- .../teams/js/spec/views/teams_tab_spec.js | 2 +- .../teams/static/teams/js/views/team_card.js | 13 +- .../static/teams/js/views/team_profile.js | 46 ++++- .../teams/static/teams/js/views/team_utils.js | 44 +++++ .../teams/static/teams/js/views/teams.js | 25 +-- .../teams/static/teams/js/views/teams_tab.js | 20 +- .../teams/templates/team-member.underscore | 6 + .../teams/templates/team-profile.underscore | 73 ++++++- .../teams/templates/teams/teams.html | 3 +- lms/djangoapps/teams/views.py | 1 + lms/static/sass/elements/_controls.scss | 13 ++ lms/static/sass/shared/_tooltips.scss | 51 +++++ lms/static/sass/views/_teams.scss | 58 ++++++ 17 files changed, 702 insertions(+), 64 deletions(-) create mode 100644 lms/djangoapps/teams/static/teams/js/views/team_utils.js create mode 100644 lms/djangoapps/teams/static/teams/templates/team-member.underscore diff --git a/common/test/acceptance/pages/lms/teams.py b/common/test/acceptance/pages/lms/teams.py index 1a37b96c51..be6fa55cd7 100644 --- a/common/test/acceptance/pages/lms/teams.py +++ b/common/test/acceptance/pages/lms/teams.py @@ -252,3 +252,63 @@ class TeamPage(CoursePage, PaginatedUIMixin): def team_description(self): """Get the team's description as displayed in the page header""" return self.q(css=TEAMS_HEADER_CSS + ' .page-description')[0].text + + @property + def team_members_present(self): + """Verifies that team members are present""" + return self.q(css='.page-content-secondary .team-members .team-member').present + + @property + def team_capacity_text(self): + """Returns team capacity text""" + return self.q(css='.page-content-secondary .team-capacity :last-child').text[0] + + @property + def team_location(self): + """ Returns team location/country. """ + return self.q(css='.page-content-secondary .team-country :last-child').text[0] + + @property + def team_language(self): + """ Returns team location/country. """ + return self.q(css='.page-content-secondary .team-language :last-child').text[0] + + @property + def team_user_membership_text(self): + """Returns the team membership text""" + query = self.q(css='.page-content-secondary > .team-user-membership-status') + return query.text[0] if query.present else '' + + @property + def team_leave_link_present(self): + """Verifies that team leave link is present""" + return self.q(css='.leave-team-link').present + + @property + def team_invite_section_present(self): + """Verifies that invite section is present""" + return self.q(css='.page-content-secondary .invite-team').present + + @property + def team_members(self): + """Returns the number of team members in this team""" + return len(self.q(css='.page-content-secondary .team-member')) + + def click_first_profile_image(self): + """Clicks on first team member's profile image""" + self.q(css='.page-content-secondary .members-info > .team-member').first.click() + + @property + def first_member_username(self): + """Returns the username of team member""" + return self.q(css='.page-content-secondary .tooltip-custom').text[0] + + @property + def team_invite_help_text(self): + """Returns the team invite help text""" + return self.q(css='.page-content-secondary .invite-text').text[0] + + @property + def team_invite_url(self): + """Returns the url of invite link box""" + return self.q(css='.page-content-secondary .invite-link-input').attrs('value')[0] diff --git a/common/test/acceptance/tests/lms/test_teams.py b/common/test/acceptance/tests/lms/test_teams.py index 352ccdc95e..8ef68ed31a 100644 --- a/common/test/acceptance/tests/lms/test_teams.py +++ b/common/test/acceptance/tests/lms/test_teams.py @@ -16,6 +16,7 @@ from ...fixtures.discussion import ( ) from ...pages.lms.auto_auth import AutoAuthPage from ...pages.lms.course_info import CourseInfoPage +from ...pages.lms.learner_profile import LearnerProfilePage from ...pages.lms.tab_nav import TabNavPage from ...pages.lms.teams import TeamsPage, MyTeamsPage, BrowseTopicsPage, BrowseTeamsPage, CreateTeamPage, TeamPage @@ -40,7 +41,9 @@ class TeamsTabBase(UniqueCourseTest): 'course_id': self.course_id, 'topic_id': topic['id'], 'name': 'Team {}'.format(i), - 'description': 'Description {}'.format(i) + 'description': 'Description {}'.format(i), + 'language': 'aa', + 'country': 'AF' } response = self.course_fixture.session.post( LMS_BASE_URL + '/api/team/v0/teams/', @@ -711,6 +714,7 @@ class CreateTeamTest(TeamsTabBase): When I fill all the fields present with appropriate data And I click Create button Then I should see the page for my team + And I should see the message that says "You are member of this team" """ self.verify_and_navigate_to_create_team_page() @@ -722,6 +726,7 @@ class CreateTeamTest(TeamsTabBase): team_page.wait_for_page() self.assertEqual(team_page.team_name, self.team_name) self.assertEqual(team_page.team_description, 'The Avengers are a fictional team of superheroes.') + self.assertEqual(team_page.team_user_membership_text, 'You are a member of this team.') def test_user_can_cancel_the_team_creation(self): """ @@ -748,9 +753,37 @@ class TeamPageTest(TeamsTabBase): def setUp(self): super(TeamPageTest, self).setUp() self.topic = {u"name": u"Example Topic", u"id": "example_topic", u"description": "Description"} - self.set_team_configuration({'course_id': self.course_id, 'max_team_size': 10, 'topics': [self.topic]}) - self.team = self.create_teams(self.topic, 1)[0] - self.team_page = TeamPage(self.browser, self.course_id, self.team) + + def _set_team_configuration_and_membership( + self, + max_team_size=10, + membership_team_index=0, + visit_team_index=0, + create_membership=True, + another_user=False): + """ + Set team configuration. + + Arguments: + max_team_size (int): number of users a team can have + membership_team_index (int): index of team user will join + visit_team_index (int): index of team user will visit + create_membership (bool): whether to create membership or not + another_user (bool): another user to visit a team + """ + #pylint: disable=attribute-defined-outside-init + self.set_team_configuration( + {'course_id': self.course_id, 'max_team_size': max_team_size, 'topics': [self.topic]} + ) + self.teams = self.create_teams(self.topic, 2) + + if create_membership: + self.create_membership(self.user_info['username'], self.teams[membership_team_index]['id']) + + if another_user: + AutoAuthPage(self.browser, course_id=self.course_id).visit() + + self.team_page = TeamPage(self.browser, self.course_id, self.teams[visit_team_index]) def setup_thread(self): """ @@ -758,7 +791,7 @@ class TeamPageTest(TeamsTabBase): """ thread = Thread( id="test_thread_{}".format(uuid4().hex), - commentable_id=self.team['discussion_topic_id'], + commentable_id=self.teams[0]['discussion_topic_id'], body="Dummy text body." ) thread_fixture = MultipleThreadFixture([thread]) @@ -787,7 +820,7 @@ class TeamPageTest(TeamsTabBase): """ thread = self.setup_thread() self.team_page.visit() - self.assertEqual(self.team_page.discussion_id, self.team['discussion_topic_id']) + self.assertEqual(self.team_page.discussion_id, self.teams[0]['discussion_topic_id']) discussion = self.team_page.discussion_page self.assertTrue(discussion.is_browser_on_page()) self.assertTrue(discussion.is_discussion_expanded()) @@ -809,7 +842,7 @@ class TeamPageTest(TeamsTabBase): And I should see the existing thread And I should see controls to change the state of the discussion """ - self.create_membership(self.user_info['username'], self.team['id']) + self._set_team_configuration_and_membership() self.verify_teams_discussion_permissions(True) @ddt.data(True, False) @@ -825,10 +858,142 @@ class TeamPageTest(TeamsTabBase): And I should see the team's thread And I should not see controls to change the state of the discussion """ + self._set_team_configuration_and_membership(create_membership=False) self.setup_discussion_user(staff=is_staff) self.verify_teams_discussion_permissions(False) @ddt.data('Moderator', 'Community TA', 'Administrator') def test_discussion_privileged(self, role): + self._set_team_configuration_and_membership(create_membership=False) self.setup_discussion_user(role=role) self.verify_teams_discussion_permissions(True) + + def assert_team_details(self, num_members, is_member=True, max_size=10, invite_text=''): + """ + Verifies that user can see all the information, present on detail page according to their membership status. + + Arguments: + num_members (int): number of users in a team + is_member (bool) default True: True if request user is member else False + max_size (int): number of users a team can have + invite_text (str): help text for invite link. + """ + self.assertEqual( + self.team_page.team_capacity_text, + '{num_members} / {max_size} {members_text}'.format( + num_members=num_members, + max_size=max_size, + members_text='Member' if num_members == max_size else 'Members' + ) + ) + self.assertEqual(self.team_page.team_location, 'Afghanistan') + self.assertEqual(self.team_page.team_language, 'Afar') + self.assertEqual(self.team_page.team_members, num_members) + + if num_members > 0: + self.assertTrue(self.team_page.team_members_present) + else: + self.assertFalse(self.team_page.team_members_present) + + if is_member: + self.assertEqual(self.team_page.team_user_membership_text, 'You are a member of this team.') + self.assertTrue(self.team_page.team_leave_link_present) + self.assertTrue(self.team_page.team_invite_section_present) + self.assertEqual(self.team_page.team_invite_help_text, invite_text) + else: + self.assertEqual(self.team_page.team_user_membership_text, '') + self.assertFalse(self.team_page.team_leave_link_present) + self.assertFalse(self.team_page.team_invite_section_present) + + def test_team_member_can_see_full_team_details(self): + """ + Scenario: Team member can see full info for team. + Given I am enrolled in a course with a team configuration, a topic, + and a team belonging to that topic of which I am a member + When I visit the Team page for that team + Then I should see the full team detail + And I should see the team members + And I should see my team membership text + And I should see the language & country + And I should see the Leave Team and Invite Team + """ + self._set_team_configuration_and_membership() + self.team_page.visit() + + self.assert_team_details( + num_members=1, + invite_text='Send this link to friends so that they can join too.' + ) + + def test_other_users_can_see_limited_team_details(self): + """ + Scenario: Users who are not member of this team can only see limited info for this team. + Given I am enrolled in a course with a team configuration, a topic, + and a team belonging to that topic of which I am not a member + When I visit the Team page for that team + Then I should not see full team detail + And I should see the team members + And I should not see my team membership text + And I should not see the Leave Team and Invite Team links + """ + self._set_team_configuration_and_membership(create_membership=False) + self.team_page.visit() + + self.assert_team_details(is_member=False, num_members=0) + + def test_user_can_navigate_to_members_profile_page(self): + """ + Scenario: User can navigate to profile page via team member profile image. + Given I am enrolled in a course with a team configuration, a topic, + and a team belonging to that topic of which I am a member + When I visit the Team page for that team + Then I should see profile images for the team members + When I click on the first profile image + Then I should be taken to the user's profile page + And I should see the username on profile page + """ + self._set_team_configuration_and_membership() + self.team_page.visit() + + self.team_page.click_first_profile_image() + + learner_profile_page = LearnerProfilePage(self.browser, self.team_page.first_member_username) + learner_profile_page.wait_for_page() + learner_profile_page.wait_for_field('username') + self.assertTrue(learner_profile_page.field_is_visible('username')) + + def test_team_member_cannot_see_invite_link_if_team_full(self): + """ + Scenario: Team members should not see the invite link if the team is full. + Given I am enrolled in a course with a team configuration, a topic, + and a team belonging to that topic of which I am a member + When I visit the Team page for that team + Then I should see the "team is full" message + And I should not see the invite link + """ + self._set_team_configuration_and_membership(max_team_size=1) + self.team_page.visit() + + self.assert_team_details( + num_members=1, + max_size=1, + invite_text='No invitations are available. This team is full.' + ) + + def test_team_member_can_see_invite_link(self): + """ + Scenario: Team members should see the invite link if the team has capacity. + Given I am enrolled in a course with a team configuration, a topic, + and a team belonging to that topic of which I am a member + When I visit the Team page for that team + Then I should see the invite link help message + And I should see the invite link that can be selected + """ + self._set_team_configuration_and_membership() + self.team_page.visit() + + self.assert_team_details( + num_members=1, + invite_text='Send this link to friends so that they can join too.' + ) + self.assertEqual(self.team_page.team_invite_url, '{0}?invite=true'.format(self.team_page.url)) diff --git a/lms/djangoapps/teams/static/teams/js/collections/team.js b/lms/djangoapps/teams/static/teams/js/collections/team.js index 2033d7e4f7..c64a3c10ff 100644 --- a/lms/djangoapps/teams/static/teams/js/collections/team.js +++ b/lms/djangoapps/teams/static/teams/js/collections/team.js @@ -8,6 +8,7 @@ this.course_id = options.course_id; this.server_api['topic_id'] = this.topic_id = options.topic_id; + this.server_api['expand'] = 'user'; this.perPage = options.per_page; this.server_api['course_id'] = function () { return encodeURIComponent(this.course_id); }; this.server_api['order_by'] = function () { return 'name'; }; // TODO surface sort order in UI diff --git a/lms/djangoapps/teams/static/teams/js/spec/views/team_profile_spec.js b/lms/djangoapps/teams/static/teams/js/spec/views/team_profile_spec.js index fba51ed1a8..449e3e2dd8 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/views/team_profile_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/views/team_profile_spec.js @@ -5,26 +5,41 @@ define([ ], function (_, AjaxHelpers, TeamModel, TeamProfileView, TeamSpecHelpers, DiscussionSpecHelper) { 'use strict'; describe('TeamProfileView', function () { - var discussionView, createTeamProfileView; + var profileView, createTeamProfileView; beforeEach(function () { DiscussionSpecHelper.setUnderscoreFixtures(); }); - createTeamProfileView = function(requests) { + createTeamProfileView = function(requests, options) { var model = new TeamModel( { id: "test-team", name: "Test Team", - discussion_topic_id: TeamSpecHelpers.testTeamDiscussionID + discussion_topic_id: TeamSpecHelpers.testTeamDiscussionID, + country: options.country || '', + language: options.language || '', + membership: options.membership || [] }, { parse: true } ); - discussionView = new TeamProfileView({ + profileView = new TeamProfileView({ courseID: TeamSpecHelpers.testCourseID, - model: model + model: model, + maxTeamSize: options.maxTeamSize || 3, + requestUsername: 'bilbo', + countries : [ + ['', ''], + ['US', 'United States'], + ['CA', 'Canada'] + ], + languages : [ + ['', ''], + ['en', 'English'], + ['fr', 'French'] + ] }); - discussionView.render(); + profileView.render(); AjaxHelpers.expectRequest( requests, 'GET', @@ -38,13 +53,147 @@ define([ ) ); AjaxHelpers.respondWithJson(requests, TeamSpecHelpers.createMockDiscussionResponse()); - return discussionView; + return profileView; }; - it('can render itself', function () { - var requests = AjaxHelpers.requests(this), - view = createTeamProfileView(requests); - expect(view.$('.discussion-thread').length).toEqual(3); + describe('DiscussionsView', function() { + it('can render itself', function () { + var requests = AjaxHelpers.requests(this), + view = createTeamProfileView(requests, {}); + expect(view.$('.discussion-thread').length).toEqual(3); + }); + }); + + describe('TeamDetailsView', function() { + + var assertTeamDetails = function(view, members) { + expect(view.$('.team-detail-header').text()).toBe('Team Details'); + expect(view.$('.team-country').text()).toContain('United States'); + expect(view.$('.team-language').text()).toContain('English'); + expect(view.$('.team-capacity').text()).toContain(members + ' / 3 Members'); + expect(view.$('.team-member').length).toBe(members); + }; + + describe('Non-Member', function() { + + it('can render itself', function() { + var requests = AjaxHelpers.requests(this); + var view = createTeamProfileView(requests, { + country: 'US', + language: 'en' + }); + assertTeamDetails(view, 0); + expect(view.$('.team-user-membership-status').length).toBe(0); + + // Verify that invite and leave team sections are not present. + expect(view.$('.leave-team').length).toBe(0); + expect(view.$('.invite-team').length).toBe(0); + + }); + it('cannot see the country & language if empty', function() { + var requests = AjaxHelpers.requests(this); + var view = createTeamProfileView(requests, {}); + expect(view.$('.team-country').length).toBe(0); + expect(view.$('.team-language').length).toBe(0); + }); + }); + + describe('Member', function() { + + it('can render itself', function() { + var requests = AjaxHelpers.requests(this); + var view = createTeamProfileView(requests, { + country: 'US', + language: 'en', + membership: [{ + 'user': { + 'username': 'bilbo', + 'profile_image': { + 'has_image': true, + 'image_url_medium': '/image-url' + } + } + }] + }); + assertTeamDetails(view, 1); + expect(view.$('.team-user-membership-status').text().trim()).toBe('You are a member of this team.'); + + // assert tooltip text. + expect(view.$('.member-profile p').text()).toBe('bilbo'); + // assert user profile page url. + expect(view.$('.member-profile').attr('href')).toBe('/u/bilbo'); + + //Verify that invite and leave team sections are present + expect(view.$('.leave-team-link').text()).toContain('Leave Team'); + expect(view.$('.invite-header').text()).toContain('Invite Others'); + expect(view.$('.invite-text').text()).toContain('Send this link to friends so that they can join too.'); + expect(view.$('.invite-link-input').length).toBe(1); + + }); + it('cannot see invite url box if team is full', function() { + var requests = AjaxHelpers.requests(this); + var view = createTeamProfileView(requests , { + country: 'US', + language: 'en', + membership: [{ + 'user': { + 'username': 'bilbo', + 'profile_image': { + 'has_image': true, + 'image_url_medium': '/image-url' + } + } + }, + { + 'user': { + 'username': 'bilbo1', + 'profile_image': { + 'has_image': true, + 'image_url_medium': '/image-url' + } + } + }, + { + 'user': { + 'username': 'bilbo2', + 'profile_image': { + 'has_image': true, + 'image_url_medium': '/image-url' + } + } + }] + }); + + assertTeamDetails(view, 3); + expect(view.$('.invite-header').text()).toContain('Invite Others'); + expect(view.$('.invite-text').text()).toContain('No invitations are available. This team is full.'); + expect(view.$('.invite-link-input').length).toBe(0); + }); + it('can see & select invite url if team has capacity', function() { + var requests = AjaxHelpers.requests(this); + spyOn(TeamProfileView.prototype, 'selectText'); + + var view = createTeamProfileView(requests, { + country: 'US', + language: 'en', + membership: [{ + 'user': { + 'username': 'bilbo', + 'profile_image': { + 'has_image': true, + 'image_url_medium': '/image-url' + } + } + }] + }); + assertTeamDetails(view, 1); + + expect(view.$('.invite-link-input').length).toBe(1); + + view.$('.invite-link-input').click(); + expect(view.selectText).toHaveBeenCalled(); + }); + }); }); }); }); diff --git a/lms/djangoapps/teams/static/teams/js/spec/views/teams_tab_spec.js b/lms/djangoapps/teams/static/teams/js/spec/views/teams_tab_spec.js index 10b4530dbc..2daf6fd0b8 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/views/teams_tab_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/views/teams_tab_spec.js @@ -92,7 +92,7 @@ define([ var requests = AjaxHelpers.requests(this), teamsTabView = createTeamsTabView(); teamsTabView.router.navigate('teams/test_topic/no_such_team', {trigger: true}); - AjaxHelpers.expectRequest(requests, 'GET', 'api/teams/no_such_team', null); + AjaxHelpers.expectRequest(requests, 'GET', 'api/teams/no_such_team?expand=user', null); AjaxHelpers.respondWithError(requests, 404); expectError(teamsTabView, 'The team "no_such_team" could not be found.'); expectFocus(teamsTabView.$('.warning')); 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 3578523572..b0b64fd1f9 100644 --- a/lms/djangoapps/teams/static/teams/js/views/team_card.js +++ b/lms/djangoapps/teams/static/teams/js/views/team_card.js @@ -5,8 +5,9 @@ 'underscore', 'gettext', 'js/components/card/views/card', + 'teams/js/views/team_utils', 'text!teams/templates/team-country-language.underscore' - ], function (Backbone, _, gettext, CardView, teamCountryLanguageTemplate) { + ], function (Backbone, _, gettext, CardView, TeamUtils, teamCountryLanguageTemplate) { var TeamMembershipView, TeamCountryLanguageView, TeamCardView; TeamMembershipView = Backbone.View.extend({ @@ -25,15 +26,7 @@ var memberships = this.model.get('membership'), maxMemberCount = this.maxTeamSize; this.$el.html(this.template({ - membership_message: interpolate( - // Translators: The following message displays the number of members on a team. - ngettext( - '%(member_count)s / %(max_member_count)s Member', - '%(member_count)s / %(max_member_count)s Members', - maxMemberCount - ), - {member_count: memberships.length, max_member_count: maxMemberCount}, true - ) + membership_message: TeamUtils.teamCapacityText(memberships.length, maxMemberCount) })); _.each(memberships, function (membership) { this.$('list-member-thumbs').append( diff --git a/lms/djangoapps/teams/static/teams/js/views/team_profile.js b/lms/djangoapps/teams/static/teams/js/views/team_profile.js index dddcafc916..90ca3fbfd2 100644 --- a/lms/djangoapps/teams/static/teams/js/views/team_profile.js +++ b/lms/djangoapps/teams/static/teams/js/views/team_profile.js @@ -4,26 +4,66 @@ ;(function (define) { 'use strict'; define(['backbone', 'underscore', 'gettext', 'teams/js/views/team_discussion', - 'text!teams/templates/team-profile.underscore'], - function (Backbone, _, gettext, TeamDiscussionView, teamTemplate) { + 'teams/js/views/team_utils', + 'text!teams/templates/team-profile.underscore', + 'text!teams/templates/team-member.underscore' + ], + function (Backbone, _, gettext, TeamDiscussionView, TeamUtils, teamTemplate, teamMemberTemplate) { var TeamProfileView = Backbone.View.extend({ + + events: { + 'click .invite-link-input': 'selectText' + }, initialize: function (options) { this.courseID = options.courseID; this.discussionTopicID = this.model.get('discussion_topic_id'); + this.maxTeamSize = options.maxTeamSize; + this.memberships = this.model.get('membership'); this.readOnly = options.readOnly; + this.requestUsername = options.requestUsername; + this.teamInviteUrl = options.teamInviteUrl; + + this.countries = TeamUtils.selectorOptionsArrayToHashWithBlank(options.countries); + this.languages = TeamUtils.selectorOptionsArrayToHashWithBlank(options.languages); + }, render: function () { this.$el.html(_.template(teamTemplate, { courseID: this.courseID, discussionTopicID: this.discussionTopicID, - readOnly: this.readOnly + readOnly: this.readOnly, + country: this.countries[this.model.get('country')], + language: this.languages[this.model.get('language')], + membershipText: TeamUtils.teamCapacityText(this.memberships.length, this.maxTeamSize), + isMember: TeamUtils.isUserMemberOfTeam(this.memberships, this.requestUsername), + hasCapacity: this.memberships.length < this.maxTeamSize, + inviteLink: this.teamInviteUrl + })); this.discussionView = new TeamDiscussionView({ el: this.$('.discussion-module') }); this.discussionView.render(); + + this.renderTeamMembers(); return this; + }, + + renderTeamMembers: function() { + var view = this; + _.each(this.memberships, function(membership) { + view.$('.members-info').append(_.template(teamMemberTemplate, { + imageUrl: membership.user.profile_image.image_url_medium, + username: membership.user.username, + memberProfileUrl: '/u/' + membership.user.username + })); + }); + }, + + selectText: function(event) { + event.preventDefault(); + $(event.currentTarget).select(); } }); diff --git a/lms/djangoapps/teams/static/teams/js/views/team_utils.js b/lms/djangoapps/teams/static/teams/js/views/team_utils.js new file mode 100644 index 0000000000..5623e73d8d --- /dev/null +++ b/lms/djangoapps/teams/static/teams/js/views/team_utils.js @@ -0,0 +1,44 @@ +/* Team utility methods*/ +;(function (define) { + 'use strict'; + define([ + ], function () { + return { + + /** + * Convert a 2d array to an object equivalent with an additional blank element + * + * @param options {Array.>} Two dimensional options array + * @returns {Object} Hash version of the input array + * @example selectorOptionsArrayToHashWithBlank([["a", "alpha"],["b","beta"]]) + * // returns {"a":"alpha", "b":"beta", "":""} + */ + selectorOptionsArrayToHashWithBlank: function (options) { + var map = _.object(options); + map[""] = ""; + return map; + }, + + teamCapacityText: function (memberCount, maxMemberCount) { + return interpolate( + // Translators: The following message displays the number of members on a team. + ngettext( + '%(memberCount)s / %(maxMemberCount)s Member', + '%(memberCount)s / %(maxMemberCount)s Members', + maxMemberCount + ), + {memberCount: memberCount, maxMemberCount: maxMemberCount}, true + ) + }, + isUserMemberOfTeam: function(memberships, requestUsername) { + return _.isObject( + _.find(memberships, function(membership) + { + return membership.user.username === requestUsername; + }) + ); + } + } + }); + +}).call(this, define || RequireJS.define); diff --git a/lms/djangoapps/teams/static/teams/js/views/teams.js b/lms/djangoapps/teams/static/teams/js/views/teams.js index 680294957d..f25adaa2df 100644 --- a/lms/djangoapps/teams/static/teams/js/views/teams.js +++ b/lms/djangoapps/teams/static/teams/js/views/teams.js @@ -4,8 +4,9 @@ 'backbone', 'gettext', 'teams/js/views/team_card', - 'common/js/components/views/paginated_view' - ], function (Backbone, gettext, TeamCardView, PaginatedView) { + 'common/js/components/views/paginated_view', + 'teams/js/views/team_utils' + ], function (Backbone, gettext, TeamCardView, PaginatedView, TeamUtils) { var TeamsView = PaginatedView.extend({ type: 'teams', @@ -26,25 +27,11 @@ router: options.router, topic: options.topic, maxTeamSize: options.maxTeamSize, - countries: this.selectorOptionsArrayToHashWithBlank(options.teamParams.countries), - languages: this.selectorOptionsArrayToHashWithBlank(options.teamParams.languages), - srInfo: this.srInfo + srInfo: this.srInfo, + countries: TeamUtils.selectorOptionsArrayToHashWithBlank(options.teamParams.countries), + languages: TeamUtils.selectorOptionsArrayToHashWithBlank(options.teamParams.languages) }); PaginatedView.prototype.initialize.call(this); - }, - - /** - * Convert a 2d array to an object equivalent with an additional blank element - * - * @param {Array.>} Two dimensional options array - * @returns {Object} Hash version of the input array - * @example selectorOptionsArrayToHashWithBlank([["a", "alpha"],["b","beta"]]) - * // returns {"a":"alpha", "b":"beta", "":""} - */ - selectorOptionsArrayToHashWithBlank: function (options) { - var map = _.object(options); - map[""] = ""; - return map; } }); return TeamsView; diff --git a/lms/djangoapps/teams/static/teams/js/views/teams_tab.js b/lms/djangoapps/teams/static/teams/js/views/teams_tab.js index 60494ce9c5..9e0beec554 100644 --- a/lms/djangoapps/teams/static/teams/js/views/teams_tab.js +++ b/lms/djangoapps/teams/static/teams/js/views/teams_tab.js @@ -56,7 +56,7 @@ this.languages = options.languages; this.countries = options.countries; this.userInfo = options.userInfo; - + this.teamsBaseUrl = options.teamsBaseUrl; // This slightly tedious approach is necessary // to use regular expressions within Backbone // routes, allowing us to capture which tab @@ -267,12 +267,17 @@ deferred = $.Deferred(), courseID = this.courseID; self.getTopic(topicID).done(function(topic) { - self.getTeam(teamID).done(function(team) { + self.getTeam(teamID, true).done(function(team) { var readOnly = self.readOnlyDiscussion(team), view = new TeamProfileView({ courseID: courseID, model: team, - readOnly: readOnly + readOnly: readOnly, + maxTeamSize: self.maxTeamSize, + requestUsername: self.userInfo.username, + countries: self.countries, + languages: self.languages, + teamInviteUrl: self.teamsBaseUrl + '#teams/' + topicID + '/' + teamID + '?invite=true' }); deferred.resolve(self.createViewWithHeader(view, team, topic)); }); @@ -350,18 +355,21 @@ * promise, since the team may need to be fetched from the * server. * @param teamID the string identifier for the requested team + * @param expandUser bool to add the users info. * @returns {promise} a jQuery deferred promise for the team. */ - getTeam: function (teamID) { + getTeam: function (teamID, expandUser) { var team = this.teamsCollection ? this.teamsCollection.get(teamID) : null, self = this, - deferred = $.Deferred(); + deferred = $.Deferred(), + teamUrl; if (team) { deferred.resolve(team); } else { + teamUrl = this.teamsUrl + teamID + (expandUser ? '?expand=user': ''); team = new TeamModel({ id: teamID, - url: this.teamsUrl + teamID + url: teamUrl }); team.fetch() .done(function() { diff --git a/lms/djangoapps/teams/static/teams/templates/team-member.underscore b/lms/djangoapps/teams/static/teams/templates/team-member.underscore new file mode 100644 index 0000000000..a400554328 --- /dev/null +++ b/lms/djangoapps/teams/static/teams/templates/team-member.underscore @@ -0,0 +1,6 @@ + + +

<%= username %>

+ profile page +
+
diff --git a/lms/djangoapps/teams/static/teams/templates/team-profile.underscore b/lms/djangoapps/teams/static/teams/templates/team-profile.underscore index f524b7939d..ebff8adff6 100644 --- a/lms/djangoapps/teams/static/teams/templates/team-profile.underscore +++ b/lms/djangoapps/teams/static/teams/templates/team-profile.underscore @@ -1,10 +1,71 @@
-
- <% if ( !readOnly) { %> - <%= gettext("New Post") %> +
+
+ <% if ( !readOnly) { %> + <%= gettext("New Post") %> + <% } %> +
+
+ +
+

<%- gettext("Team Details") %>

+ <% if (isMember) { %> +
+

<%- gettext("You are a member of this team.") %>

+
+ <% } %> +
+ <%- gettext("Team member profiles") %> +
+
+ +
+ <%- gettext("Team capacity") %> + <%- membershipText %> +
+ + <% if (country) { %> +
+ <%- gettext("The country that team members primarily identify with.") %> + + + <%- gettext(country) %> + +
+ <% } %> + <% if (language) { %> +
+ <%- gettext("The language that team members primarily use to communicate with each other.") %> + + + <%- gettext(language) %> + +
+ <% } %> + + <% if (isMember) { %> +
+ +
+ +
+ +
+

<%- gettext("Invite Others") %>

+ <% if (hasCapacity) { %> + + + <%- gettext("Send this link to friends so that they can join too.") %> + + <% } else { %> + + <%- gettext("No invitations are available. This team is full.") %> + + <% } %> +
<% } %>
diff --git a/lms/djangoapps/teams/templates/teams/teams.html b/lms/djangoapps/teams/templates/teams/teams.html index e524e5703a..d3e57db3af 100644 --- a/lms/djangoapps/teams/templates/teams/teams.html +++ b/lms/djangoapps/teams/templates/teams/teams.html @@ -42,7 +42,8 @@ teamMembershipsUrl: '${ team_memberships_url }', maxTeamSize: ${ course.teams_max_size }, languages: ${ json.dumps(languages, cls=EscapedEdxJSONEncoder) }, - countries: ${ json.dumps(countries, cls=EscapedEdxJSONEncoder) } + countries: ${ json.dumps(countries, cls=EscapedEdxJSONEncoder) }, + teamsBaseUrl: '${ teams_base_url }' }); diff --git a/lms/djangoapps/teams/views.py b/lms/djangoapps/teams/views.py index ac7cabe4b9..c742db51c2 100644 --- a/lms/djangoapps/teams/views.py +++ b/lms/djangoapps/teams/views.py @@ -111,6 +111,7 @@ class TeamsDashboardView(View): "languages": settings.ALL_LANGUAGES, "countries": list(countries), "disable_courseware_js": True, + "teams_base_url": reverse('teams_dashboard', request=request, kwargs={'course_id': course_id}), } return render_to_response("teams/teams.html", context) diff --git a/lms/static/sass/elements/_controls.scss b/lms/static/sass/elements/_controls.scss index a38dc4a739..9c47ae3c4a 100644 --- a/lms/static/sass/elements/_controls.scss +++ b/lms/static/sass/elements/_controls.scss @@ -403,3 +403,16 @@ margin-bottom: none; } } + + .btn-link { + @extend %btn-pl-secondary-base; + + background-image: none; + + &:focus, + &:hover { + background-image: none !important; + background-color: transparent !important; + color: $link-color; + } + } diff --git a/lms/static/sass/shared/_tooltips.scss b/lms/static/sass/shared/_tooltips.scss index c94c8f137a..bce8b23e86 100644 --- a/lms/static/sass/shared/_tooltips.scss +++ b/lms/static/sass/shared/_tooltips.scss @@ -10,3 +10,54 @@ text-align: center; -webkit-font-smoothing: antialiased; } + + +// custom tool tip style. +@mixin tooltip-hover-style ($margin-top) { + p { + @extend %ui-depth2; + background: $dark-gray; + border-radius: ($baseline/5); + color: $white; + font-family: $sans-serif; + line-height: lh(); + opacity: 0.0; + padding: 6px; + position: absolute; + text-shadow: 0 -1px 0 $black; + @include transition(all .1s $ease-in-out-quart 0s); + white-space: pre; + visibility: hidden; + pointer-events: none; + right: 0; + + &:empty { + background: none; + + &::after { + display: none; + } + } + + &::after { + background: $dark-gray; + content: " "; + display: block; + height: ($baseline/2); + right: 18px; + position: absolute; + top: ($baseline + ($baseline/4)); + @include transform(rotate(45deg)); + width: ($baseline/2); + } + } + + &:hover, &:focus { + p { + display: block; + margin-top: $margin-top; + opacity: 1.0; + visibility: visible; + } + } +} diff --git a/lms/static/sass/views/_teams.scss b/lms/static/sass/views/_teams.scss index 1091dabfdb..74e5a1824e 100644 --- a/lms/static/sass/views/_teams.scss +++ b/lms/static/sass/views/_teams.scss @@ -346,6 +346,64 @@ } } + .team-profile { + + .page-content-main { + display: inline-block; + width: flex-grid(8, 12); + vertical-align: top; + + .forum-new-post-form, + .edit-post-form { + min-width: 700px; + } + } + + .page-content-secondary { + display: inline-block; + width: flex-grid(4,12); + @include margin-left($baseline * 0.75); + margin-bottom: $baseline; + + @extend %t-copy-sub1; + color: $gray-l1; + + div, .team-detail-header { + margin-bottom: ($baseline/4); + } + + .image-url { + border: 2px solid $outer-border-color; + border-radius: ($baseline/4); + margin-bottom: ($baseline/4); + } + + .leave-team { + margin-bottom: $baseline; + margin-top: ($baseline/2); + } + + .invite-header, .invite-text { + margin-bottom: ($baseline/2); + } + + .invite-team { + margin-top: ($baseline); + margin-bottom: ($baseline/2); + } + .invite-link-input { + width: 100%; + } + + .member-profile { + position: relative; + display: inline-block; + + @include tooltip-hover-style(-($baseline*2)); + } + } + } + .create-team { legend {