@@ -11,6 +11,7 @@ from .fields import FieldsMixin
|
||||
|
||||
|
||||
TOPIC_CARD_CSS = 'div.wrapper-card-core'
|
||||
TEAMS_BUTTON_CSS = 'a.nav-item[data-index="0"]'
|
||||
BROWSE_BUTTON_CSS = 'a.nav-item[data-index="1"]'
|
||||
TEAMS_LINK_CSS = '.action-view'
|
||||
TEAMS_HEADER_CSS = '.teams-header'
|
||||
@@ -36,11 +37,35 @@ class TeamsPage(CoursePage):
|
||||
)
|
||||
return self.q(css=main_page_content_css).text[0]
|
||||
|
||||
def active_tab(self):
|
||||
""" Get the active tab. """
|
||||
return self.q(css='.is-active').attrs('data-url')[0]
|
||||
|
||||
def browse_topics(self):
|
||||
""" View the Browse tab of the Teams page. """
|
||||
self.q(css=BROWSE_BUTTON_CSS).click()
|
||||
|
||||
|
||||
class MyTeamsPage(CoursePage, PaginatedUIMixin):
|
||||
"""
|
||||
The 'My Teams' tab of the Teams page.
|
||||
"""
|
||||
|
||||
url_path = "teams/#my-teams"
|
||||
|
||||
def is_browser_on_page(self):
|
||||
"""Check if the "My Teams" tab is being viewed."""
|
||||
button_classes = self.q(css=TEAMS_BUTTON_CSS).attrs('class')
|
||||
if len(button_classes) == 0:
|
||||
return False
|
||||
return 'is-active' in button_classes[0]
|
||||
|
||||
@property
|
||||
def team_cards(self):
|
||||
"""Get all the team cards on the page."""
|
||||
return self.q(css='.team-card')
|
||||
|
||||
|
||||
class BrowseTopicsPage(CoursePage, PaginatedUIMixin):
|
||||
"""
|
||||
The 'Browse' tab of the Teams page.
|
||||
|
||||
@@ -17,7 +17,7 @@ from ...fixtures.discussion import (
|
||||
from ...pages.lms.auto_auth import AutoAuthPage
|
||||
from ...pages.lms.course_info import CourseInfoPage
|
||||
from ...pages.lms.tab_nav import TabNavPage
|
||||
from ...pages.lms.teams import TeamsPage, BrowseTopicsPage, BrowseTeamsPage, CreateTeamPage, TeamPage
|
||||
from ...pages.lms.teams import TeamsPage, MyTeamsPage, BrowseTopicsPage, BrowseTeamsPage, CreateTeamPage, TeamPage
|
||||
|
||||
|
||||
class TeamsTabBase(UniqueCourseTest):
|
||||
@@ -84,10 +84,33 @@ class TeamsTabBase(UniqueCourseTest):
|
||||
if present:
|
||||
self.assertIn("Teams", self.tab_nav.tab_names)
|
||||
self.teams_page.visit()
|
||||
self.assertEqual("This is the new Teams tab.", self.teams_page.get_body_text())
|
||||
self.assertEqual(self.teams_page.active_tab(), 'my-teams')
|
||||
self.assertEqual("Showing 0 out of 0 total", self.teams_page.get_body_text())
|
||||
else:
|
||||
self.assertNotIn("Teams", self.tab_nav.tab_names)
|
||||
|
||||
def verify_teams(self, page, expected_teams):
|
||||
"""Verify that the list of team cards on the current page match the expected teams in order."""
|
||||
|
||||
def assert_team_equal(expected_team, team_card_name, team_card_description):
|
||||
"""
|
||||
Helper to assert that a single team card has the expected name and
|
||||
description.
|
||||
"""
|
||||
self.assertEqual(expected_team['name'], team_card_name)
|
||||
self.assertEqual(expected_team['description'], team_card_description)
|
||||
|
||||
team_cards = page.team_cards
|
||||
team_card_names = [
|
||||
team_card.find_element_by_css_selector('.card-title').text
|
||||
for team_card in team_cards.results
|
||||
]
|
||||
team_card_descriptions = [
|
||||
team_card.find_element_by_css_selector('.card-description').text
|
||||
for team_card in team_cards.results
|
||||
]
|
||||
map(assert_team_equal, expected_teams, team_card_names, team_card_descriptions)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@attr('shard_5')
|
||||
@@ -159,7 +182,7 @@ class TeamsTabTest(TeamsTabBase):
|
||||
|
||||
@ddt.data(
|
||||
('browse', 'div.topics-list'),
|
||||
('teams', 'p.temp-tab-view'),
|
||||
('my-teams', 'div.teams-paging-header'),
|
||||
('teams/{topic_id}/{team_id}', 'div.discussion-module'),
|
||||
('topics/{topic_id}/create-team', 'div.create-team-instructions'),
|
||||
('topics/{topic_id}', 'div.teams-list'),
|
||||
@@ -191,6 +214,55 @@ class TeamsTabTest(TeamsTabBase):
|
||||
self.assertTrue(self.teams_page.q(css=selector).visible)
|
||||
|
||||
|
||||
@attr('shard_5')
|
||||
class MyTeamsTest(TeamsTabBase):
|
||||
"""
|
||||
Tests for the "My Teams" tab of the Teams page.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(MyTeamsTest, 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.my_teams_page = MyTeamsPage(self.browser, self.course_id)
|
||||
|
||||
def test_not_member_of_any_teams(self):
|
||||
"""
|
||||
Scenario: Visiting the My Teams page when user is not a member of any team should not display any teams.
|
||||
Given I am enrolled in a course with a team configuration and a topic but am not a member of a team
|
||||
When I visit the My Teams page
|
||||
Then I should see a pagination header showing no teams
|
||||
And I should see no teams
|
||||
And I should not see a pagination footer
|
||||
"""
|
||||
self.my_teams_page.visit()
|
||||
self.assertEqual(self.my_teams_page.get_pagination_header_text(), 'Showing 0 out of 0 total')
|
||||
self.assertEqual(len(self.my_teams_page.team_cards), 0, msg='Expected to see no team cards')
|
||||
self.assertFalse(
|
||||
self.my_teams_page.pagination_controls_visible(),
|
||||
msg='Expected paging footer to be invisible'
|
||||
)
|
||||
|
||||
def test_member_of_a_team(self):
|
||||
"""
|
||||
Scenario: Visiting the My Teams page when user is a member of a team should display the teams.
|
||||
Given I am enrolled in a course with a team configuration and a topic and am a member of a team
|
||||
When I visit the My Teams page
|
||||
Then I should see a pagination header showing the number of teams
|
||||
And I should see all the expected team cards
|
||||
And I should not see a pagination footer
|
||||
"""
|
||||
teams = self.create_teams(self.topic, 1)
|
||||
self.create_membership(self.user_info['username'], teams[0]['id'])
|
||||
self.my_teams_page.visit()
|
||||
self.assertEqual(self.my_teams_page.get_pagination_header_text(), 'Showing 1 out of 1 total')
|
||||
self.verify_teams(self.my_teams_page, teams)
|
||||
self.assertFalse(
|
||||
self.my_teams_page.pagination_controls_visible(),
|
||||
msg='Expected paging footer to be invisible'
|
||||
)
|
||||
|
||||
|
||||
@attr('shard_5')
|
||||
class BrowseTopicsTest(TeamsTabBase):
|
||||
"""
|
||||
@@ -344,28 +416,6 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
|
||||
self.assertEqual(self.browse_teams_page.header_topic_name, self.topic['name'])
|
||||
self.assertEqual(self.browse_teams_page.header_topic_description, self.topic['description'])
|
||||
|
||||
def verify_teams(self, expected_teams):
|
||||
"""Verify that the list of team cards on the current page match the expected teams in order."""
|
||||
|
||||
def assert_team_equal(expected_team, team_card_name, team_card_description):
|
||||
"""
|
||||
Helper to assert that a single team card has the expected name and
|
||||
description.
|
||||
"""
|
||||
self.assertEqual(expected_team['name'], team_card_name)
|
||||
self.assertEqual(expected_team['description'], team_card_description)
|
||||
|
||||
team_cards = self.browse_teams_page.team_cards
|
||||
team_card_names = [
|
||||
team_card.find_element_by_css_selector('.card-title').text
|
||||
for team_card in team_cards.results
|
||||
]
|
||||
team_card_descriptions = [
|
||||
team_card.find_element_by_css_selector('.card-description').text
|
||||
for team_card in team_cards.results
|
||||
]
|
||||
map(assert_team_equal, expected_teams, team_card_names, team_card_descriptions)
|
||||
|
||||
def verify_on_page(self, page_num, total_teams, pagination_header_text, footer_visible):
|
||||
"""
|
||||
Verify that we are on the correct team list page.
|
||||
@@ -381,7 +431,10 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
|
||||
"""
|
||||
alphabetized_teams = sorted(total_teams, key=lambda team: team['name'])
|
||||
self.assertEqual(self.browse_teams_page.get_pagination_header_text(), pagination_header_text)
|
||||
self.verify_teams(alphabetized_teams[(page_num - 1) * self.TEAMS_PAGE_SIZE:page_num * self.TEAMS_PAGE_SIZE])
|
||||
self.verify_teams(
|
||||
self.browse_teams_page,
|
||||
alphabetized_teams[(page_num - 1) * self.TEAMS_PAGE_SIZE:page_num * self.TEAMS_PAGE_SIZE]
|
||||
)
|
||||
self.assertEqual(
|
||||
self.browse_teams_page.pagination_controls_visible(),
|
||||
footer_visible,
|
||||
@@ -424,7 +477,7 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
|
||||
self.browse_teams_page.visit()
|
||||
self.verify_page_header()
|
||||
self.assertEqual(self.browse_teams_page.get_pagination_header_text(), 'Showing 1-10 out of 10 total')
|
||||
self.verify_teams(teams)
|
||||
self.verify_teams(self.browse_teams_page, teams)
|
||||
self.assertFalse(
|
||||
self.browse_teams_page.pagination_controls_visible(),
|
||||
msg='Expected paging footer to be invisible'
|
||||
@@ -488,7 +541,7 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
|
||||
teams = self.create_teams(self.topic, 1)
|
||||
self.browse_teams_page.visit()
|
||||
self.verify_page_header()
|
||||
self.verify_teams(teams)
|
||||
self.verify_teams(self.browse_teams_page, teams)
|
||||
self.create_membership(self.user_info['username'], teams[0]['id'])
|
||||
self.browser.refresh()
|
||||
self.browse_teams_page.wait_for_ajax()
|
||||
|
||||
@@ -88,3 +88,23 @@ class CourseTeamMembership(models.Model):
|
||||
user = models.ForeignKey(User)
|
||||
team = models.ForeignKey(CourseTeam, related_name='membership')
|
||||
date_joined = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
@classmethod
|
||||
def get_memberships(cls, username=None, course_ids=None, team_id=None):
|
||||
"""
|
||||
Get a queryset of memberships.
|
||||
|
||||
Args:
|
||||
username (unicode, optional): The username to filter on.
|
||||
course_ids (list of unicode, optional) Course Ids to filter on.
|
||||
team_id (unicode, optional): The team_id to filter on.
|
||||
"""
|
||||
queryset = cls.objects.all()
|
||||
if username is not None:
|
||||
queryset = queryset.filter(user__username=username)
|
||||
if course_ids is not None:
|
||||
queryset = queryset.filter(team__course_id__in=course_ids)
|
||||
if team_id is not None:
|
||||
queryset = queryset.filter(team__team_id=team_id)
|
||||
|
||||
return queryset
|
||||
|
||||
@@ -113,6 +113,13 @@ class MembershipSerializer(serializers.ModelSerializer):
|
||||
read_only_fields = ("date_joined",)
|
||||
|
||||
|
||||
class PaginatedMembershipSerializer(PaginationSerializer):
|
||||
"""Serializes team memberships with support for pagination."""
|
||||
class Meta(object):
|
||||
"""Defines meta information for the PaginatedMembershipSerializer."""
|
||||
object_serializer_class = MembershipSerializer
|
||||
|
||||
|
||||
class BaseTopicSerializer(serializers.Serializer):
|
||||
"""Serializes a topic without team_count."""
|
||||
description = serializers.CharField()
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
;(function (define) {
|
||||
'use strict';
|
||||
define(['common/js/components/collections/paging_collection', 'teams/js/models/team_membership'],
|
||||
function(PagingCollection, TeamMembershipModel) {
|
||||
var TeamMembershipCollection = PagingCollection.extend({
|
||||
initialize: function(team_memberships, options) {
|
||||
PagingCollection.prototype.initialize.call(this);
|
||||
|
||||
this.course_id = options.course_id;
|
||||
this.username = options.username;
|
||||
this.perPage = options.per_page || 10;
|
||||
this.server_api['expand'] = 'team';
|
||||
this.server_api['course_id'] = function () { return encodeURIComponent(this.course_id); };
|
||||
this.server_api['username'] = this.username;
|
||||
delete this.server_api['sort_order']; // Sort order is not specified for the TeamMembership API
|
||||
delete this.server_api['order_by']; // Order by is not specified for the TeamMembership API
|
||||
},
|
||||
|
||||
model: TeamMembershipModel
|
||||
});
|
||||
return TeamMembershipCollection;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Model for a team membership.
|
||||
*/
|
||||
(function (define) {
|
||||
'use strict';
|
||||
define(['backbone', 'teams/js/models/team'], function (Backbone, TeamModel) {
|
||||
var TeamMembership = Backbone.Model.extend({
|
||||
defaults: {
|
||||
date_joined: '',
|
||||
team: null,
|
||||
user: null
|
||||
},
|
||||
|
||||
parse: function (response, options) {
|
||||
response.team = new TeamModel(response.team);
|
||||
return response;
|
||||
}
|
||||
});
|
||||
return TeamMembership;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
@@ -21,7 +21,8 @@ define(["jquery", "backbone", "teams/js/teams_tab_factory"],
|
||||
});
|
||||
|
||||
it("can load templates", function() {
|
||||
expect($("body").text()).toContain("This is the new Teams tab");
|
||||
expect($("body").text()).toContain("My Teams");
|
||||
expect($("body").text()).toContain("Showing 0 out of 0 total");
|
||||
});
|
||||
|
||||
it("displays a header", function() {
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
define([
|
||||
'backbone', 'teams/js/collections/team', 'teams/js/views/teams'
|
||||
], function (Backbone, TeamCollection, TeamsView) {
|
||||
'backbone',
|
||||
'teams/js/collections/team',
|
||||
'teams/js/collections/team_membership',
|
||||
'teams/js/views/teams'
|
||||
], function (Backbone, TeamCollection, TeamMembershipCollection, TeamsView) {
|
||||
'use strict';
|
||||
describe('Teams View', function () {
|
||||
var teamsView, teamCollection, initialTeams,
|
||||
createTeams = function (startIndex, stopIndex) {
|
||||
initialTeamMemberships, teamMembershipCollection;
|
||||
|
||||
var createTeams = function (startIndex, stopIndex) {
|
||||
return _.map(_.range(startIndex, stopIndex + 1), function (i) {
|
||||
return {
|
||||
name: "team " + i,
|
||||
@@ -29,6 +34,30 @@ define([
|
||||
['fr', 'French']
|
||||
];
|
||||
|
||||
var createTeamMemberships = function(startIndex, stopIndex) {
|
||||
var teams = createTeams(startIndex, stopIndex)
|
||||
return _.map(_.range(startIndex, stopIndex + 1), function (i) {
|
||||
return {
|
||||
user: {
|
||||
'username': 'andya',
|
||||
'url': 'https://openedx.example.com/api/user/v1/accounts/andya'
|
||||
},
|
||||
team: teams[i-1]
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
var verifyCards = function(view, teams) {
|
||||
var teamCards = view.$('.team-card');
|
||||
_.each(teams, function (team, index) {
|
||||
var currentCard = teamCards.eq(index);
|
||||
expect(currentCard.text()).toMatch(team.name);
|
||||
expect(currentCard.text()).toMatch(_.object(languages)[team.language]);
|
||||
expect(currentCard.text()).toMatch(_.object(countries)[team.country]);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
setFixtures('<div class="teams-container"></div>');
|
||||
initialTeams = createTeams(1, 5);
|
||||
@@ -40,8 +69,31 @@ define([
|
||||
start: 0,
|
||||
results: initialTeams
|
||||
},
|
||||
{course_id: 'my/course/id', parse: true}
|
||||
{
|
||||
course_id: 'my/course/id',
|
||||
parse: true
|
||||
}
|
||||
);
|
||||
|
||||
initialTeamMemberships = createTeamMemberships(1, 5);
|
||||
teamMembershipCollection = new TeamMembershipCollection(
|
||||
{
|
||||
count: 11,
|
||||
num_pages: 3,
|
||||
current_page: 1,
|
||||
start: 0,
|
||||
results: initialTeamMemberships
|
||||
},
|
||||
{
|
||||
course_id: 'my/course/id',
|
||||
parse: true,
|
||||
url: 'api/teams/team_memberships',
|
||||
username: 'andya',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('can render itself with teams collection', function () {
|
||||
teamsView = new TeamsView({
|
||||
el: '.teams-container',
|
||||
collection: teamCollection,
|
||||
@@ -50,21 +102,52 @@ define([
|
||||
languages: languages
|
||||
}
|
||||
}).render();
|
||||
});
|
||||
|
||||
it('can render itself', function () {
|
||||
var footerEl = teamsView.$('.teams-paging-footer'),
|
||||
teamCards = teamsView.$('.team-card');
|
||||
expect(teamsView.$('.teams-paging-header').text()).toMatch('Showing 1-5 out of 6 total');
|
||||
_.each(initialTeams, function (team, index) {
|
||||
var currentCard = teamCards.eq(index);
|
||||
|
||||
expect(currentCard.text()).toMatch(team.name);
|
||||
expect(currentCard.text()).toMatch(_.object(languages)[team.language]);
|
||||
expect(currentCard.text()).toMatch(_.object(countries)[team.country]);
|
||||
});
|
||||
var footerEl = teamsView.$('.teams-paging-footer');
|
||||
expect(footerEl.text()).toMatch('1\\s+out of\\s+\/\\s+2');
|
||||
expect(footerEl).not.toHaveClass('hidden');
|
||||
|
||||
verifyCards(teamsView, initialTeams);
|
||||
});
|
||||
|
||||
it('can render itself with team memberships collection', function () {
|
||||
teamsView = new TeamsView({
|
||||
el: '.teams-container',
|
||||
collection: teamMembershipCollection,
|
||||
teamParams: {}
|
||||
}).render();
|
||||
|
||||
expect(teamsView.$('.teams-paging-header').text()).toMatch('Showing 1-5 out of 11 total');
|
||||
var footerEl = teamsView.$('.teams-paging-footer');
|
||||
expect(footerEl.text()).toMatch('1\\s+out of\\s+\/\\s+3');
|
||||
expect(footerEl).not.toHaveClass('hidden');
|
||||
|
||||
verifyCards(teamsView, initialTeamMemberships);
|
||||
});
|
||||
|
||||
it ('can render the actions view', function () {
|
||||
teamsView = new TeamsView({
|
||||
el: '.teams-container',
|
||||
collection: teamCollection,
|
||||
teamParams: {},
|
||||
}).render();
|
||||
|
||||
expect(teamsView.$el.text()).not.toContain(
|
||||
'Are you having trouble finding a team to join?'
|
||||
);
|
||||
|
||||
teamsView = new TeamsView({
|
||||
el: '.teams-container',
|
||||
collection: teamCollection,
|
||||
teamParams: {},
|
||||
showActions: true
|
||||
}).render();
|
||||
|
||||
expect(teamsView.$el.text()).toContain(
|
||||
'Are you having trouble finding a team to join?'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,6 +38,28 @@ define([
|
||||
team_count: 0
|
||||
}]
|
||||
},
|
||||
teamMemberships: {
|
||||
count: 1,
|
||||
currentPage: 1,
|
||||
numPages: 1,
|
||||
next: null,
|
||||
previous: null,
|
||||
results: [
|
||||
{
|
||||
user: {
|
||||
username: 'andya',
|
||||
url: 'https://openedx.example.com/api/user/v1/accounts/andya'
|
||||
},
|
||||
team: {
|
||||
description: '',
|
||||
name: 'Discrete Maths',
|
||||
id: 'dm',
|
||||
topic_id: 'algorithms'
|
||||
},
|
||||
date_joined: '2015-04-09T17:31:56Z'
|
||||
},
|
||||
]
|
||||
},
|
||||
topicsUrl: 'api/topics/',
|
||||
topicUrl: 'api/topics/topic_id,test/course/id',
|
||||
teamsUrl: 'api/teams/',
|
||||
@@ -51,17 +73,19 @@ define([
|
||||
Backbone.history.stop();
|
||||
});
|
||||
|
||||
it('shows the teams tab initially', function () {
|
||||
it('shows the my teams tab initially', function () {
|
||||
expectHeader('See all teams in your course, organized by topic');
|
||||
expectContent('This is the new Teams tab.');
|
||||
expectContent('Showing 1 out of 1 total');
|
||||
expectContent('Discrete Maths');
|
||||
});
|
||||
|
||||
describe('Navigation', function () {
|
||||
it('can switch tabs', function () {
|
||||
teamsTabView.$('a.nav-item[data-url="browse"]').click();
|
||||
expectContent('test description');
|
||||
teamsTabView.$('a.nav-item[data-url="teams"]').click();
|
||||
expectContent('This is the new Teams tab.');
|
||||
teamsTabView.$('a.nav-item[data-url="my-teams"]').click();
|
||||
expectContent('Showing 1 out of 1 total');
|
||||
expectContent('Discrete Maths');
|
||||
});
|
||||
|
||||
it('displays and focuses an error message when trying to navigate to a nonexistent page', function () {
|
||||
|
||||
@@ -66,30 +66,35 @@
|
||||
CardView.prototype.initialize.apply(this, arguments);
|
||||
// TODO: show last activity detail view
|
||||
this.detailViews = [
|
||||
new TeamMembershipView({model: this.model, maxTeamSize: this.maxTeamSize}),
|
||||
new TeamMembershipView({model: this.teamModel(), maxTeamSize: this.maxTeamSize}),
|
||||
new TeamCountryLanguageView({
|
||||
model: this.model,
|
||||
model: this.teamModel(),
|
||||
countries: this.countries,
|
||||
languages: this.languages
|
||||
})
|
||||
];
|
||||
},
|
||||
|
||||
teamModel: function () {
|
||||
if (this.model.has('team')) { return this.model.get('team'); };
|
||||
return this.model;
|
||||
},
|
||||
|
||||
configuration: 'list_card',
|
||||
cardClass: 'team-card',
|
||||
title: function () { return this.model.get('name'); },
|
||||
description: function () { return this.model.get('description'); },
|
||||
title: function () { return this.teamModel().get('name'); },
|
||||
description: function () { return this.teamModel().get('description'); },
|
||||
details: function () { return this.detailViews; },
|
||||
actionClass: 'action-view',
|
||||
actionContent: function() {
|
||||
return interpolate(
|
||||
gettext('View %(span_start)s %(team_name)s %(span_end)s'),
|
||||
{span_start: '<span class="sr">', team_name: this.model.get('name'), span_end: '</span>'},
|
||||
{span_start: '<span class="sr">', team_name: this.teamModel().get('name'), span_end: '</span>'},
|
||||
true
|
||||
);
|
||||
},
|
||||
action: function (event) {
|
||||
var url = 'teams/' + this.topic.get('id') + '/' + this.model.get('id');
|
||||
var url = 'teams/' + this.teamModel().get('topic_id') + '/' + this.teamModel().get('id');
|
||||
event.preventDefault();
|
||||
this.router.navigate(url, {trigger: true});
|
||||
}
|
||||
|
||||
@@ -20,16 +20,20 @@
|
||||
});
|
||||
PaginatedView.prototype.initialize.call(this);
|
||||
this.teamParams = options.teamParams;
|
||||
this.showActions = options.showActions;
|
||||
},
|
||||
|
||||
render: function () {
|
||||
PaginatedView.prototype.render.call(this);
|
||||
|
||||
var teamActionsView = new TeamActionsView({
|
||||
teamParams: this.teamParams
|
||||
});
|
||||
this.$el.append(teamActionsView.$el);
|
||||
teamActionsView.render();
|
||||
if (this.showActions === true) {
|
||||
var teamActionsView = new TeamActionsView({
|
||||
teamParams: this.teamParams
|
||||
});
|
||||
this.$el.append(teamActionsView.$el);
|
||||
teamActionsView.render();
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
|
||||
@@ -11,13 +11,14 @@
|
||||
'teams/js/collections/topic',
|
||||
'teams/js/models/team',
|
||||
'teams/js/collections/team',
|
||||
'teams/js/collections/team_membership',
|
||||
'teams/js/views/topics',
|
||||
'teams/js/views/team_profile',
|
||||
'teams/js/views/teams',
|
||||
'teams/js/views/edit_team',
|
||||
'text!teams/templates/teams_tab.underscore'],
|
||||
function (Backbone, _, gettext, HeaderView, HeaderModel, TabbedView,
|
||||
TopicModel, TopicCollection, TeamModel, TeamCollection,
|
||||
TopicModel, TopicCollection, TeamModel, TeamCollection, TeamMembershipCollection,
|
||||
TopicsView, TeamProfileView, TeamsView, TeamEditView,
|
||||
teamsTemplate) {
|
||||
var ViewWithHeader = Backbone.View.extend({
|
||||
@@ -37,14 +38,17 @@
|
||||
|
||||
var TeamTabView = Backbone.View.extend({
|
||||
initialize: function(options) {
|
||||
var TempTabView, router;
|
||||
var router;
|
||||
this.courseID = options.courseID;
|
||||
this.topics = options.topics;
|
||||
this.teamMemberships = options.teamMemberships;
|
||||
this.topicUrl = options.topicUrl;
|
||||
this.teamsUrl = options.teamsUrl;
|
||||
this.teamMembershipsUrl = options.teamMembershipsUrl;
|
||||
this.maxTeamSize = options.maxTeamSize;
|
||||
this.languages = options.languages;
|
||||
this.countries = options.countries;
|
||||
this.username = options.username;
|
||||
// This slightly tedious approach is necessary
|
||||
// to use regular expressions within Backbone
|
||||
// routes, allowing us to capture which tab
|
||||
@@ -56,27 +60,44 @@
|
||||
['topics/:topic_id/create-team(/)', _.bind(this.newTeam, this)],
|
||||
['teams/:topic_id/:team_id(/)', _.bind(this.browseTeam, this)],
|
||||
[new RegExp('^(browse)\/?$'), _.bind(this.goToTab, this)],
|
||||
[new RegExp('^(teams)\/?$'), _.bind(this.goToTab, this)]
|
||||
[new RegExp('^(my-teams)\/?$'), _.bind(this.goToTab, this)]
|
||||
], function (route) {
|
||||
router.route.apply(router, route);
|
||||
});
|
||||
// TODO replace this with actual views!
|
||||
TempTabView = Backbone.View.extend({
|
||||
initialize: function (options) {
|
||||
this.text = options.text;
|
||||
},
|
||||
render: function () {
|
||||
this.$el.html(this.text);
|
||||
|
||||
this.teamMembershipsCollection = new TeamMembershipCollection(
|
||||
this.teamMemberships,
|
||||
{
|
||||
url: this.teamMembershipsUrl,
|
||||
course_id: this.courseID,
|
||||
username: this.username,
|
||||
parse: true,
|
||||
|
||||
}
|
||||
).bootstrap();
|
||||
|
||||
this.myTeamsView = new TeamsView({
|
||||
router: this.router,
|
||||
collection: this.teamMembershipsCollection,
|
||||
maxTeamSize: this.maxTeamSize,
|
||||
teamParams: {
|
||||
courseId: this.courseID,
|
||||
teamsUrl: this.teamsUrl,
|
||||
languages: this.languages,
|
||||
countries: this.countries
|
||||
}
|
||||
});
|
||||
|
||||
this.topicsCollection = new TopicCollection(
|
||||
this.topics,
|
||||
{url: options.topicsUrl, course_id: this.courseID, parse: true}
|
||||
).bootstrap();
|
||||
|
||||
this.topicsView = new TopicsView({
|
||||
collection: this.topicsCollection,
|
||||
router: this.router
|
||||
});
|
||||
|
||||
this.mainView = this.tabbedView = new ViewWithHeader({
|
||||
header: new HeaderView({
|
||||
model: new HeaderModel({
|
||||
@@ -87,8 +108,8 @@
|
||||
main: new TabbedView({
|
||||
tabs: [{
|
||||
title: gettext('My Teams'),
|
||||
url: 'teams',
|
||||
view: new TempTabView({text: '<p class="temp-tab-view">This is the new Teams tab.</p>'})
|
||||
url: 'my-teams',
|
||||
view: this.myTeamsView
|
||||
}, {
|
||||
title: gettext('Browse'),
|
||||
url: 'browse',
|
||||
@@ -170,9 +191,9 @@
|
||||
.done(function() {
|
||||
var teamsView = new TeamsView({
|
||||
router: router,
|
||||
topic: topic,
|
||||
collection: collection,
|
||||
maxTeamSize: self.maxTeamSize,
|
||||
showActions: true,
|
||||
teamParams: {
|
||||
courseId: self.courseID,
|
||||
teamsUrl: self.teamsUrl,
|
||||
@@ -366,7 +387,7 @@
|
||||
* the main teams tab, and adds an error message.
|
||||
*/
|
||||
notFoundError: function (message) {
|
||||
this.router.navigate('teams', {trigger: true});
|
||||
this.router.navigate('my-teams', {trigger: true});
|
||||
this.showWarning(message);
|
||||
},
|
||||
|
||||
|
||||
@@ -35,12 +35,15 @@
|
||||
TeamsTabFactory({
|
||||
courseID: '${ unicode(course.id) }',
|
||||
topics: ${ json.dumps(topics, cls=EscapedEdxJSONEncoder) },
|
||||
teamMemberships: ${ json.dumps(team_memberships, cls=EscapedEdxJSONEncoder) },
|
||||
topicUrl: '${ topic_url }',
|
||||
topicsUrl: '${ topics_url }',
|
||||
teamsUrl: '${ teams_url }',
|
||||
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) },
|
||||
username: '${ username }'
|
||||
});
|
||||
</%static:require_module>
|
||||
</%block>
|
||||
|
||||
@@ -5,7 +5,7 @@ from uuid import uuid4
|
||||
import factory
|
||||
from factory.django import DjangoModelFactory
|
||||
|
||||
from ..models import CourseTeam
|
||||
from ..models import CourseTeam, CourseTeamMembership
|
||||
|
||||
|
||||
class CourseTeamFactory(DjangoModelFactory):
|
||||
@@ -20,3 +20,8 @@ class CourseTeamFactory(DjangoModelFactory):
|
||||
discussion_topic_id = factory.LazyAttribute(lambda a: uuid4().hex)
|
||||
name = "Awesome Team"
|
||||
description = "A simple description"
|
||||
|
||||
|
||||
class CourseTeamMembershipFactory(DjangoModelFactory):
|
||||
"""Factory for CourseTeamMemberships."""
|
||||
FACTORY_FOR = CourseTeamMembership
|
||||
|
||||
48
lms/djangoapps/teams/tests/test_models.py
Normal file
48
lms/djangoapps/teams/tests/test_models.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests for the teams API at the HTTP request level."""
|
||||
import ddt
|
||||
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
from .factories import CourseTeamFactory, CourseTeamMembershipFactory
|
||||
from ..models import CourseTeamMembership
|
||||
|
||||
COURSE_KEY1 = CourseKey.from_string('edx/history/1')
|
||||
COURSE_KEY2 = CourseKey.from_string('edx/history/2')
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TeamMembershipTest(SharedModuleStoreTestCase):
|
||||
"""Tests for the TeamMembership model."""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Set up tests.
|
||||
"""
|
||||
super(TeamMembershipTest, self).setUp()
|
||||
|
||||
self.user1 = UserFactory.create(username='user1')
|
||||
self.user2 = UserFactory.create(username='user2')
|
||||
|
||||
self.team1 = CourseTeamFactory(course_id=COURSE_KEY1, team_id='team1')
|
||||
self.team2 = CourseTeamFactory(course_id=COURSE_KEY2, team_id='team2')
|
||||
|
||||
self.team_membership11 = CourseTeamMembershipFactory(user=self.user1, team=self.team1)
|
||||
self.team_membership12 = CourseTeamMembershipFactory(user=self.user2, team=self.team1)
|
||||
self.team_membership21 = CourseTeamMembershipFactory(user=self.user1, team=self.team2)
|
||||
|
||||
@ddt.data(
|
||||
(None, None, None, 3),
|
||||
('user1', None, None, 2),
|
||||
('user1', [COURSE_KEY1], None, 1),
|
||||
('user1', None, 'team1', 1),
|
||||
('user2', None, None, 1),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_get_memberships(self, username, course_ids, team_id, expected_count):
|
||||
self.assertEqual(
|
||||
CourseTeamMembership.get_memberships(username=username, course_ids=course_ids, team_id=team_id).count(),
|
||||
expected_count
|
||||
)
|
||||
@@ -52,12 +52,14 @@ from .serializers import (
|
||||
BaseTopicSerializer,
|
||||
TopicSerializer,
|
||||
PaginatedTopicSerializer,
|
||||
MembershipSerializer
|
||||
MembershipSerializer,
|
||||
PaginatedMembershipSerializer,
|
||||
)
|
||||
from .errors import AlreadyOnTeamInCourse, NotEnrolledInCourseForTeam
|
||||
|
||||
|
||||
# Constants
|
||||
TEAM_MEMBERSHIPS_PER_PAGE = 2
|
||||
TOPICS_PER_PAGE = 12
|
||||
|
||||
|
||||
@@ -91,14 +93,24 @@ class TeamsDashboardView(View):
|
||||
context={'course_id': course.id, 'sort_order': sort_order}
|
||||
)
|
||||
user = request.user
|
||||
|
||||
team_memberships = CourseTeamMembership.get_memberships(request.user.username, [course.id])
|
||||
team_memberships_page = Paginator(team_memberships, TEAM_MEMBERSHIPS_PER_PAGE).page(1)
|
||||
team_memberships_serializer = PaginatedMembershipSerializer(
|
||||
instance=team_memberships_page,
|
||||
context={'expand': ('team',)},
|
||||
)
|
||||
|
||||
context = {
|
||||
"course": course,
|
||||
"topics": topics_serializer.data,
|
||||
"topic_url": reverse(
|
||||
'topics_detail', kwargs={'topic_id': 'topic_id', 'course_id': str(course_id)}, request=request
|
||||
),
|
||||
"team_memberships": team_memberships_serializer.data,
|
||||
"topics_url": reverse('topics_list', request=request),
|
||||
"teams_url": reverse('teams_list', request=request),
|
||||
"team_memberships_url": reverse('team_membership_list', request=request),
|
||||
"languages": settings.ALL_LANGUAGES,
|
||||
"countries": list(countries),
|
||||
"username": user.username,
|
||||
@@ -789,9 +801,10 @@ class MembershipListView(ExpandableFieldViewMixin, GenericAPIView):
|
||||
|
||||
def get(self, request):
|
||||
"""GET /api/team/v0/team_membership"""
|
||||
queryset = CourseTeamMembership.objects.all()
|
||||
|
||||
specified_username_or_team = False
|
||||
username = None
|
||||
valid_courses = None
|
||||
team_id = None
|
||||
|
||||
if 'team_id' in request.QUERY_PARAMS:
|
||||
specified_username_or_team = True
|
||||
@@ -802,10 +815,10 @@ class MembershipListView(ExpandableFieldViewMixin, GenericAPIView):
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
if not has_team_api_access(request.user, team.course_id):
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
queryset = queryset.filter(team__team_id=team_id)
|
||||
|
||||
if 'username' in request.QUERY_PARAMS:
|
||||
specified_username_or_team = True
|
||||
username = request.QUERY_PARAMS['username']
|
||||
if not request.user.is_staff:
|
||||
enrolled_courses = (
|
||||
CourseEnrollment.enrollments_for_user(request.user).values_list('course_id', flat=True)
|
||||
@@ -818,8 +831,6 @@ class MembershipListView(ExpandableFieldViewMixin, GenericAPIView):
|
||||
for course_list in [enrolled_courses, staff_courses]
|
||||
for course_key_string in course_list
|
||||
]
|
||||
queryset = queryset.filter(team__course_id__in=valid_courses)
|
||||
queryset = queryset.filter(user__username=request.QUERY_PARAMS['username'])
|
||||
|
||||
if not specified_username_or_team:
|
||||
return Response(
|
||||
@@ -827,6 +838,7 @@ class MembershipListView(ExpandableFieldViewMixin, GenericAPIView):
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
queryset = CourseTeamMembership.get_memberships(username, valid_courses, team_id)
|
||||
page = self.paginate_queryset(queryset)
|
||||
serializer = self.get_pagination_serializer(page)
|
||||
return Response(serializer.data) # pylint: disable=maybe-no-member
|
||||
|
||||
Reference in New Issue
Block a user