Commit f785ae46 by Usman Khalid

Merge pull request #9228 from edx/muzaffar/tnl1906-team-profile

Team profile page sidebar.
parents d96e1eb2 b9cf31f4
......@@ -252,3 +252,95 @@ 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
def click_leave_team_link(self):
""" Click on Leave Team link"""
self.q(css='.leave-team-link').first.click()
self.wait_for_ajax()
@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]
def click_join_team_button(self):
""" Click on Join Team button"""
self.q(css='.join-team .action-primary').first.click()
self.wait_for_ajax()
@property
def join_team_message(self):
""" Returns join team message """
self.wait_for_ajax()
return self.q(css='.join-team .join-team-message').text[0]
@property
def join_team_button_present(self):
""" Returns True if Join Team button is present else False """
self.wait_for_ajax()
return self.q(css='.join-team .action-primary').present
@property
def join_team_message_present(self):
""" Returns True if Join Team message is present else False """
return self.q(css='.join-team .join-team-message').present
@property
def new_post_button_present(self):
""" Returns True if New Post button is present else False """
return self.q(css='.discussion-module .new-post-btn').present
......@@ -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):
"""
......@@ -745,12 +750,43 @@ class CreateTeamTest(TeamsTabBase):
@ddt.ddt
class TeamPageTest(TeamsTabBase):
"""Tests for viewing a specific team"""
SEND_INVITE_TEXT = 'Send this link to friends so that they can join too.'
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 +794,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 +823,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 +845,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 +861,228 @@ 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)
self.assertTrue(self.team_page.new_post_button_present)
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)
self.assertFalse(self.team_page.new_post_button_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=self.SEND_INVITE_TEXT
)
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=self.SEND_INVITE_TEXT
)
self.assertEqual(self.team_page.team_invite_url, '{0}?invite=true'.format(self.team_page.url))
def test_join_team(self):
"""
Scenario: User can join a Team if not a member already..
Given I am enrolled in a course with a team configuration, a topic,
and a team belonging to that topic
And I visit the Team page for that team
Then I should see Join Team button
And I should not see New Post button
When I click on Join Team button
Then there should be no Join Team button and no message
And I should see the updated information under Team Details
And I should see New Post button
"""
self._set_team_configuration_and_membership(create_membership=False)
self.team_page.visit()
self.assertTrue(self.team_page.join_team_button_present)
self.team_page.click_join_team_button()
self.assertFalse(self.team_page.join_team_button_present)
self.assertFalse(self.team_page.join_team_message_present)
self.assert_team_details(num_members=1, is_member=True, invite_text=self.SEND_INVITE_TEXT)
def test_already_member_message(self):
"""
Scenario: User should see `You are already in a team` if user is a
member of other team.
Given I am enrolled in a course with a team configuration, a topic,
and a team belonging to that topic
And I am already a member of a team
And I visit a team other than mine
Then I should see `You are already in a team` message
"""
self._set_team_configuration_and_membership(membership_team_index=0, visit_team_index=1)
self.team_page.visit()
self.assertEqual(self.team_page.join_team_message, 'You already belong to another team.')
self.assert_team_details(num_members=0, is_member=False)
def test_team_full_message(self):
"""
Scenario: User should see `Team is full` message when team is full.
Given I am enrolled in a course with a team configuration, a topic,
and a team belonging to that topic
And team has no space left
And I am not a member of any team
And I visit the team
Then I should see `Team is full` message
"""
self._set_team_configuration_and_membership(
create_membership=True,
max_team_size=1,
membership_team_index=0,
visit_team_index=0,
another_user=True
)
self.team_page.visit()
self.assertEqual(self.team_page.join_team_message, 'This team is full.')
self.assert_team_details(num_members=1, is_member=False, max_size=1)
def test_leave_team(self):
"""
Scenario: User can leave a team.
Given I am enrolled in a course with a team configuration, a topic,
and a team belonging to that topic
And I am a member of team
And I visit the team
And I should not see Join Team button
And I should see New Post button
Then I should see Leave Team link
When I click on Leave Team link
Then user should be removed from team
And I should see Join Team button
And I should not see New Post button
"""
self._set_team_configuration_and_membership()
self.team_page.visit()
self.assertFalse(self.team_page.join_team_button_present)
self.assert_team_details(num_members=1, invite_text=self.SEND_INVITE_TEXT)
self.team_page.click_leave_team_link()
self.assert_team_details(num_members=0, is_member=False)
self.assertTrue(self.team_page.join_team_button_present)
......@@ -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
......
define([
'underscore', 'common/js/spec_helpers/ajax_helpers', 'teams/js/models/team',
'teams/js/views/team_join'
], function (_, AjaxHelpers, TeamModel, TeamJoinView) {
'use strict';
describe('TeamJoinView', function () {
var createTeamsUrl,
createTeamModelData,
createMembershipData,
createJoinView,
verifyErrorMessage,
ACCOUNTS_API_URL = '/api/user/v1/accounts/',
TEAMS_URL = '/api/team/v0/teams/',
TEAMS_MEMBERSHIP_URL = '/api/team/v0/team_membership/';
beforeEach(function () {
setFixtures(
'<div class="teams-content"><div class="msg-content"><div class="copy"></div></div><div class="header-action-view"></div></div>'
);
});
verifyErrorMessage = function (requests, errorMessage, expectedMessage, joinTeam) {
var view = createJoinView(1, 'ma', createTeamModelData('teamA', 'teamAlpha', []));
if (joinTeam) {
// if we want the error to return when user try to join team, respond with no membership
AjaxHelpers.respondWithJson(requests, {"count": 0});
view.$('.action.action-primary').click();
}
AjaxHelpers.respondWithTextError(requests, 400, errorMessage);
expect($('.msg-content .copy').text().trim()).toBe(expectedMessage);
};
createTeamsUrl = function (teamId) {
return TEAMS_URL + teamId + '?expand=user';
};
createTeamModelData = function (teamId, teamName, membership) {
return {
id: teamId,
name: teamName,
membership: membership
};
};
createMembershipData = function (username) {
return [
{
"user": {
"username": username,
"url": ACCOUNTS_API_URL + username
}
}
];
};
createJoinView = function(maxTeamSize, currentUsername, teamModelData, teamId) {
teamId = teamId || 'teamA';
var model = new TeamModel(teamModelData, { parse: true });
model.url = createTeamsUrl(teamId);
var teamJoinView = new TeamJoinView(
{
model: model,
teamsUrl: createTeamsUrl(teamId),
maxTeamSize: maxTeamSize,
currentUsername: currentUsername,
teamMembershipsUrl: TEAMS_MEMBERSHIP_URL
}
);
return teamJoinView.render();
};
it('can render itself', function () {
var teamModelData = createTeamModelData('teamA', 'teamAlpha', createMembershipData('ma'));
var view = createJoinView(1, 'ma', teamModelData);
expect(view.$('.join-team').length).toEqual(1);
});
it('can join team successfully', function () {
var requests = AjaxHelpers.requests(this);
var currentUsername = 'ma1';
var teamId = 'teamA';
var teamName = 'teamAlpha';
var teamModelData = createTeamModelData(teamId, teamName, []);
var view = createJoinView(1, currentUsername, teamModelData);
// a get request will be sent to get user membership info
// because current user is not member of current team
AjaxHelpers.expectRequest(
requests,
'GET',
TEAMS_MEMBERSHIP_URL + '?' + $.param({"username": currentUsername})
);
// current user is not a member of any team so we should see the Join Team button
AjaxHelpers.respondWithJson(requests, {"count": 0});
expect(view.$('.action.action-primary').length).toEqual(1);
// a post request will be sent to add current user to current team
view.$('.action.action-primary').click();
AjaxHelpers.expectRequest(
requests,
'POST',
TEAMS_MEMBERSHIP_URL,
$.param({'username': currentUsername, 'team_id': teamId})
);
AjaxHelpers.respondWithJson(requests, {});
// on success, team model will be fetched and
// join team view and team profile will be re-rendered
AjaxHelpers.expectRequest(
requests,
'GET',
createTeamsUrl(teamId)
);
AjaxHelpers.respondWithJson(
requests, createTeamModelData(teamId, teamName, createMembershipData(currentUsername))
);
// current user is now member of the current team then there should be no button and no message
expect(view.$('.action.action-primary').length).toEqual(0);
expect(view.$('.join-team-message').length).toEqual(0);
});
it('shows already member message', function () {
var requests = AjaxHelpers.requests(this);
var currentUsername = 'ma1';
var view = createJoinView(1, currentUsername, createTeamModelData('teamA', 'teamAlpha', []));
// a get request will be sent to get user membership info
// because current user is not member of current team
AjaxHelpers.expectRequest(
requests,
'GET',
TEAMS_MEMBERSHIP_URL + '?' + $.param({"username": currentUsername})
);
// current user is a member of another team so we should see the correct message
AjaxHelpers.respondWithJson(requests, {"count": 1});
expect(view.$('.action.action-primary').length).toEqual(0);
expect(view.$('.join-team-message').text().trim()).toBe(view.alreadyMemberMessage);
});
it('shows team full message', function () {
var requests = AjaxHelpers.requests(this);
var view = createJoinView(
1,
'ma1',
createTeamModelData('teamA', 'teamAlpha', createMembershipData('ma'))
);
// team has no space and current user is a not member of
// current team so we should see the correct message
expect(view.$('.action.action-primary').length).toEqual(0);
expect(view.$('.join-team-message').text().trim()).toBe(view.teamFullMessage);
// there should be no request made
expect(requests.length).toBe(0);
});
it('shows correct error message if user fails to join team', function () {
var requests = AjaxHelpers.requests(this);
// verify user_message
verifyErrorMessage(
requests,
JSON.stringify({'user_message': "Can't be made member"}),
"Can't be made member",
true
);
// verify generic error message
verifyErrorMessage(
requests,
'',
'An error occurred. Try again.',
true
);
// verify error message when json parsing succeeded but error message format is incorrect
verifyErrorMessage(
requests,
JSON.stringify({'blah': "Can't be made member"}),
'An error occurred. Try again.',
true
);
});
it('shows correct error message if initializing the view fails', function () {
// Rendering the view sometimes require fetching user's memberships. This may fail.
var requests = AjaxHelpers.requests(this);
// verify user_message
verifyErrorMessage(
requests,
JSON.stringify({'user_message': "Can't return user memberships"}),
"Can't return user memberships",
false
);
// verify generic error message
verifyErrorMessage(
requests,
'',
'An error occurred. Try again.',
false
);
});
});
});
......@@ -5,26 +5,56 @@ define([
], function (_, AjaxHelpers, TeamModel, TeamProfileView, TeamSpecHelpers, DiscussionSpecHelper) {
'use strict';
describe('TeamProfileView', function () {
var discussionView, createTeamProfileView;
var profileView, createTeamProfileView, createTeamModelData, teamModel,
DEFAULT_MEMBERSHIP = [
{
'user': {
'username': 'bilbo',
'profile_image': {
'has_image': true,
'image_url_medium': '/image-url'
}
}
}
];
beforeEach(function () {
setFixtures('<div class="teams-content"><div class="msg-content"><div class="copy"></div></div></div>');
DiscussionSpecHelper.setUnderscoreFixtures();
});
createTeamProfileView = function(requests) {
var model = new TeamModel(
{
id: "test-team",
name: "Test Team",
discussion_topic_id: TeamSpecHelpers.testTeamDiscussionID
},
{ parse: true }
);
discussionView = new TeamProfileView({
createTeamModelData = function (options) {
return {
id: "test-team",
name: "Test Team",
discussion_topic_id: TeamSpecHelpers.testTeamDiscussionID,
country: options.country || '',
language: options.language || '',
membership: options.membership || [],
url: '/api/team/v0/teams/test-team'
};
};
createTeamProfileView = function(requests, options) {
teamModel = new TeamModel(createTeamModelData(options), { parse: true });
profileView = new TeamProfileView({
courseID: TeamSpecHelpers.testCourseID,
model: model
model: teamModel,
maxTeamSize: options.maxTeamSize || 3,
requestUsername: 'bilbo',
countries : [
['', ''],
['US', 'United States'],
['CA', 'Canada']
],
languages : [
['', ''],
['en', 'English'],
['fr', 'French']
],
teamMembershipDetailUrl: 'api/team/v0/team_membership/team_id,bilbo'
});
discussionView.render();
profileView.render();
AjaxHelpers.expectRequest(
requests,
'GET',
......@@ -38,13 +68,203 @@ 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);
});
it('shows New Post button when user joins a team', function () {
var requests = AjaxHelpers.requests(this),
view = createTeamProfileView(requests, {});
expect(view.$('.new-post-btn').length).toEqual(0);
teamModel.set('membership', DEFAULT_MEMBERSHIP); // This should re-render the view.
expect(view.$('.new-post-btn').length).toEqual(1);
});
it('hides New Post button when user left a team', function () {
var requests = AjaxHelpers.requests(this),
view = createTeamProfileView(requests, {membership: DEFAULT_MEMBERSHIP});
expect(view.$('.new-post-btn').length).toEqual(1);
teamModel.set('membership', []);
expect(view.$('.new-post-btn').length).toEqual(0);
});
});
describe('TeamDetailsView', function() {
var assertTeamDetails = function(view, members, memberOfTeam) {
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);
expect(Boolean(view.$('.leave-team-link').length)).toBe(memberOfTeam);
};
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, false);
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: DEFAULT_MEMBERSHIP
});
assertTeamDetails(view, 1, true);
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, true);
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: DEFAULT_MEMBERSHIP}
);
assertTeamDetails(view, 1, true);
expect(view.$('.invite-link-input').length).toBe(1);
view.$('.invite-link-input').click();
expect(view.selectText).toHaveBeenCalled();
});
it('can leave team successfully', function() {
var requests = AjaxHelpers.requests(this);
var leaveTeamLinkSelector = '.leave-team-link';
var view = createTeamProfileView(
requests, { country: 'US', language: 'en', membership: DEFAULT_MEMBERSHIP}
);
assertTeamDetails(view, 1, true);
expect(view.$(leaveTeamLinkSelector).length).toBe(1);
// click on Leave Team link under Team Details
view.$(leaveTeamLinkSelector).click();
// response to DELETE
AjaxHelpers.respondWithNoContent(requests);
// response to model fetch request
AjaxHelpers.respondWithJson(requests, createTeamModelData({country: 'US', language: 'en'}));
assertTeamDetails(view, 0, false);
});
it('shows correct error messages', function () {
var requests = AjaxHelpers.requests(this);
var verifyErrorMessage = function (requests, errorMessage, expectedMessage) {
var view = createTeamProfileView(
requests, {country: 'US', language: 'en', membership: DEFAULT_MEMBERSHIP}
);
view.$('.leave-team-link').click();
AjaxHelpers.respondWithTextError(requests, 400, errorMessage);
expect($('.msg-content .copy').text().trim()).toBe(expectedMessage);
};
// verify user_message
verifyErrorMessage(
requests,
JSON.stringify({'user_message': "can't remove user from team"}),
"can't remove user from team"
);
// verify generic error message
verifyErrorMessage(
requests,
'',
'An error occurred. Try again.'
);
// verify error message when json parsing succeeded but error message format is incorrect
verifyErrorMessage(
requests,
JSON.stringify({'blah': "can't remove user from team"}),
'An error occurred. Try again.'
);
});
});
});
});
});
......@@ -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(
......
;(function (define) {
'use strict';
define(['backbone',
'underscore',
'gettext',
'teams/js/views/team_utils',
'text!teams/templates/team-join.underscore'],
function (Backbone, _, gettext, TeamUtils, teamJoinTemplate) {
return Backbone.View.extend({
errorMessage: gettext("An error occurred. Try again."),
alreadyMemberMessage: gettext("You already belong to another team."),
teamFullMessage: gettext("This team is full."),
events: {
"click .action-primary": "joinTeam"
},
initialize: function(options) {
this.template = _.template(teamJoinTemplate);
this.maxTeamSize = options.maxTeamSize;
this.currentUsername = options.currentUsername;
this.teamMembershipsUrl = options.teamMembershipsUrl;
_.bindAll(this, 'render', 'joinTeam', 'getUserTeamInfo');
this.listenTo(this.model, "change", this.render);
},
render: function() {
var message,
showButton,
teamHasSpace;
var view = this;
this.getUserTeamInfo(this.currentUsername, view.maxTeamSize).done(function (info) {
teamHasSpace = info.teamHasSpace;
// if user is the member of current team then we wouldn't show anything
if (!info.memberOfCurrentTeam) {
showButton = !info.alreadyMember && teamHasSpace;
if (info.alreadyMember) {
message = info.memberOfCurrentTeam ? '' : view.alreadyMemberMessage;
} else if (!teamHasSpace) {
message = view.teamFullMessage;
}
}
view.$el.html(view.template({showButton: showButton, message: message}));
});
return view;
},
joinTeam: function () {
var view = this;
$.ajax({
type: 'POST',
url: view.teamMembershipsUrl,
data: {'username': view.currentUsername, 'team_id': view.model.get('id')}
}).done(function (data) {
view.model.fetch({});
}).fail(function (data) {
TeamUtils.parseAndShowMessage(data, view.errorMessage);
});
},
getUserTeamInfo: function (username, maxTeamSize) {
var deferred = $.Deferred();
var info = {
alreadyMember: false,
memberOfCurrentTeam: false,
teamHasSpace: false
};
info.memberOfCurrentTeam = TeamUtils.isUserMemberOfTeam(this.model.get('membership'), username);
var teamHasSpace = this.model.get('membership').length < maxTeamSize;
if (info.memberOfCurrentTeam) {
info.alreadyMember = true;
info.memberOfCurrentTeam = true;
deferred.resolve(info);
} else {
if (teamHasSpace) {
var view = this;
$.ajax({
type: 'GET',
url: view.teamMembershipsUrl,
data: {'username': username}
}).done(function (data) {
info.alreadyMember = (data.count > 0);
info.memberOfCurrentTeam = false;
info.teamHasSpace = teamHasSpace;
deferred.resolve(info);
}).fail(function (data) {
TeamUtils.parseAndShowMessage(data, view.errorMessage);
deferred.reject();
});
} else {
deferred.resolve(info);
}
}
return deferred.promise();
}
});
});
}).call(this, define || RequireJS.define);
......@@ -4,26 +4,85 @@
;(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({
errorMessage: gettext("An error occurred. Try again."),
events: {
'click .invite-link-input': 'selectText',
'click .leave-team-link': 'leaveTeam'
},
initialize: function (options) {
this.listenTo(this.model, "change", this.render);
this.courseID = options.courseID;
this.discussionTopicID = this.model.get('discussion_topic_id');
this.readOnly = options.readOnly;
this.maxTeamSize = options.maxTeamSize;
this.requestUsername = options.requestUsername;
this.isPrivileged = options.isPrivileged;
this.teamInviteUrl = options.teamInviteUrl;
this.teamMembershipDetailUrl = options.teamMembershipDetailUrl;
this.countries = TeamUtils.selectorOptionsArrayToHashWithBlank(options.countries);
this.languages = TeamUtils.selectorOptionsArrayToHashWithBlank(options.languages);
},
render: function () {
var memberships = this.model.get('membership'),
discussionTopicID = this.model.get('discussion_topic_id'),
isMember = TeamUtils.isUserMemberOfTeam(memberships, this.requestUsername);
this.$el.html(_.template(teamTemplate, {
courseID: this.courseID,
discussionTopicID: this.discussionTopicID,
readOnly: this.readOnly
discussionTopicID: discussionTopicID,
readOnly: !(this.isPrivileged || isMember),
country: this.countries[this.model.get('country')],
language: this.languages[this.model.get('language')],
membershipText: TeamUtils.teamCapacityText(memberships.length, this.maxTeamSize),
isMember: isMember,
hasCapacity: 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.model.get('membership'), 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();
},
leaveTeam: function (event) {
event.preventDefault();
var view = this;
$.ajax({
type: 'DELETE',
url: view.teamMembershipDetailUrl.replace('team_id', view.model.get('id'))
}).done(function (data) {
view.model.fetch({});
}).fail(function (data) {
TeamUtils.parseAndShowMessage(data, view.errorMessage);
});
}
});
......
/* 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;
})
);
},
showMessage: function (message) {
var messageElement = $('.teams-content .wrapper-msg');
messageElement.removeClass('is-hidden');
$('.teams-content .msg-content .copy').text(message);
messageElement.focus();
},
/**
* Parse `data` and show user message. If parsing fails than show `genericErrorMessage`
*/
parseAndShowMessage: function (data, genericErrorMessage) {
try {
var errors = JSON.parse(data.responseText);
this.showMessage(_.isUndefined(errors.user_message) ? genericErrorMessage : errors.user_message);
} catch (error) {
this.showMessage(genericErrorMessage);
}
}
};
});
}).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;
......
......@@ -17,11 +17,12 @@
'teams/js/views/my_teams',
'teams/js/views/topic_teams',
'teams/js/views/edit_team',
'teams/js/views/team_join',
'text!teams/templates/teams_tab.underscore'],
function (Backbone, _, gettext, HeaderView, HeaderModel, TabbedView,
TopicModel, TopicCollection, TeamModel, TeamCollection, TeamMembershipCollection,
TopicsView, TeamProfileView, MyTeamsView, TopicTeamsView, TeamEditView,
teamsTemplate) {
TeamJoinView, teamsTemplate) {
var TeamsHeaderModel = HeaderModel.extend({
initialize: function (attributes) {
_.extend(this.defaults, {nav_aria_label: gettext('teams')});
......@@ -52,11 +53,12 @@
this.topicUrl = options.topicUrl;
this.teamsUrl = options.teamsUrl;
this.teamMembershipsUrl = options.teamMembershipsUrl;
this.teamMembershipDetailUrl = options.teamMembershipDetailUrl;
this.maxTeamSize = options.maxTeamSize;
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
......@@ -241,7 +243,14 @@
countries: self.countries
}
});
deferred.resolve(self.createViewWithHeader(teamsView, topic));
deferred.resolve(
self.createViewWithHeader(
{
mainView: teamsView,
subject: topic
}
)
);
});
});
}
......@@ -267,38 +276,62 @@
deferred = $.Deferred(),
courseID = this.courseID;
self.getTopic(topicID).done(function(topic) {
self.getTeam(teamID).done(function(team) {
var readOnly = self.readOnlyDiscussion(team),
view = new TeamProfileView({
self.getTeam(teamID, true).done(function(team) {
var view = new TeamProfileView({
courseID: courseID,
model: team,
readOnly: readOnly
maxTeamSize: self.maxTeamSize,
isPrivileged: self.userInfo.privileged,
requestUsername: self.userInfo.username,
countries: self.countries,
languages: self.languages,
teamInviteUrl: self.teamsBaseUrl + '#teams/' + topicID + '/' + teamID + '?invite=true',
teamMembershipDetailUrl: self.teamMembershipDetailUrl
});
deferred.resolve(self.createViewWithHeader(view, team, topic));
var teamJoinView = new TeamJoinView(
{
model: team,
teamsUrl: self.teamsUrl,
maxTeamSize: self.maxTeamSize,
currentUsername: self.userInfo.username,
teamMembershipsUrl: self.teamMembershipsUrl
}
);
deferred.resolve(
self.createViewWithHeader(
{
mainView: view,
subject: team,
parentTopic: topic,
headerActionsView: teamJoinView
}
)
);
});
});
return deferred.promise();
},
createViewWithHeader: function (mainView, subject, parentTopic) {
createViewWithHeader: function (options) {
var router = this.router,
breadcrumbs, headerView;
breadcrumbs = [{
title: gettext('All Topics'),
url: '#browse'
}];
if (parentTopic) {
if (options.parentTopic) {
breadcrumbs.push({
title: parentTopic.get('name'),
url: '#topics/' + parentTopic.id
title: options.parentTopic.get('name'),
url: '#topics/' + options.parentTopic.id
});
}
headerView = new HeaderView({
model: new TeamsHeaderModel({
description: subject.get('description'),
title: subject.get('name'),
description: options.subject.get('description'),
title: options.subject.get('name'),
breadcrumbs: breadcrumbs
}),
headerActionsView: options.headerActionsView,
events: {
'click nav.breadcrumbs a.nav-item': function (event) {
var url = $(event.currentTarget).attr('href');
......@@ -309,7 +342,7 @@
});
return new ViewWithHeader({
header: headerView,
main: mainView
main: options.mainView
});
},
......@@ -350,18 +383,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 = this.teamsUrl + teamID + (expandUser ? '?expand=user': '');
if (team) {
team.url = teamUrl;
deferred.resolve(team);
} else {
team = new TeamModel({
id: teamID,
url: this.teamsUrl + teamID
url: teamUrl
});
team.fetch()
.done(function() {
......
<div class="join-team form-actions">
<% if (showButton) {%>
<button class="action action-primary">
<%- gettext("Join Team") %>
</button>
<% } else if (message) { %>
<p class="join-team-message"><%- message %></p>
<% } %>
</div>
<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>
<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>
......@@ -40,9 +40,11 @@
topicsUrl: '${ topics_url }',
teamsUrl: '${ teams_url }',
teamMembershipsUrl: '${ team_memberships_url }',
teamMembershipDetailUrl: '${ team_membership_detail_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>
......
......@@ -108,9 +108,11 @@ class TeamsDashboardView(View):
"topics_url": reverse('topics_list', request=request),
"teams_url": reverse('teams_list', request=request),
"team_memberships_url": reverse('team_membership_list', request=request),
"team_membership_detail_url": reverse('team_membership_detail', args=['team_id', user.username]),
"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)
......
......@@ -8,6 +8,7 @@
var HeaderView = Backbone.View.extend({
initialize: function (options) {
this.template = _.template(headerTemplate);
this.headerActionsView = options.headerActionsView;
this.listenTo(this.model, 'change', this.render);
this.render();
},
......@@ -15,6 +16,9 @@
render: function () {
var json = this.model.attributes;
this.$el.html(this.template(json));
if (this.headerActionsView) {
this.headerActionsView.setElement(this.$('.header-action-view')).render();
}
return this;
}
});
......
......@@ -800,7 +800,8 @@
'lms/include/teams/js/spec/views/teams_tab_spec.js',
'lms/include/teams/js/spec/views/topic_card_spec.js',
'lms/include/teams/js/spec/views/topic_teams_spec.js',
'lms/include/teams/js/spec/views/topics_spec.js'
'lms/include/teams/js/spec/views/topics_spec.js',
'lms/include/teams/js/spec/views/team_join_spec.js'
]);
}).call(this, requirejs, define);
......@@ -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;
}
}
}
......@@ -228,7 +228,7 @@
.action-view {
@extend %btn-pl-default-base;
float: right;
@include float(right);
margin: ($baseline/4) 0;
}
}
......@@ -242,11 +242,11 @@
.meta-detail {
margin-top: ($baseline/4);
margin-right: ($baseline*.75);
@include margin-right ($baseline*.75);
color: $gray;
.icon {
margin-right: ($baseline/4);
@include margin-right ($baseline/4);
}
}
......@@ -306,7 +306,7 @@
}
.team-activity {
float: right;
@include float(right);
}
.card-actions {
......@@ -328,7 +328,7 @@
display: block;
position: absolute;
top: ($baseline/2);
left: -($baseline/4);
@include left(-($baseline/4));
box-shadow: 1px 1px 1px 0 $blue-d1;
background-color: $m-blue-l2;
padding: ($baseline/10) ($baseline*.75);
......@@ -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 {
......@@ -426,7 +484,23 @@
}
}
.form-actions {
.required-wrapper {
display: inline-block;
vertical-align: top;
width: 60%; // TODO: susy grid
}
.optional-wrapper {
display: inline-block;
vertical-align: top;
width: 35%; // TODO: susy grid
@include margin-left(2%);
border-left: 2px solid $gray-l4;
padding-left: 2%;
}
}
.form-actions {
margin-top: $baseline;
}
......@@ -454,25 +528,24 @@
&:focus {
border: 1px solid $link-color;
color: $link-color;
}
}
}
.required-wrapper {
display: inline-block;
vertical-align: top;
width: 60%; // TODO: susy grid
}
.header-action-view {
display: inline-block;
width: 33%;
vertical-align: text-bottom;
.optional-wrapper {
display: inline-block;
vertical-align: top;
width: 35%; // TODO: susy grid
margin-left: 2%;
border-left: 2px solid $gray-l4;
padding-left: 2%;
.join-team.form-actions, .join-team-message {
@include text-align(right);
}
}
.join-team-message {
@extend %t-copy-sub1;
color: $gray-l1;
}
.team-actions {
@extend %ui-well;
margin: 20px 1.2%;
......@@ -629,3 +702,5 @@
.create-team.form-actions {
margin-top: $baseline;
}
......@@ -11,4 +11,5 @@
<h2 class="page-title"><%- title %></h2>
<p class="page-description"><%- description %></p>
</div>
<div class="header-action-view"></div>
</header>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment