Team details page.
TNL-1906
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
44
lms/djangoapps/teams/static/teams/js/views/team_utils.js
Normal file
44
lms/djangoapps/teams/static/teams/js/views/team_utils.js
Normal file
@@ -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.<Array.<string>>} 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);
|
||||
@@ -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.<Array.<string>>} 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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<span class="team-member">
|
||||
<a class="member-profile" href="<%= memberProfileUrl %>">
|
||||
<p class="tooltip-custom"><%= username %></p>
|
||||
<img class="image-url" src="<%= imageUrl %>" alt="profile page" />
|
||||
</a>
|
||||
</span>
|
||||
@@ -1,10 +1,71 @@
|
||||
<div class="team-profile">
|
||||
<div class="discussion-module" data-course-id="<%= courseID %>" data-discussion-id="<%= discussionTopicID %>"
|
||||
data-read-only="<%= readOnly %>"
|
||||
data-user-create-comment="<%= !readOnly %>"
|
||||
data-user-create-subcomment="<%= !readOnly %>">
|
||||
<% if ( !readOnly) { %>
|
||||
<a href="#" class="new-post-btn" role="button"><span class="icon fa fa-edit new-post-icon"></span><%= gettext("New Post") %></a>
|
||||
<div class="page-content-main">
|
||||
<div class="discussion-module" data-course-id="<%= courseID %>" data-discussion-id="<%= discussionTopicID %>"
|
||||
data-read-only="<%= readOnly %>"
|
||||
data-user-create-comment="<%= !readOnly %>"
|
||||
data-user-create-subcomment="<%= !readOnly %>">
|
||||
<% if ( !readOnly) { %>
|
||||
<a href="#" class="new-post-btn" role="button"><span class="icon fa fa-edit new-post-icon"></span><%= gettext("New Post") %></a>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-content-secondary">
|
||||
<h4 class="team-detail-header"><%- gettext("Team Details") %></h4>
|
||||
<% if (isMember) { %>
|
||||
<div class="team-user-membership-status">
|
||||
<p><%- gettext("You are a member of this team.") %></p>
|
||||
</div>
|
||||
<% } %>
|
||||
<div class="team-members">
|
||||
<span class="sr"><%- gettext("Team member profiles") %></span>
|
||||
<div class="members-info"></div>
|
||||
</div>
|
||||
|
||||
<div class="team-capacity">
|
||||
<span class="sr"><%- gettext("Team capacity") %></span>
|
||||
<span><%- membershipText %></span>
|
||||
</div>
|
||||
|
||||
<% if (country) { %>
|
||||
<div class="team-country">
|
||||
<span class="sr"><%- gettext("The country that team members primarily identify with.") %></span>
|
||||
<i class="icon fa fa-globe fa-fw" aria-hidden="true"></i>
|
||||
<span>
|
||||
<%- gettext(country) %>
|
||||
</span>
|
||||
</div>
|
||||
<% } %>
|
||||
<% if (language) { %>
|
||||
<div class="team-language">
|
||||
<span class="sr"><%- gettext("The language that team members primarily use to communicate with each other.") %></span>
|
||||
<i class="icon fa fa-comment-o fa-fw" aria-hidden="true"></i>
|
||||
<span>
|
||||
<%- gettext(language) %>
|
||||
</span>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% if (isMember) { %>
|
||||
<div class="leave-team">
|
||||
<button class="btn btn-link btn-base btn-secondary leave-team-link"><%- gettext("Leave Team") %></button>
|
||||
</div>
|
||||
|
||||
<div class="divider-lv1"></div>
|
||||
|
||||
<div class="invite-team">
|
||||
<h4 class="invite-header"><%- gettext("Invite Others") %></h4>
|
||||
<% if (hasCapacity) { %>
|
||||
<input type="text" class="invite-link-input" value="<%= inviteLink %>" aria-describedby="invite-text" readonly >
|
||||
<span class="invite-text" id="invite-text">
|
||||
<%- gettext("Send this link to friends so that they can join too.") %>
|
||||
</span>
|
||||
<% } else { %>
|
||||
<span class="invite-text">
|
||||
<%- gettext("No invitations are available. This team is full.") %>
|
||||
</span>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 }'
|
||||
});
|
||||
</%static:require_module>
|
||||
</%block>
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user