Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
1de9fc64
Commit
1de9fc64
authored
Aug 11, 2015
by
Usman Khalid
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #9200 from edx/usman/tnl1911-my-teams
My teams tab.
parents
bf0dddfe
b4ed9a28
Hide whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
435 additions
and
80 deletions
+435
-80
common/test/acceptance/pages/lms/teams.py
+25
-0
common/test/acceptance/tests/lms/test_teams.py
+81
-28
lms/djangoapps/teams/models.py
+20
-0
lms/djangoapps/teams/serializers.py
+7
-0
lms/djangoapps/teams/static/teams/js/collections/team_membership.js
+23
-0
lms/djangoapps/teams/static/teams/js/models/team_membership.js
+21
-0
lms/djangoapps/teams/static/teams/js/spec/teams_factory_spec.js
+2
-1
lms/djangoapps/teams/static/teams/js/spec/views/teams_spec.js
+97
-14
lms/djangoapps/teams/static/teams/js/spec/views/teams_tab_spec.js
+28
-4
lms/djangoapps/teams/static/teams/js/views/team_card.js
+11
-6
lms/djangoapps/teams/static/teams/js/views/teams.js
+9
-5
lms/djangoapps/teams/static/teams/js/views/teams_tab.js
+35
-14
lms/djangoapps/teams/templates/teams/teams.html
+4
-1
lms/djangoapps/teams/tests/factories.py
+6
-1
lms/djangoapps/teams/tests/test_models.py
+48
-0
lms/djangoapps/teams/views.py
+18
-6
No files found.
common/test/acceptance/pages/lms/teams.py
View file @
1de9fc64
...
...
@@ -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.
...
...
common/test/acceptance/tests/lms/test_teams.py
View file @
1de9fc64
...
...
@@ -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'
),
...
...
@@ -192,6 +215,55 @@ class TeamsTabTest(TeamsTabBase):
@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
):
"""
Tests for the Browse tab of the Teams page.
...
...
@@ -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
()
...
...
lms/djangoapps/teams/models.py
View file @
1de9fc64
...
...
@@ -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
lms/djangoapps/teams/serializers.py
View file @
1de9fc64
...
...
@@ -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
()
...
...
lms/djangoapps/teams/static/teams/js/collections/team_membership.js
0 → 100644
View file @
1de9fc64
;(
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
);
lms/djangoapps/teams/static/teams/js/models/team_membership.js
0 → 100644
View file @
1de9fc64
/**
* 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
);
lms/djangoapps/teams/static/teams/js/spec/teams_factory_spec.js
View file @
1de9fc64
...
...
@@ -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
()
{
...
...
lms/djangoapps/teams/static/teams/js/spec/views/teams_spec.js
View file @
1de9fc64
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?'
);
});
});
});
lms/djangoapps/teams/static/teams/js/spec/views/teams_tab_spec.js
View file @
1de9fc64
...
...
@@ -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
()
{
...
...
lms/djangoapps/teams/static/teams/js/views/team_card.js
View file @
1de9fc64
...
...
@@ -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
.
t
opic
.
get
(
'id'
)
+
'/'
+
this
.
model
.
get
(
'id'
);
var
url
=
'teams/'
+
this
.
t
eamModel
().
get
(
'topic_id'
)
+
'/'
+
this
.
teamModel
()
.
get
(
'id'
);
event
.
preventDefault
();
this
.
router
.
navigate
(
url
,
{
trigger
:
true
});
}
...
...
lms/djangoapps/teams/static/teams/js/views/teams.js
View file @
1de9fc64
...
...
@@ -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
;
},
...
...
lms/djangoapps/teams/static/teams/js/views/teams_tab.js
View file @
1de9fc64
...
...
@@ -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
);
},
...
...
lms/djangoapps/teams/templates/teams/teams.html
View file @
1de9fc64
...
...
@@ -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>
...
...
lms/djangoapps/teams/tests/factories.py
View file @
1de9fc64
...
...
@@ -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
lms/djangoapps/teams/tests/test_models.py
0 → 100644
View file @
1de9fc64
# -*- 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
)
lms/djangoapps/teams/views.py
View file @
1de9fc64
...
...
@@ -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
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment