From b4ed9a28b0349fe7f4f4dcad9b1a4f529c2b5187 Mon Sep 17 00:00:00 2001 From: Usman Khalid <2200617@gmail.com> Date: Wed, 5 Aug 2015 17:13:54 +0500 Subject: [PATCH] My Teams tab. TNL-1911 --- common/test/acceptance/pages/lms/teams.py | 25 ++++ .../test/acceptance/tests/lms/test_teams.py | 109 ++++++++++++----- lms/djangoapps/teams/models.py | 20 ++++ lms/djangoapps/teams/serializers.py | 7 ++ .../teams/js/collections/team_membership.js | 23 ++++ .../static/teams/js/models/team_membership.js | 21 ++++ .../teams/js/spec/teams_factory_spec.js | 3 +- .../static/teams/js/spec/views/teams_spec.js | 111 +++++++++++++++--- .../teams/js/spec/views/teams_tab_spec.js | 32 ++++- .../teams/static/teams/js/views/team_card.js | 17 ++- .../teams/static/teams/js/views/teams.js | 14 ++- .../teams/static/teams/js/views/teams_tab.js | 49 +++++--- .../teams/templates/teams/teams.html | 5 +- lms/djangoapps/teams/tests/factories.py | 7 +- lms/djangoapps/teams/tests/test_models.py | 48 ++++++++ lms/djangoapps/teams/views.py | 24 +++- 16 files changed, 435 insertions(+), 80 deletions(-) create mode 100644 lms/djangoapps/teams/static/teams/js/collections/team_membership.js create mode 100644 lms/djangoapps/teams/static/teams/js/models/team_membership.js create mode 100644 lms/djangoapps/teams/tests/test_models.py diff --git a/common/test/acceptance/pages/lms/teams.py b/common/test/acceptance/pages/lms/teams.py index 3eb3618578..c871aa46e4 100644 --- a/common/test/acceptance/pages/lms/teams.py +++ b/common/test/acceptance/pages/lms/teams.py @@ -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. diff --git a/common/test/acceptance/tests/lms/test_teams.py b/common/test/acceptance/tests/lms/test_teams.py index 881b0d4161..3f3ae2a169 100644 --- a/common/test/acceptance/tests/lms/test_teams.py +++ b/common/test/acceptance/tests/lms/test_teams.py @@ -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() diff --git a/lms/djangoapps/teams/models.py b/lms/djangoapps/teams/models.py index d96c527ea1..ab6ced3d6a 100644 --- a/lms/djangoapps/teams/models.py +++ b/lms/djangoapps/teams/models.py @@ -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 diff --git a/lms/djangoapps/teams/serializers.py b/lms/djangoapps/teams/serializers.py index 0ffa224b61..e75912117a 100644 --- a/lms/djangoapps/teams/serializers.py +++ b/lms/djangoapps/teams/serializers.py @@ -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() diff --git a/lms/djangoapps/teams/static/teams/js/collections/team_membership.js b/lms/djangoapps/teams/static/teams/js/collections/team_membership.js new file mode 100644 index 0000000000..226f591d61 --- /dev/null +++ b/lms/djangoapps/teams/static/teams/js/collections/team_membership.js @@ -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); diff --git a/lms/djangoapps/teams/static/teams/js/models/team_membership.js b/lms/djangoapps/teams/static/teams/js/models/team_membership.js new file mode 100644 index 0000000000..7bd5cfdc4a --- /dev/null +++ b/lms/djangoapps/teams/static/teams/js/models/team_membership.js @@ -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); diff --git a/lms/djangoapps/teams/static/teams/js/spec/teams_factory_spec.js b/lms/djangoapps/teams/static/teams/js/spec/teams_factory_spec.js index 911412ea88..fa79a194e9 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/teams_factory_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/teams_factory_spec.js @@ -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() { diff --git a/lms/djangoapps/teams/static/teams/js/spec/views/teams_spec.js b/lms/djangoapps/teams/static/teams/js/spec/views/teams_spec.js index 8b06632e8b..085d92b46f 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/views/teams_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/views/teams_spec.js @@ -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('
'); 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?' + ); }); }); }); diff --git a/lms/djangoapps/teams/static/teams/js/spec/views/teams_tab_spec.js b/lms/djangoapps/teams/static/teams/js/spec/views/teams_tab_spec.js index 8aee23a2d0..2a8c21e89f 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/views/teams_tab_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/views/teams_tab_spec.js @@ -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 () { diff --git a/lms/djangoapps/teams/static/teams/js/views/team_card.js b/lms/djangoapps/teams/static/teams/js/views/team_card.js index 6de32ea892..68eb780496 100644 --- a/lms/djangoapps/teams/static/teams/js/views/team_card.js +++ b/lms/djangoapps/teams/static/teams/js/views/team_card.js @@ -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: '', team_name: this.model.get('name'), span_end: ''}, + {span_start: '', team_name: this.teamModel().get('name'), span_end: ''}, 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}); } diff --git a/lms/djangoapps/teams/static/teams/js/views/teams.js b/lms/djangoapps/teams/static/teams/js/views/teams.js index 08a6bd0872..06ab4fe334 100644 --- a/lms/djangoapps/teams/static/teams/js/views/teams.js +++ b/lms/djangoapps/teams/static/teams/js/views/teams.js @@ -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; }, diff --git a/lms/djangoapps/teams/static/teams/js/views/teams_tab.js b/lms/djangoapps/teams/static/teams/js/views/teams_tab.js index 23942297a2..b985225d66 100644 --- a/lms/djangoapps/teams/static/teams/js/views/teams_tab.js +++ b/lms/djangoapps/teams/static/teams/js/views/teams_tab.js @@ -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: 'This is the new Teams tab.
'}) + 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); }, diff --git a/lms/djangoapps/teams/templates/teams/teams.html b/lms/djangoapps/teams/templates/teams/teams.html index d2ac44343a..6232294a65 100644 --- a/lms/djangoapps/teams/templates/teams/teams.html +++ b/lms/djangoapps/teams/templates/teams/teams.html @@ -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> diff --git a/lms/djangoapps/teams/tests/factories.py b/lms/djangoapps/teams/tests/factories.py index d3cc339ef2..1a491d296a 100644 --- a/lms/djangoapps/teams/tests/factories.py +++ b/lms/djangoapps/teams/tests/factories.py @@ -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 diff --git a/lms/djangoapps/teams/tests/test_models.py b/lms/djangoapps/teams/tests/test_models.py new file mode 100644 index 0000000000..885088ae4b --- /dev/null +++ b/lms/djangoapps/teams/tests/test_models.py @@ -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 + ) diff --git a/lms/djangoapps/teams/views.py b/lms/djangoapps/teams/views.py index 3430d0198a..eecbf62297 100644 --- a/lms/djangoapps/teams/views.py +++ b/lms/djangoapps/teams/views.py @@ -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