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
d61d193d
Commit
d61d193d
authored
Jul 22, 2015
by
Daniel Friedman
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #8859 from edx/dan-f/teams-list-view
Teams List View
parents
6ee90509
21b39ca4
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
43 changed files
with
1054 additions
and
283 deletions
+1054
-283
common/static/common/js/components/views/paginated_view.js
+43
-0
common/static/common/js/components/views/paging_footer.js
+2
-1
common/static/common/js/components/views/paging_header.js
+2
-2
common/static/common/js/spec/components/paginated_view_spec.js
+184
-0
common/static/common/templates/components/paginated-view.underscore
+4
-0
common/static/js/spec/main_requirejs.js
+1
-0
common/test/acceptance/pages/lms/teams.py
+50
-1
common/test/acceptance/tests/lms/test_teams.py
+0
-0
lms/djangoapps/teams/api_urls.py
+2
-2
lms/djangoapps/teams/serializers.py
+43
-4
lms/djangoapps/teams/static/teams/js/collections/team.js
+24
-0
lms/djangoapps/teams/static/teams/js/models/team.js
+23
-0
lms/djangoapps/teams/static/teams/js/spec/teams_factory_spec.js
+8
-2
lms/djangoapps/teams/static/teams/js/spec/teams_spec.js
+53
-0
lms/djangoapps/teams/static/teams/js/spec/teams_tab_spec.js
+74
-0
lms/djangoapps/teams/static/teams/js/spec/topics_spec.js
+12
-140
lms/djangoapps/teams/static/teams/js/teams_tab_factory.js
+6
-10
lms/djangoapps/teams/static/teams/js/views/team_card.js
+85
-0
lms/djangoapps/teams/static/teams/js/views/teams.js
+33
-0
lms/djangoapps/teams/static/teams/js/views/teams_tab.js
+0
-0
lms/djangoapps/teams/static/teams/js/views/topic_card.js
+1
-1
lms/djangoapps/teams/static/teams/js/views/topics.js
+7
-44
lms/djangoapps/teams/static/teams/templates/team-country-language.underscore
+2
-0
lms/djangoapps/teams/static/teams/templates/teams.underscore
+4
-0
lms/djangoapps/teams/static/teams/templates/teams_tab.underscore
+12
-0
lms/djangoapps/teams/templates/teams/teams.html
+8
-1
lms/djangoapps/teams/tests/test_serializers.py
+151
-0
lms/djangoapps/teams/tests/test_views.py
+34
-10
lms/djangoapps/teams/urls.py
+2
-1
lms/djangoapps/teams/views.py
+34
-11
lms/static/js/components/card/views/card.js
+2
-1
lms/static/js/components/tabbed/views/tabbed_view.js
+38
-14
lms/static/js/spec/components/card/card_spec.js
+1
-1
lms/static/js/spec/components/tabbed/tabbed_view_spec.js
+50
-22
lms/static/js/spec/main.js
+2
-0
lms/static/sass/_developer.scss
+0
-3
lms/static/sass/base/_variables.scss
+3
-3
lms/static/sass/elements/_system-feedback.scss
+12
-1
lms/templates/components/card/card.underscore
+19
-0
lms/templates/components/tabbed/tab.underscore
+1
-1
lms/templates/components/tabbed/tabbed_view.underscore
+3
-5
lms/templates/ux/reference/teams-base.html
+9
-1
lms/templates/ux/reference/teams-create.html
+10
-1
No files found.
common/static/common/js/components/views/paginated_view.js
0 → 100644
View file @
d61d193d
;(
function
(
define
)
{
'use strict'
;
define
([
'backbone'
,
'underscore'
,
'common/js/components/views/paging_header'
,
'common/js/components/views/paging_footer'
,
'common/js/components/views/list'
,
'text!common/templates/components/paginated-view.underscore'
],
function
(
Backbone
,
_
,
PagingHeader
,
PagingFooter
,
ListView
,
paginatedViewTemplate
)
{
var
PaginatedView
=
Backbone
.
View
.
extend
({
initialize
:
function
()
{
var
ItemListView
=
ListView
.
extend
({
tagName
:
'div'
,
className
:
this
.
type
+
'-container'
,
itemViewClass
:
this
.
itemViewClass
});
this
.
listView
=
new
ItemListView
({
collection
:
this
.
options
.
collection
});
this
.
headerView
=
this
.
headerView
=
new
PagingHeader
({
collection
:
this
.
options
.
collection
});
this
.
footerView
=
new
PagingFooter
({
collection
:
this
.
options
.
collection
,
hideWhenOnePage
:
true
});
this
.
collection
.
on
(
'page_changed'
,
function
()
{
this
.
$
(
'.sr-is-focusable.sr-'
+
this
.
type
+
'-view'
).
focus
();
},
this
);
},
render
:
function
()
{
this
.
$el
.
html
(
_
.
template
(
paginatedViewTemplate
,
{
type
:
this
.
type
}));
this
.
assign
(
this
.
listView
,
'.'
+
this
.
type
+
'-list'
);
this
.
assign
(
this
.
headerView
,
'.'
+
this
.
type
+
'-paging-header'
);
this
.
assign
(
this
.
footerView
,
'.'
+
this
.
type
+
'-paging-footer'
);
return
this
;
},
assign
:
function
(
view
,
selector
)
{
view
.
setElement
(
this
.
$
(
selector
)).
render
();
}
});
return
PaginatedView
;
});
}).
call
(
this
,
define
||
RequireJS
.
define
);
common/static/common/js/components/views/paging_footer.js
View file @
d61d193d
...
...
@@ -23,7 +23,8 @@
var
onFirstPage
=
!
this
.
collection
.
hasPreviousPage
(),
onLastPage
=
!
this
.
collection
.
hasNextPage
();
if
(
this
.
hideWhenOnePage
)
{
if
(
this
.
collection
.
totalPages
<=
1
)
{
if
(
_
.
isUndefined
(
this
.
collection
.
totalPages
)
||
this
.
collection
.
totalPages
<=
1
)
{
this
.
$el
.
addClass
(
'hidden'
);
}
else
if
(
this
.
$el
.
hasClass
(
'hidden'
))
{
this
.
$el
.
removeClass
(
'hidden'
);
...
...
common/static/common/js/components/views/paging_header.js
View file @
d61d193d
...
...
@@ -16,9 +16,9 @@
render
:
function
()
{
var
message
,
start
=
this
.
collection
.
start
,
start
=
_
.
isUndefined
(
this
.
collection
.
start
)
?
0
:
this
.
collection
.
start
,
end
=
start
+
this
.
collection
.
length
,
num_items
=
this
.
collection
.
totalCount
,
num_items
=
_
.
isUndefined
(
this
.
collection
.
totalCount
)
?
0
:
this
.
collection
.
totalCount
,
context
=
{
first_index
:
Math
.
min
(
start
+
1
,
end
),
last_index
:
end
,
num_items
:
num_items
};
if
(
end
<=
1
)
{
message
=
interpolate
(
gettext
(
'Showing %(first_index)s out of %(num_items)s total'
),
context
,
true
);
...
...
common/static/common/js/spec/components/paginated_view_spec.js
0 → 100644
View file @
d61d193d
define
([
'backbone'
,
'underscore'
,
'common/js/spec_helpers/ajax_helpers'
,
'common/js/components/views/paginated_view'
,
'common/js/components/collections/paging_collection'
],
function
(
Backbone
,
_
,
AjaxHelpers
,
PaginatedView
,
PagingCollection
)
{
'use strict'
;
describe
(
'PaginatedView'
,
function
()
{
var
TestItemView
=
Backbone
.
View
.
extend
({
className
:
'test-item'
,
tagName
:
'div'
,
initialize
:
function
()
{
this
.
render
();
},
render
:
function
()
{
this
.
$el
.
text
(
this
.
model
.
get
(
'text'
));
return
this
;
}
}),
TestPaginatedView
=
PaginatedView
.
extend
({
type
:
'test'
,
itemViewClass
:
TestItemView
}),
testCollection
,
testView
,
initialItems
,
nextPageButtonCss
=
'.next-page-link'
,
previousPageButtonCss
=
'.previous-page-link'
,
generateItems
=
function
(
numItems
)
{
return
_
.
map
(
_
.
range
(
numItems
),
function
(
i
)
{
return
{
text
:
'item '
+
i
};
});
};
beforeEach
(
function
()
{
setFixtures
(
'<div class="test-container"></div>'
);
initialItems
=
generateItems
(
5
);
testCollection
=
new
PagingCollection
({
count
:
6
,
num_pages
:
2
,
current_page
:
1
,
start
:
0
,
results
:
initialItems
},
{
parse
:
true
});
testView
=
new
TestPaginatedView
({
el
:
'.test-container'
,
collection
:
testCollection
}).
render
();
});
/**
* Verify that the view's header reflects the page we're currently viewing.
* @param matchString the header we expect to see
*/
function
expectHeader
(
matchString
)
{
expect
(
testView
.
$
(
'.test-paging-header'
).
text
()).
toMatch
(
matchString
);
}
/**
* Verify that the list view renders the expected items
* @param expectedItems an array of topic objects we expect to see
*/
function
expectItems
(
expectedItems
)
{
var
$items
=
testView
.
$
(
'.test-item'
);
_
.
each
(
expectedItems
,
function
(
item
,
index
)
{
var
currentItem
=
$items
.
eq
(
index
);
expect
(
currentItem
.
text
()).
toMatch
(
item
.
text
);
});
}
/**
* Verify that the footer reflects the current pagination
* @param options a parameters hash containing:
* - currentPage: the one-indexed page we expect to be viewing
* - totalPages: the total number of pages to page through
* - isHidden: whether the footer is expected to be visible
*/
function
expectFooter
(
options
)
{
var
footerEl
=
testView
.
$
(
'.test-paging-footer'
);
expect
(
footerEl
.
text
())
.
toMatch
(
new
RegExp
(
options
.
currentPage
+
'
\\
s+out of
\\
s+
\
/
\\
s+'
+
testCollection
.
totalPages
));
expect
(
footerEl
.
hasClass
(
'hidden'
)).
toBe
(
options
.
isHidden
);
}
it
(
'can render the first of many pages'
,
function
()
{
expectHeader
(
'Showing 1-5 out of 6 total'
);
expectItems
(
initialItems
);
expectFooter
({
currentPage
:
1
,
totalPages
:
2
,
isHidden
:
false
});
});
it
(
'can render the only page'
,
function
()
{
initialItems
=
generateItems
(
1
);
testCollection
.
set
(
{
"count"
:
1
,
"num_pages"
:
1
,
"current_page"
:
1
,
"start"
:
0
,
"results"
:
initialItems
},
{
parse
:
true
}
);
expectHeader
(
'Showing 1 out of 1 total'
);
expectItems
(
initialItems
);
expectFooter
({
currentPage
:
1
,
totalPages
:
1
,
isHidden
:
true
});
});
it
(
'can change to the next page'
,
function
()
{
var
requests
=
AjaxHelpers
.
requests
(
this
),
newItems
=
generateItems
(
1
);
expectHeader
(
'Showing 1-5 out of 6 total'
);
expectItems
(
initialItems
);
expectFooter
({
currentPage
:
1
,
totalPages
:
2
,
isHidden
:
false
});
expect
(
requests
.
length
).
toBe
(
0
);
testView
.
$
(
nextPageButtonCss
).
click
();
expect
(
requests
.
length
).
toBe
(
1
);
AjaxHelpers
.
respondWithJson
(
requests
,
{
"count"
:
6
,
"num_pages"
:
2
,
"current_page"
:
2
,
"start"
:
5
,
"results"
:
newItems
});
expectHeader
(
'Showing 6-6 out of 6 total'
);
expectItems
(
newItems
);
expectFooter
({
currentPage
:
2
,
totalPages
:
2
,
isHidden
:
false
});
});
it
(
'can change to the previous page'
,
function
()
{
var
requests
=
AjaxHelpers
.
requests
(
this
),
previousPageItems
;
initialItems
=
generateItems
(
1
);
testCollection
.
set
(
{
"count"
:
6
,
"num_pages"
:
2
,
"current_page"
:
2
,
"start"
:
5
,
"results"
:
initialItems
},
{
parse
:
true
}
);
expectHeader
(
'Showing 6-6 out of 6 total'
);
expectItems
(
initialItems
);
expectFooter
({
currentPage
:
2
,
totalPages
:
2
,
isHidden
:
false
});
testView
.
$
(
previousPageButtonCss
).
click
();
previousPageItems
=
generateItems
(
5
);
AjaxHelpers
.
respondWithJson
(
requests
,
{
"count"
:
6
,
"num_pages"
:
2
,
"current_page"
:
1
,
"start"
:
0
,
"results"
:
previousPageItems
});
expectHeader
(
'Showing 1-5 out of 6 total'
);
expectItems
(
previousPageItems
);
expectFooter
({
currentPage
:
1
,
totalPages
:
2
,
isHidden
:
false
});
});
it
(
'sets focus for screen readers'
,
function
()
{
var
requests
=
AjaxHelpers
.
requests
(
this
);
spyOn
(
$
.
fn
,
'focus'
);
testView
.
$
(
nextPageButtonCss
).
click
();
AjaxHelpers
.
respondWithJson
(
requests
,
{
"count"
:
6
,
"num_pages"
:
2
,
"current_page"
:
2
,
"start"
:
5
,
"results"
:
generateItems
(
1
)
});
expect
(
testView
.
$
(
'.sr-is-focusable'
).
focus
).
toHaveBeenCalled
();
});
it
(
'does not change on server error'
,
function
()
{
var
requests
=
AjaxHelpers
.
requests
(
this
),
expectInitialState
=
function
()
{
expectHeader
(
'Showing 1-5 out of 6 total'
);
expectItems
(
initialItems
);
expectFooter
({
currentPage
:
1
,
totalPages
:
2
,
isHidden
:
false
});
};
expectInitialState
();
testView
.
$
(
nextPageButtonCss
).
click
();
requests
[
0
].
respond
(
500
);
expectInitialState
();
});
});
});
common/static/common/templates/components/paginated-view.underscore
0 → 100644
View file @
d61d193d
<div class="sr-is-focusable sr-<%= type %>-view" tabindex="-1"></div>
<div class="<%= type %>-paging-header"></div>
<div class="<%= type %>-list"></div>
<div class="<%= type %>-paging-footer"></div>
common/static/js/spec/main_requirejs.js
View file @
d61d193d
...
...
@@ -156,6 +156,7 @@
define
([
// Run the common tests that use RequireJS.
'common-requirejs/include/common/js/spec/components/list_spec.js'
,
'common-requirejs/include/common/js/spec/components/paginated_view_spec.js'
,
'common-requirejs/include/common/js/spec/components/paging_collection_spec.js'
,
'common-requirejs/include/common/js/spec/components/paging_header_spec.js'
,
'common-requirejs/include/common/js/spec/components/paging_footer_spec.js'
...
...
common/test/acceptance/pages/lms/teams.py
View file @
d61d193d
# -*- coding: utf-8 -*-
"""
Teams page.
Teams page
s
.
"""
from
.course_page
import
CoursePage
...
...
@@ -9,6 +9,8 @@ from ..common.paging import PaginatedUIMixin
TOPIC_CARD_CSS
=
'div.wrapper-card-core'
BROWSE_BUTTON_CSS
=
'a.nav-item[data-index="1"]'
TEAMS_LINK_CSS
=
'.action-view'
TEAMS_HEADER_CSS
=
'.teams-header'
class
TeamsPage
(
CoursePage
):
...
...
@@ -53,3 +55,50 @@ class BrowseTopicsPage(CoursePage, PaginatedUIMixin):
def
topic_cards
(
self
):
"""Return a list of the topic cards present on the page."""
return
self
.
q
(
css
=
TOPIC_CARD_CSS
)
.
results
def
browse_teams_for_topic
(
self
,
topic_name
):
"""
Show the teams list for `topic_name`.
"""
self
.
q
(
css
=
TEAMS_LINK_CSS
)
.
filter
(
text
=
'View Teams in the {topic_name} Topic'
.
format
(
topic_name
=
topic_name
)
)[
0
]
.
click
()
self
.
wait_for_ajax
()
class
BrowseTeamsPage
(
CoursePage
,
PaginatedUIMixin
):
"""
The paginated UI for browsing teams within a Topic on the Teams
page.
"""
def
__init__
(
self
,
browser
,
course_id
,
topic
):
"""
Set up `self.url_path` on instantiation, since it dynamically
reflects the current topic. Note that `topic` is a dict
representation of a topic following the same convention as a
course module's topic.
"""
super
(
BrowseTeamsPage
,
self
)
.
__init__
(
browser
,
course_id
)
self
.
topic
=
topic
self
.
url_path
=
"teams/#topics/{topic_id}"
.
format
(
topic_id
=
self
.
topic
[
'id'
])
def
is_browser_on_page
(
self
):
"""Check if we're on the teams list page for a particular topic."""
has_correct_url
=
self
.
url
.
endswith
(
self
.
url_path
)
teams_list_view_present
=
self
.
q
(
css
=
'.teams-main'
)
.
present
return
has_correct_url
and
teams_list_view_present
@property
def
header_topic_name
(
self
):
"""Get the topic name displayed by the page header"""
return
self
.
q
(
css
=
TEAMS_HEADER_CSS
+
' .page-title'
)[
0
]
.
text
@property
def
header_topic_description
(
self
):
"""Get the topic description displayed by the page header"""
return
self
.
q
(
css
=
TEAMS_HEADER_CSS
+
' .page-description'
)[
0
]
.
text
@property
def
team_cards
(
self
):
"""Get all the team cards on the page."""
return
self
.
q
(
css
=
'.team-card'
)
common/test/acceptance/tests/lms/test_teams.py
View file @
d61d193d
This diff is collapsed.
Click to expand it.
lms/djangoapps/teams/api_urls.py
View file @
d61d193d
...
...
@@ -19,7 +19,7 @@ TOPIC_ID_PATTERN = TEAM_ID_PATTERN.replace('team_id', 'topic_id')
urlpatterns
=
patterns
(
''
,
url
(
r'^v0/teams$'
,
r'^v0/teams
/
$'
,
TeamsListView
.
as_view
(),
name
=
"teams_list"
),
...
...
@@ -39,7 +39,7 @@ urlpatterns = patterns(
name
=
"topics_detail"
),
url
(
r'^v0/team_membership$'
,
r'^v0/team_membership
/
$'
,
MembershipListView
.
as_view
(),
name
=
"team_membership_list"
),
...
...
lms/djangoapps/teams/serializers.py
View file @
d61d193d
"""Defines serializers used by the Team API."""
from
django.contrib.auth.models
import
User
from
django.db.models
import
Count
from
rest_framework
import
serializers
from
openedx.core.lib.api.serializers
import
CollapsedReferenceSerializer
from
openedx.core.lib.api.serializers
import
CollapsedReferenceSerializer
,
PaginationSerializer
from
openedx.core.lib.api.fields
import
ExpandableField
from
.models
import
CourseTeam
,
CourseTeamMembership
from
openedx.core.djangoapps.user_api.serializers
import
UserSerializer
from
.models
import
CourseTeam
,
CourseTeamMembership
class
UserMembershipSerializer
(
serializers
.
ModelSerializer
):
"""Serializes CourseTeamMemberships with only user and date_joined
...
...
@@ -108,8 +112,43 @@ class MembershipSerializer(serializers.ModelSerializer):
read_only_fields
=
(
"date_joined"
,)
class
TopicSerializer
(
serializers
.
Serializer
):
"""Serializes a topic."""
class
Base
TopicSerializer
(
serializers
.
Serializer
):
"""Serializes a topic
without team_count
."""
description
=
serializers
.
CharField
()
name
=
serializers
.
CharField
()
id
=
serializers
.
CharField
()
# pylint: disable=invalid-name
class
TopicSerializer
(
BaseTopicSerializer
):
"""
Adds team_count to the basic topic serializer. Use only when
serializing a single topic. When serializing many topics, use
`PaginatedTopicSerializer` to avoid O(N) SQL queries.
"""
team_count
=
serializers
.
SerializerMethodField
(
'get_team_count'
)
def
get_team_count
(
self
,
topic
):
"""Get the number of teams associated with this topic"""
return
CourseTeam
.
objects
.
filter
(
topic_id
=
topic
[
'id'
])
.
count
()
class
PaginatedTopicSerializer
(
PaginationSerializer
):
"""Serializes a set of topics. Adds team_count field to each topic."""
class
Meta
(
object
):
"""Defines meta information for the PaginatedTopicSerializer."""
object_serializer_class
=
BaseTopicSerializer
def
__init__
(
self
,
*
args
,
**
kwargs
):
"""Adds team_count to each topic."""
super
(
PaginatedTopicSerializer
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
# The following query gets all the team_counts for each topic
# and outputs the result as a list of dicts (one per topic).
topic_ids
=
[
topic
[
'id'
]
for
topic
in
self
.
data
[
'results'
]]
teams_per_topic
=
CourseTeam
.
objects
.
filter
(
topic_id__in
=
topic_ids
)
.
values
(
'topic_id'
)
.
annotate
(
team_count
=
Count
(
'topic_id'
))
topics_to_team_count
=
{
d
[
'topic_id'
]:
d
[
'team_count'
]
for
d
in
teams_per_topic
}
for
topic
in
self
.
data
[
'results'
]:
topic
[
'team_count'
]
=
topics_to_team_count
.
get
(
topic
[
'id'
],
0
)
lms/djangoapps/teams/static/teams/js/collections/team.js
0 → 100644
View file @
d61d193d
;(
function
(
define
)
{
'use strict'
;
define
([
'common/js/components/collections/paging_collection'
,
'teams/js/models/team'
,
'gettext'
],
function
(
PagingCollection
,
TeamModel
,
gettext
)
{
var
TeamCollection
=
PagingCollection
.
extend
({
initialize
:
function
(
teams
,
options
)
{
PagingCollection
.
prototype
.
initialize
.
call
(
this
);
this
.
course_id
=
options
.
course_id
;
this
.
server_api
[
'topic_id'
]
=
this
.
topic_id
=
options
.
topic_id
;
this
.
perPage
=
options
.
per_page
;
this
.
server_api
[
'course_id'
]
=
function
()
{
return
encodeURIComponent
(
this
.
course_id
);
};
this
.
server_api
[
'order_by'
]
=
function
()
{
return
'name'
;
};
// TODO surface sort order in UI
delete
this
.
server_api
[
'sort_order'
];
// Sort order is not specified for the Team API
this
.
registerSortableField
(
'name'
,
gettext
(
'name'
));
this
.
registerSortableField
(
'open_slots'
,
gettext
(
'open_slots'
));
},
model
:
TeamModel
});
return
TeamCollection
;
});
}).
call
(
this
,
define
||
RequireJS
.
define
);
lms/djangoapps/teams/static/teams/js/models/team.js
0 → 100644
View file @
d61d193d
/**
* Model for a team.
*/
(
function
(
define
)
{
'use strict'
;
define
([
'backbone'
],
function
(
Backbone
)
{
var
Team
=
Backbone
.
Model
.
extend
({
defaults
:
{
id
:
''
,
name
:
''
,
is_active
:
null
,
course_id
:
''
,
topic_id
:
''
,
date_created
:
''
,
description
:
''
,
country
:
''
,
language
:
''
,
membership
:
[]
}
});
return
Team
;
});
}).
call
(
this
,
define
||
RequireJS
.
define
);
lms/djangoapps/teams/static/teams/js/spec/teams_factory_spec.js
View file @
d61d193d
...
...
@@ -7,7 +7,13 @@ define(["jquery", "backbone", "teams/js/teams_tab_factory"],
beforeEach
(
function
()
{
setFixtures
(
'<section class="teams-content"></section>'
);
teamsTab
=
new
TeamsTabFactory
(
$
(
".teams-content"
),
{
results
:
[]},
''
,
'edX/DemoX/Demo_Course'
);
teamsTab
=
new
TeamsTabFactory
({
topics
:
{
results
:
[]},
topics_url
:
''
,
teams_url
:
''
,
maxTeamSize
:
9999
course_id
:
'edX/DemoX/Demo_Course'
});
});
afterEach
(
function
()
{
...
...
@@ -19,7 +25,7 @@ define(["jquery", "backbone", "teams/js/teams_tab_factory"],
});
it
(
"displays a header"
,
function
()
{
expect
(
$
(
"body"
).
html
()).
toContain
(
"
Course teams are organized
"
);
expect
(
$
(
"body"
).
html
()).
toContain
(
"
See all teams in your course, organized by topic
"
);
});
});
}
...
...
lms/djangoapps/teams/static/teams/js/spec/teams_spec.js
0 → 100644
View file @
d61d193d
define
([
'teams/js/collections/team'
,
'teams/js/views/teams'
],
function
(
TeamCollection
,
TeamsView
)
{
'use strict'
;
describe
(
'TeamsView'
,
function
()
{
var
teamsView
,
teamCollection
,
initialTeams
,
createTeams
=
function
(
startIndex
,
stopIndex
)
{
return
_
.
map
(
_
.
range
(
startIndex
,
stopIndex
+
1
),
function
(
i
)
{
return
{
name
:
"team "
+
i
,
id
:
"id "
+
i
,
language
:
"English"
,
country
:
"Sealand"
,
is_active
:
true
,
membership
:
[]
};
});
};
beforeEach
(
function
()
{
setFixtures
(
'<div class="teams-container"></div>'
);
initialTeams
=
createTeams
(
1
,
5
);
teamCollection
=
new
TeamCollection
(
{
count
:
6
,
num_pages
:
2
,
current_page
:
1
,
start
:
0
,
results
:
initialTeams
},
{
course_id
:
'my/course/id'
,
parse
:
true
}
);
teamsView
=
new
TeamsView
({
el
:
'.teams-container'
,
collection
:
teamCollection
}).
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
(
team
.
language
);
expect
(
currentCard
.
text
()).
toMatch
(
team
.
country
);
});
expect
(
footerEl
.
text
()).
toMatch
(
'1
\\
s+out of
\\
s+
\
/
\\
s+2'
);
expect
(
footerEl
).
not
.
toHaveClass
(
'hidden'
);
});
});
});
lms/djangoapps/teams/static/teams/js/spec/teams_tab_spec.js
0 → 100644
View file @
d61d193d
define
([
'jquery'
,
'backbone'
,
'common/js/spec_helpers/ajax_helpers'
,
'teams/js/views/teams_tab'
],
function
(
$
,
Backbone
,
AjaxHelpers
,
TeamsTabView
)
{
'use strict'
;
describe
(
'TeamsTab'
,
function
()
{
var
teamsTabView
,
expectContent
=
function
(
text
)
{
expect
(
teamsTabView
.
$
(
'.page-content-main'
).
text
()).
toContain
(
text
);
},
expectHeader
=
function
(
text
)
{
expect
(
teamsTabView
.
$
(
'.teams-header'
).
text
()).
toContain
(
text
);
},
expectError
=
function
(
text
)
{
expect
(
teamsTabView
.
$
(
'.warning'
).
text
()).
toContain
(
text
);
};
beforeEach
(
function
()
{
setFixtures
(
'<div class="teams-content"></div>'
);
teamsTabView
=
new
TeamsTabView
({
el
:
$
(
'.teams-content'
),
topics
:
{
count
:
1
,
num_pages
:
1
,
current_page
:
1
,
start
:
0
,
results
:
[{
description
:
'test description'
,
name
:
'test topic'
,
id
:
'test_id'
,
team_count
:
0
}]
},
topic_url
:
'api/topics/topic_id,course_id'
,
topics_url
:
'topics_url'
,
teams_url
:
'teams_url'
,
course_id
:
'test/course/id'
}).
render
();
Backbone
.
history
.
start
();
});
afterEach
(
function
()
{
Backbone
.
history
.
stop
();
});
it
(
'shows the teams tab initially'
,
function
()
{
expectHeader
(
'See all teams in your course, organized by topic'
);
expectContent
(
'This is the new Teams tab.'
);
});
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.'
);
});
it
(
'displays an error message when trying to navigate to a nonexistent route'
,
function
()
{
teamsTabView
.
router
.
navigate
(
'test'
,
{
trigger
:
true
});
expectError
(
'The page "test" could not be found.'
);
});
it
(
'displays an error message when trying to navigate to a nonexistent topic'
,
function
()
{
var
requests
=
AjaxHelpers
.
requests
(
this
);
teamsTabView
.
router
.
navigate
(
'topics/test'
,
{
trigger
:
true
});
AjaxHelpers
.
expectRequest
(
requests
,
'GET'
,
'api/topics/test,course_id'
,
null
);
AjaxHelpers
.
respondWithError
(
requests
,
404
);
expectError
(
'The topic "test" could not be found.'
);
});
});
});
lms/djangoapps/teams/static/teams/js/spec/topics_spec.js
View file @
d61d193d
define
([
'
common/js/spec_helpers/ajax_helpers'
,
'
teams/js/collections/topic'
,
'teams/js/views/topics'
],
function
(
AjaxHelpers
,
TopicCollection
,
TopicsView
)
{
'teams/js/collections/topic'
,
'teams/js/views/topics'
],
function
(
TopicCollection
,
TopicsView
)
{
'use strict'
;
describe
(
'TopicsView'
,
function
()
{
var
initialTopics
,
topicCollection
,
topicsView
,
nextPageButtonCss
;
nextPageButtonCss
=
'.next-page-link'
;
function
generateTopics
(
startIndex
,
stopIndex
)
{
var
initialTopics
,
topicCollection
,
topicsView
,
generateTopics
=
function
(
startIndex
,
stopIndex
)
{
return
_
.
map
(
_
.
range
(
startIndex
,
stopIndex
+
1
),
function
(
i
)
{
return
{
"description"
:
"description "
+
i
,
...
...
@@ -16,7 +13,7 @@ define([
"team_count"
:
0
};
});
}
}
;
beforeEach
(
function
()
{
setFixtures
(
'<div class="topics-container"></div>'
);
...
...
@@ -34,143 +31,18 @@ define([
topicsView
=
new
TopicsView
({
el
:
'.topics-container'
,
collection
:
topicCollection
}).
render
();
});
/**
* Verify that the topics view's header reflects the page we're currently viewing.
* @param matchString the header we expect to see
*/
function
expectHeader
(
matchString
)
{
expect
(
topicsView
.
$
(
'.topics-paging-header'
).
text
()).
toMatch
(
matchString
);
}
/**
* Verify that the topics list view renders the expected topics
* @param expectedTopics an array of topic objects we expect to see
*/
function
expectTopics
(
expectedTopics
)
{
var
topicCards
;
topicCards
=
topicsView
.
$
(
'.topic-card'
);
_
.
each
(
expectedTopics
,
function
(
topic
,
index
)
{
it
(
'can render the first of many pages'
,
function
()
{
var
footerEl
=
topicsView
.
$
(
'.topics-paging-footer'
),
topicCards
=
topicsView
.
$
(
'.topic-card'
);
expect
(
topicsView
.
$
(
'.topics-paging-header'
).
text
()).
toMatch
(
'Showing 1-5 out of 6 total'
);
_
.
each
(
initialTopics
,
function
(
topic
,
index
)
{
var
currentCard
=
topicCards
.
eq
(
index
);
expect
(
currentCard
.
text
()).
toMatch
(
topic
.
name
);
expect
(
currentCard
.
text
()).
toMatch
(
topic
.
description
);
expect
(
currentCard
.
text
()).
toMatch
(
topic
.
team_count
+
' Teams'
);
});
}
/**
* Verify that the topics footer reflects the current pagination
* @param options a parameters hash containing:
* - currentPage: the one-indexed page we expect to be viewing
* - totalPages: the total number of pages to page through
* - isHidden: whether the footer is expected to be visible
*/
function
expectFooter
(
options
)
{
var
footerEl
=
topicsView
.
$
(
'.topics-paging-footer'
);
expect
(
footerEl
.
text
())
.
toMatch
(
new
RegExp
(
options
.
currentPage
+
'
\\
s+out of
\\
s+
\
/
\\
s+'
+
topicCollection
.
totalPages
));
expect
(
footerEl
.
hasClass
(
'hidden'
)).
toBe
(
options
.
isHidden
);
}
it
(
'can render the first of many pages'
,
function
()
{
expectHeader
(
'Showing 1-5 out of 6 total'
);
expectTopics
(
initialTopics
);
expectFooter
({
currentPage
:
1
,
totalPages
:
2
,
isHidden
:
false
});
});
it
(
'can render the only page'
,
function
()
{
initialTopics
=
generateTopics
(
1
,
1
);
topicCollection
.
set
(
{
"count"
:
1
,
"num_pages"
:
1
,
"current_page"
:
1
,
"start"
:
0
,
"results"
:
initialTopics
},
{
parse
:
true
}
);
expectHeader
(
'Showing 1 out of 1 total'
);
expectTopics
(
initialTopics
);
expectFooter
({
currentPage
:
1
,
totalPages
:
1
,
isHidden
:
true
});
});
it
(
'can change to the next page'
,
function
()
{
var
requests
=
AjaxHelpers
.
requests
(
this
),
newTopics
=
generateTopics
(
1
,
1
);
expectHeader
(
'Showing 1-5 out of 6 total'
);
expectTopics
(
initialTopics
);
expectFooter
({
currentPage
:
1
,
totalPages
:
2
,
isHidden
:
false
});
expect
(
requests
.
length
).
toBe
(
0
);
topicsView
.
$
(
nextPageButtonCss
).
click
();
expect
(
requests
.
length
).
toBe
(
1
);
AjaxHelpers
.
respondWithJson
(
requests
,
{
"count"
:
6
,
"num_pages"
:
2
,
"current_page"
:
2
,
"start"
:
5
,
"results"
:
newTopics
});
expectHeader
(
'Showing 6-6 out of 6 total'
);
expectTopics
(
newTopics
);
expectFooter
({
currentPage
:
2
,
totalPages
:
2
,
isHidden
:
false
});
});
it
(
'can change to the previous page'
,
function
()
{
var
requests
=
AjaxHelpers
.
requests
(
this
),
previousPageTopics
;
initialTopics
=
generateTopics
(
1
,
1
);
topicCollection
.
set
(
{
"count"
:
6
,
"num_pages"
:
2
,
"current_page"
:
2
,
"start"
:
5
,
"results"
:
initialTopics
},
{
parse
:
true
}
);
expectHeader
(
'Showing 6-6 out of 6 total'
);
expectTopics
(
initialTopics
);
expectFooter
({
currentPage
:
2
,
totalPages
:
2
,
isHidden
:
false
});
topicsView
.
$
(
'.previous-page-link'
).
click
();
previousPageTopics
=
generateTopics
(
1
,
5
);
AjaxHelpers
.
respondWithJson
(
requests
,
{
"count"
:
6
,
"num_pages"
:
2
,
"current_page"
:
1
,
"start"
:
0
,
"results"
:
previousPageTopics
});
expectHeader
(
'Showing 1-5 out of 6 total'
);
expectTopics
(
previousPageTopics
);
expectFooter
({
currentPage
:
1
,
totalPages
:
2
,
isHidden
:
false
});
});
it
(
'sets focus for screen readers'
,
function
()
{
var
requests
=
AjaxHelpers
.
requests
(
this
);
spyOn
(
$
.
fn
,
'focus'
);
topicsView
.
$
(
nextPageButtonCss
).
click
();
AjaxHelpers
.
respondWithJson
(
requests
,
{
"count"
:
6
,
"num_pages"
:
2
,
"current_page"
:
2
,
"start"
:
5
,
"results"
:
generateTopics
(
1
,
1
)
});
expect
(
topicsView
.
$
(
'.sr-is-focusable'
).
focus
).
toHaveBeenCalled
();
});
it
(
'does not change on server error'
,
function
()
{
var
requests
=
AjaxHelpers
.
requests
(
this
),
expectInitialState
=
function
()
{
expectHeader
(
'Showing 1-5 out of 6 total'
);
expectTopics
(
initialTopics
);
expectFooter
({
currentPage
:
1
,
totalPages
:
2
,
isHidden
:
false
});
};
expectInitialState
();
topicsView
.
$
(
nextPageButtonCss
).
click
();
requests
[
0
].
respond
(
500
);
expectInitialState
();
expect
(
footerEl
.
text
()).
toMatch
(
'1
\\
s+out of
\\
s+
\
/
\\
s+2'
);
expect
(
footerEl
).
not
.
toHaveClass
(
'hidden'
);
});
});
});
lms/djangoapps/teams/static/teams/js/teams_tab_factory.js
View file @
d61d193d
;(
function
(
define
)
{
'use strict'
;
define
([
'jquery'
,
'teams/js/views/teams_tab'
,
'teams/js/collections/topic'
],
function
(
$
,
TeamsTabView
,
TopicCollection
)
{
return
function
(
element
,
topics
,
topics_url
,
course_id
)
{
var
topicCollection
=
new
TopicCollection
(
topics
,
{
url
:
topics_url
,
course_id
:
course_id
,
parse
:
true
});
topicCollection
.
bootstrap
();
var
view
=
new
TeamsTabView
({
el
:
element
,
topicCollection
:
topicCollection
});
view
.
render
();
define
([
'jquery'
,
'teams/js/views/teams_tab'
],
function
(
$
,
TeamsTabView
)
{
return
function
(
options
)
{
var
teamsTab
=
new
TeamsTabView
(
_
.
extend
(
options
,
{
el
:
$
(
'.teams-content'
)}));
teamsTab
.
render
();
Backbone
.
history
.
start
();
};
});
}).
call
(
this
,
define
||
RequireJS
.
define
);
lms/djangoapps/teams/static/teams/js/views/team_card.js
0 → 100644
View file @
d61d193d
;(
function
(
define
)
{
'use strict'
;
define
([
'backbone'
,
'underscore'
,
'gettext'
,
'js/components/card/views/card'
,
'text!teams/templates/team-country-language.underscore'
],
function
(
Backbone
,
_
,
gettext
,
CardView
,
teamCountryLanguageTemplate
)
{
var
TeamMembershipView
,
TeamCountryLanguageView
,
TeamCardView
;
TeamMembershipView
=
Backbone
.
View
.
extend
({
tagName
:
'div'
,
className
:
'team-members'
,
template
:
_
.
template
(
'<span class="member-count"><%= membership_message %></span>'
+
'<ul class="list-member-thumbs"></ul>'
),
initialize
:
function
(
options
)
{
this
.
maxTeamSize
=
options
.
maxTeamSize
;
},
render
:
function
()
{
var
memberships
=
this
.
model
.
get
(
'membership'
),
maxMemberCount
=
this
.
maxTeamSize
;
this
.
$el
.
html
(
this
.
template
({
membership_message
:
interpolate
(
// Translators: The following message displays the number of members on a team.
ngettext
(
'%(member_count)s / %(max_member_count)s Member'
,
'%(member_count)s / %(max_member_count)s Members'
,
maxMemberCount
),
{
member_count
:
memberships
.
length
,
max_member_count
:
maxMemberCount
},
true
)
}));
_
.
each
(
memberships
,
function
(
membership
)
{
this
.
$
(
'list-member-thumbs'
).
append
(
'<li class="item-member-thumb"><img alt="'
+
membership
.
user
.
username
+
'" src=""></img></li>'
);
},
this
);
return
this
;
}
});
TeamCountryLanguageView
=
Backbone
.
View
.
extend
({
template
:
_
.
template
(
teamCountryLanguageTemplate
),
render
:
function
()
{
// this.$el should be the card meta div
this
.
$el
.
append
(
this
.
template
({
country
:
this
.
model
.
get
(
'country'
),
language
:
this
.
model
.
get
(
'language'
)
}));
}
});
TeamCardView
=
CardView
.
extend
({
initialize
:
function
()
{
CardView
.
prototype
.
initialize
.
apply
(
this
,
arguments
);
// TODO: show last activity detail view
this
.
detailViews
=
[
new
TeamMembershipView
({
model
:
this
.
model
,
maxTeamSize
:
this
.
maxTeamSize
}),
new
TeamCountryLanguageView
({
model
:
this
.
model
})
];
},
configuration
:
'list_card'
,
cardClass
:
'team-card'
,
title
:
function
()
{
return
this
.
model
.
get
(
'name'
);
},
description
:
function
()
{
return
this
.
model
.
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>'
},
true
);
}
});
return
TeamCardView
;
});
}).
call
(
this
,
define
||
RequireJS
.
define
);
lms/djangoapps/teams/static/teams/js/views/teams.js
0 → 100644
View file @
d61d193d
;(
function
(
define
)
{
'use strict'
;
define
([
'teams/js/views/team_card'
,
'common/js/components/views/paginated_view'
],
function
(
TeamCardView
,
PaginatedView
)
{
var
TeamsView
=
PaginatedView
.
extend
({
type
:
'teams'
,
events
:
{
'click button.action'
:
''
// entry point for team creation
},
initialize
:
function
(
options
)
{
this
.
itemViewClass
=
TeamCardView
.
extend
({
router
:
options
.
router
,
maxTeamSize
:
options
.
maxTeamSize
});
PaginatedView
.
prototype
.
initialize
.
call
(
this
);
},
render
:
function
()
{
PaginatedView
.
prototype
.
render
.
call
(
this
);
this
.
$el
.
append
(
$
(
'<button class="action action-primary">'
+
gettext
(
'Create new team'
)
+
'</button>'
)
);
return
this
;
}
});
return
TeamsView
;
});
}).
call
(
this
,
define
||
RequireJS
.
define
);
lms/djangoapps/teams/static/teams/js/views/teams_tab.js
View file @
d61d193d
This diff is collapsed.
Click to expand it.
lms/djangoapps/teams/static/teams/js/views/topic_card.js
View file @
d61d193d
...
...
@@ -32,7 +32,7 @@
action
:
function
(
event
)
{
event
.
preventDefault
();
// TODO implement actual navigation
this
.
router
.
navigate
(
'topics/'
+
this
.
model
.
get
(
'id'
),
{
trigger
:
true
});
},
configuration
:
'square_card'
,
...
...
lms/djangoapps/teams/static/teams/js/views/topics.js
View file @
d61d193d
;(
function
(
define
)
{
'use strict'
;
define
([
'backbone'
,
'underscore'
,
'gettext'
,
'common/js/components/views/list'
,
'common/js/components/views/paging_header'
,
'common/js/components/views/paging_footer'
,
'teams/js/views/topic_card'
,
'text!teams/templates/topics.underscore'
],
function
(
Backbone
,
_
,
gettext
,
ListView
,
PagingHeader
,
PagingFooterView
,
TopicCardView
,
topics_template
)
{
var
TopicsListView
=
ListView
.
extend
({
tagName
:
'div'
,
className
:
'topics-container'
,
itemViewClass
:
TopicCardView
});
var
TopicsView
=
Backbone
.
View
.
extend
({
initialize
:
function
()
{
this
.
listView
=
new
TopicsListView
({
collection
:
this
.
collection
});
this
.
headerView
=
new
PagingHeader
({
collection
:
this
.
collection
});
this
.
pagingFooterView
=
new
PagingFooterView
({
collection
:
this
.
collection
,
hideWhenOnePage
:
true
});
// Focus top of view for screen readers
this
.
collection
.
on
(
'page_changed'
,
function
()
{
this
.
$
(
'.sr-is-focusable.sr-topics-view'
).
focus
();
},
this
);
},
render
:
function
()
{
this
.
$el
.
html
(
_
.
template
(
topics_template
));
this
.
assign
(
this
.
listView
,
'.topics-list'
);
this
.
assign
(
this
.
headerView
,
'.topics-paging-header'
);
this
.
assign
(
this
.
pagingFooterView
,
'.topics-paging-footer'
);
return
this
;
},
'common/js/components/views/paginated_view'
],
function
(
TopicCardView
,
PaginatedView
)
{
var
TopicsView
=
PaginatedView
.
extend
({
type
:
'topics'
,
/**
* Helper method to render subviews and re-bind events.
*
* Borrowed from http://ianstormtaylor.com/rendering-views-in-backbonejs-isnt-always-simple/
*
* @param view The Backbone view to render
* @param selector The string CSS selector which the view should attach to
*/
assign
:
function
(
view
,
selector
)
{
view
.
setElement
(
this
.
$
(
selector
)).
render
();
initialize
:
function
(
options
)
{
this
.
itemViewClass
=
TopicCardView
.
extend
({
router
:
options
.
router
});
PaginatedView
.
prototype
.
initialize
.
call
(
this
);
}
});
return
TopicsView
;
...
...
lms/djangoapps/teams/static/teams/templates/team-country-language.underscore
0 → 100644
View file @
d61d193d
<% if (country) { print('<p class="meta-detail team-location"><span class="icon fa-globe"></span>' + country + '</p>'); } %>
<% if (language) { print('<p class="meta-detail team-language"><span class="icon fa-chat"></span>' + language + '</p>'); } %>
lms/djangoapps/teams/static/teams/templates/teams.underscore
0 → 100644
View file @
d61d193d
<div class="sr-is-focusable sr-teams-view" tabindex="-1"></div>
<div class="teams-paging-header"></div>
<div class="teams-list"></div>
<div class="teams-paging-footer"></div>
lms/djangoapps/teams/static/teams/templates/teams_tab.underscore
0 → 100644
View file @
d61d193d
<div class="wrapper-msg is-incontext urgency-low warning is-hidden">
<div class="msg">
<div class="msg-content">
<div class="copy">
</div>
</div>
</div>
</div>
<div class="teams-header"></div>
<div class="teams-main">
<div class="page-content"></div>
</div>
lms/djangoapps/teams/templates/teams/teams.html
View file @
d61d193d
...
...
@@ -22,6 +22,13 @@
<
%
block
name=
"js_extra"
>
<
%
static:require_module
module_name=
"teams/js/teams_tab_factory"
class_name=
"TeamsTabFactory"
>
TeamsTabFactory($('.teams-content'), ${ json.dumps(topics, cls=EscapedEdxJSONEncoder) }, '${ topics_url }', '${ unicode(course.id) }');
new TeamsTabFactory({
topics: ${ json.dumps(topics, cls=EscapedEdxJSONEncoder) },
topic_url: '${ topic_url }',
topics_url: '${ topics_url }',
teams_url: '${ teams_url }',
maxTeamSize: ${ course.teams_max_size },
course_id: '${ unicode(course.id) }'
});
</
%
static:require
_module
>
</
%
block>
lms/djangoapps/teams/tests/test_serializers.py
0 → 100644
View file @
d61d193d
# -*- coding: utf-8 -*-
"""
Tests for custom Teams Serializers.
"""
from
django.core.paginator
import
Paginator
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
lms.djangoapps.teams.tests.factories
import
CourseTeamFactory
from
lms.djangoapps.teams.serializers
import
BaseTopicSerializer
,
PaginatedTopicSerializer
,
TopicSerializer
class
TopicTestCase
(
ModuleStoreTestCase
):
"""
Base test class to set up a course with topics
"""
def
setUp
(
self
):
"""
Set up a course with a teams configuration.
"""
super
(
TopicTestCase
,
self
)
.
setUp
()
self
.
course
=
CourseFactory
.
create
(
teams_configuration
=
{
"max_team_size"
:
10
,
"topics"
:
[{
u'name'
:
u'Tøpic'
,
u'description'
:
u'The bést topic!'
,
u'id'
:
u'0'
}]
}
)
class
BaseTopicSerializerTestCase
(
TopicTestCase
):
"""
Tests for the `BaseTopicSerializer`, which should not serialize team count
data.
"""
def
test_team_count_not_included
(
self
):
"""Verifies that the `BaseTopicSerializer` does not include team count"""
with
self
.
assertNumQueries
(
0
):
serializer
=
BaseTopicSerializer
(
self
.
course
.
teams_topics
[
0
])
self
.
assertEqual
(
serializer
.
data
,
{
u'name'
:
u'Tøpic'
,
u'description'
:
u'The bést topic!'
,
u'id'
:
u'0'
}
)
class
TopicSerializerTestCase
(
TopicTestCase
):
"""
Tests for the `TopicSerializer`, which should serialize team count data for
a single topic.
"""
def
test_topic_with_no_team_count
(
self
):
"""
Verifies that the `TopicSerializer` correctly displays a topic with a
team count of 0, and that it only takes one SQL query.
"""
with
self
.
assertNumQueries
(
1
):
serializer
=
TopicSerializer
(
self
.
course
.
teams_topics
[
0
])
self
.
assertEqual
(
serializer
.
data
,
{
u'name'
:
u'Tøpic'
,
u'description'
:
u'The bést topic!'
,
u'id'
:
u'0'
,
u'team_count'
:
0
}
)
def
test_topic_with_team_count
(
self
):
"""
Verifies that the `TopicSerializer` correctly displays a topic with a
positive team count, and that it only takes one SQL query.
"""
CourseTeamFactory
.
create
(
course_id
=
self
.
course
.
id
,
topic_id
=
self
.
course
.
teams_topics
[
0
][
'id'
])
with
self
.
assertNumQueries
(
1
):
serializer
=
TopicSerializer
(
self
.
course
.
teams_topics
[
0
])
self
.
assertEqual
(
serializer
.
data
,
{
u'name'
:
u'Tøpic'
,
u'description'
:
u'The bést topic!'
,
u'id'
:
u'0'
,
u'team_count'
:
1
}
)
class
PaginatedTopicSerializerTestCase
(
TopicTestCase
):
"""
Tests for the `PaginatedTopicSerializer`, which should serialize team count
data for many topics with constant time SQL queries.
"""
PAGE_SIZE
=
5
def
_merge_dicts
(
self
,
first
,
second
):
"""Convenience method to merge two dicts in a single expression"""
result
=
first
.
copy
()
result
.
update
(
second
)
return
result
def
setup_topics
(
self
,
num_topics
=
5
,
teams_per_topic
=
0
):
"""
Helper method to set up topics on the course. Returns a list of
created topics.
"""
self
.
course
.
teams_configuration
[
'topics'
]
=
[]
topics
=
[
{
u'name'
:
u'Tøpic {}'
.
format
(
i
),
u'description'
:
u'The bést topic! {}'
.
format
(
i
),
u'id'
:
unicode
(
i
)}
for
i
in
xrange
(
num_topics
)
]
for
i
in
xrange
(
num_topics
):
topic_id
=
unicode
(
i
)
self
.
course
.
teams_configuration
[
'topics'
]
.
append
(
topics
[
i
])
for
_
in
xrange
(
teams_per_topic
):
CourseTeamFactory
.
create
(
course_id
=
self
.
course
.
id
,
topic_id
=
topic_id
)
return
topics
def
assert_serializer_output
(
self
,
topics
,
num_teams_per_topic
,
num_queries
):
"""
Verify that the serializer produced the expected topics.
"""
with
self
.
assertNumQueries
(
num_queries
):
page
=
Paginator
(
self
.
course
.
teams_topics
,
self
.
PAGE_SIZE
)
.
page
(
1
)
serializer
=
PaginatedTopicSerializer
(
instance
=
page
)
self
.
assertEqual
(
serializer
.
data
[
'results'
],
[
self
.
_merge_dicts
(
topic
,
{
u'team_count'
:
num_teams_per_topic
})
for
topic
in
topics
]
)
def
test_no_topics
(
self
):
"""
Verify that we return no results and make no SQL queries for a page
with no topics.
"""
self
.
course
.
teams_configuration
[
'topics'
]
=
[]
self
.
assert_serializer_output
([],
num_teams_per_topic
=
0
,
num_queries
=
0
)
def
test_topics_with_no_team_counts
(
self
):
"""
Verify that we serialize topics with no team count, making only one SQL
query.
"""
topics
=
self
.
setup_topics
(
teams_per_topic
=
0
)
self
.
assert_serializer_output
(
topics
,
num_teams_per_topic
=
0
,
num_queries
=
1
)
def
test_topics_with_team_counts
(
self
):
"""
Verify that we serialize topics with a positive team count, making only
one SQL query.
"""
teams_per_topic
=
10
topics
=
self
.
setup_topics
(
teams_per_topic
=
teams_per_topic
)
self
.
assert_serializer_output
(
topics
,
num_teams_per_topic
=
teams_per_topic
,
num_queries
=
1
)
def
test_subset_of_topics
(
self
):
"""
Verify that we serialize a subset of the course's topics, making only
one SQL query.
"""
teams_per_topic
=
10
topics
=
self
.
setup_topics
(
num_topics
=
self
.
PAGE_SIZE
+
1
,
teams_per_topic
=
teams_per_topic
)
self
.
assert_serializer_output
(
topics
[:
self
.
PAGE_SIZE
],
num_teams_per_topic
=
teams_per_topic
,
num_queries
=
1
)
lms/djangoapps/teams/tests/test_views.py
View file @
d61d193d
...
...
@@ -6,6 +6,7 @@ import json
import
ddt
from
django.core.urlresolvers
import
reverse
from
django.conf
import
settings
from
nose.plugins.attrib
import
attr
from
rest_framework.test
import
APITestCase
,
APIClient
...
...
@@ -35,13 +36,16 @@ class TestDashboard(ModuleStoreTestCase):
self
.
teams_url
=
reverse
(
'teams_dashboard'
,
args
=
[
self
.
course
.
id
])
def
test_anonymous
(
self
):
""" Verifies that an anonymous client cannot access the team dashboard. """
"""Verifies that an anonymous client cannot access the team
dashboard, and is redirected to the login page."""
anonymous_client
=
APIClient
()
response
=
anonymous_client
.
get
(
self
.
teams_url
)
self
.
assertEqual
(
404
,
response
.
status_code
)
redirect_url
=
'{0}?next={1}'
.
format
(
settings
.
LOGIN_URL
,
self
.
teams_url
)
self
.
assertRedirects
(
response
,
redirect_url
)
def
test_not_enrolled_not_staff
(
self
):
""" Verifies that a student who is not enrolled cannot access the team dashboard. """
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
response
=
self
.
client
.
get
(
self
.
teams_url
)
self
.
assertEqual
(
404
,
response
.
status_code
)
...
...
@@ -82,6 +86,8 @@ class TestDashboard(ModuleStoreTestCase):
"""
bad_org
=
"badorgxxx"
bad_team_url
=
self
.
teams_url
.
replace
(
self
.
course
.
id
.
org
,
bad_org
)
CourseEnrollmentFactory
.
create
(
user
=
self
.
user
,
course_id
=
self
.
course
.
id
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
response
=
self
.
client
.
get
(
bad_team_url
)
self
.
assertEqual
(
404
,
response
.
status_code
)
...
...
@@ -134,12 +140,12 @@ class TeamAPITestCase(APITestCase, ModuleStoreTestCase):
self
.
test_team_1
=
CourseTeamFactory
.
create
(
name
=
u'sólar team'
,
course_id
=
self
.
test_course_1
.
id
,
topic_id
=
'
renewable
'
topic_id
=
'
topic_0
'
)
self
.
test_team_2
=
CourseTeamFactory
.
create
(
name
=
'Wind Team'
,
course_id
=
self
.
test_course_1
.
id
)
self
.
test_team_3
=
CourseTeamFactory
.
create
(
name
=
'Nuclear Team'
,
course_id
=
self
.
test_course_1
.
id
)
self
.
test_team_4
=
CourseTeamFactory
.
create
(
name
=
'Coal Team'
,
course_id
=
self
.
test_course_1
.
id
,
is_active
=
False
)
self
.
test_team_
4
=
CourseTeamFactory
.
create
(
name
=
'Another Team'
,
course_id
=
self
.
test_course_2
.
id
)
self
.
test_team_
5
=
CourseTeamFactory
.
create
(
name
=
'Another Team'
,
course_id
=
self
.
test_course_2
.
id
)
for
user
,
course
in
[
(
'student_enrolled'
,
self
.
test_course_1
),
...
...
@@ -153,7 +159,7 @@ class TeamAPITestCase(APITestCase, ModuleStoreTestCase):
self
.
test_team_1
.
add_user
(
self
.
users
[
'student_enrolled'
])
self
.
test_team_3
.
add_user
(
self
.
users
[
'student_enrolled_both_courses_other_team'
])
self
.
test_team_
4
.
add_user
(
self
.
users
[
'student_enrolled_both_courses_other_team'
])
self
.
test_team_
5
.
add_user
(
self
.
users
[
'student_enrolled_both_courses_other_team'
])
def
login
(
self
,
user
):
"""Given a user string, logs the given user in.
...
...
@@ -312,7 +318,7 @@ class TestListTeamsAPI(TeamAPITestCase):
self
.
verify_names
({
'course_id'
:
self
.
test_course_2
.
id
},
200
,
[
'Another Team'
],
user
=
'staff'
)
def
test_filter_topic_id
(
self
):
self
.
verify_names
({
'course_id'
:
self
.
test_course_1
.
id
,
'topic_id'
:
'
renewable
'
},
200
,
[
u'sólar team'
])
self
.
verify_names
({
'course_id'
:
self
.
test_course_1
.
id
,
'topic_id'
:
'
topic_0
'
},
200
,
[
u'sólar team'
])
def
test_filter_include_inactive
(
self
):
self
.
verify_names
({
'include_inactive'
:
True
},
200
,
[
'Coal Team'
,
'Nuclear Team'
,
u'sólar team'
,
'Wind Team'
])
...
...
@@ -333,9 +339,10 @@ class TestListTeamsAPI(TeamAPITestCase):
data
=
{
'order_by'
:
field
}
if
field
else
{}
self
.
verify_names
(
data
,
status
,
names
)
@ddt.data
({
'course_id'
:
'no/such/course'
},
{
'topic_id'
:
'no_such_topic'
})
def
test_no_results
(
self
,
data
):
self
.
get_teams_list
(
404
,
data
)
@ddt.data
((
404
,
{
'course_id'
:
'no/such/course'
}),
(
400
,
{
'topic_id'
:
'no_such_topic'
}))
@ddt.unpack
def
test_no_results
(
self
,
status
,
data
):
self
.
get_teams_list
(
status
,
data
)
def
test_page_size
(
self
):
result
=
self
.
get_teams_list
(
200
,
{
'page_size'
:
2
})
...
...
@@ -348,7 +355,7 @@ class TestListTeamsAPI(TeamAPITestCase):
self
.
assertIsNotNone
(
result
[
'previous'
])
def
test_expand_user
(
self
):
result
=
self
.
get_teams_list
(
200
,
{
'expand'
:
'user'
,
'topic_id'
:
'
renewable
'
})
result
=
self
.
get_teams_list
(
200
,
{
'expand'
:
'user'
,
'topic_id'
:
'
topic_0
'
})
self
.
verify_expanded_user
(
result
[
'results'
][
0
][
'membership'
][
0
][
'user'
])
...
...
@@ -561,6 +568,16 @@ class TestListTopicsAPI(TeamAPITestCase):
response
=
self
.
get_topics_list
(
data
=
{
'course_id'
:
self
.
test_course_1
.
id
})
self
.
assertEqual
(
response
[
'sort_order'
],
'name'
)
def
test_team_count
(
self
):
"""Test that team_count is included for each topic"""
response
=
self
.
get_topics_list
(
data
=
{
'course_id'
:
self
.
test_course_1
.
id
})
for
topic
in
response
[
'results'
]:
self
.
assertIn
(
'team_count'
,
topic
)
if
topic
[
'id'
]
==
u'topic_0'
:
self
.
assertEqual
(
topic
[
'team_count'
],
1
)
else
:
self
.
assertEqual
(
topic
[
'team_count'
],
0
)
@ddt.ddt
class
TestDetailTopicAPI
(
TeamAPITestCase
):
...
...
@@ -588,6 +605,13 @@ class TestDetailTopicAPI(TeamAPITestCase):
def
test_invalid_topic_id
(
self
):
self
.
get_topic_detail
(
'no_such_topic'
,
self
.
test_course_1
.
id
,
404
)
def
test_team_count
(
self
):
"""Test that team_count is included with a topic"""
topic
=
self
.
get_topic_detail
(
topic_id
=
'topic_0'
,
course_id
=
self
.
test_course_1
.
id
)
self
.
assertEqual
(
topic
[
'team_count'
],
1
)
topic
=
self
.
get_topic_detail
(
topic_id
=
'topic_1'
,
course_id
=
self
.
test_course_1
.
id
)
self
.
assertEqual
(
topic
[
'team_count'
],
0
)
@ddt.ddt
class
TestListMembershipAPI
(
TeamAPITestCase
):
...
...
lms/djangoapps/teams/urls.py
View file @
d61d193d
"""Defines the URL routes for this app."""
from
django.conf.urls
import
patterns
,
url
from
django.contrib.auth.decorators
import
login_required
from
.views
import
TeamsDashboardView
urlpatterns
=
patterns
(
'teams.views'
,
url
(
r"^/$"
,
TeamsDashboardView
.
as_view
(
),
name
=
"teams_dashboard"
)
url
(
r"^/$"
,
login_required
(
TeamsDashboardView
.
as_view
()
),
name
=
"teams_dashboard"
)
)
lms/djangoapps/teams/views.py
View file @
d61d193d
...
...
@@ -42,7 +42,14 @@ from opaque_keys import InvalidKeyError
from
opaque_keys.edx.keys
import
CourseKey
from
.models
import
CourseTeam
,
CourseTeamMembership
from
.serializers
import
CourseTeamSerializer
,
CourseTeamCreationSerializer
,
TopicSerializer
,
MembershipSerializer
from
.serializers
import
(
CourseTeamSerializer
,
CourseTeamCreationSerializer
,
BaseTopicSerializer
,
TopicSerializer
,
PaginatedTopicSerializer
,
MembershipSerializer
)
from
.errors
import
AlreadyOnTeamInCourse
,
NotEnrolledInCourseForTeam
...
...
@@ -75,9 +82,15 @@ class TeamsDashboardView(View):
sort_order
=
'name'
topics
=
get_ordered_topics
(
course
,
sort_order
)
topics_page
=
Paginator
(
topics
,
TOPICS_PER_PAGE
)
.
page
(
1
)
topics_serializer
=
Paginat
ion
Serializer
(
instance
=
topics_page
,
context
=
{
'sort_order'
:
sort_order
})
topics_serializer
=
Paginat
edTopic
Serializer
(
instance
=
topics_page
,
context
=
{
'sort_order'
:
sort_order
})
context
=
{
"course"
:
course
,
"topics"
:
topics_serializer
.
data
,
"topics_url"
:
reverse
(
'topics_list'
,
request
=
request
)
"course"
:
course
,
"topics"
:
topics_serializer
.
data
,
"topic_url"
:
reverse
(
'topics_detail'
,
kwargs
=
{
'topic_id'
:
'topic_id'
,
'course_id'
:
str
(
course_id
)},
request
=
request
),
"topics_url"
:
reverse
(
'topics_list'
,
request
=
request
),
"teams_url"
:
reverse
(
'teams_list'
,
request
=
request
)
}
return
render_to_response
(
"teams/teams.html"
,
context
)
...
...
@@ -248,7 +261,8 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView):
try
:
course_key
=
CourseKey
.
from_string
(
course_id_string
)
# Ensure the course exists
if
not
modulestore
()
.
has_course
(
course_key
):
course_module
=
modulestore
()
.
get_course
(
course_key
)
if
course_module
is
None
:
return
Response
(
status
=
status
.
HTTP_404_NOT_FOUND
)
result_filter
.
update
({
'course_id'
:
course_key
})
except
InvalidKeyError
:
...
...
@@ -267,6 +281,13 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView):
)
if
'topic_id'
in
request
.
QUERY_PARAMS
:
topic_id
=
request
.
QUERY_PARAMS
[
'topic_id'
]
if
topic_id
not
in
[
topic
[
'id'
]
for
topic
in
course_module
.
teams_configuration
[
'topics'
]]:
error
=
build_api_error
(
ugettext_noop
(
'The supplied topic id {topic_id} is not valid'
),
topic_id
=
topic_id
)
return
Response
(
error
,
status
=
status
.
HTTP_400_BAD_REQUEST
)
result_filter
.
update
({
'topic_id'
:
request
.
QUERY_PARAMS
[
'topic_id'
]})
if
'include_inactive'
in
request
.
QUERY_PARAMS
and
request
.
QUERY_PARAMS
[
'include_inactive'
]
.
lower
()
==
'true'
:
del
result_filter
[
'is_active'
]
...
...
@@ -290,14 +311,17 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView):
build_api_error
(
ugettext_noop
(
"last_activity is not yet supported"
)),
status
=
status
.
HTTP_400_BAD_REQUEST
)
else
:
return
Response
({
'developer_message'
:
"unsupported order_by value {}"
.
format
(
order_by_input
),
'user_message'
:
_
(
u"The ordering {} is not supported"
)
.
format
(
order_by_input
),
},
status
=
status
.
HTTP_400_BAD_REQUEST
)
queryset
=
queryset
.
order_by
(
order_by_field
)
if
not
queryset
:
return
Response
(
status
=
status
.
HTTP_404_NOT_FOUND
)
page
=
self
.
paginate_queryset
(
queryset
)
serializer
=
self
.
get_pagination_serializer
(
page
)
serializer
.
context
.
update
({
'sort_order'
:
order_by_input
})
# pylint: disable=maybe-no-member
return
Response
(
serializer
.
data
)
# pylint: disable=maybe-no-member
def
post
(
self
,
request
):
...
...
@@ -492,8 +516,8 @@ class TopicListView(GenericAPIView):
paginate_by
=
TOPICS_PER_PAGE
paginate_by_param
=
'page_size'
pagination_serializer_class
=
Paginat
ion
Serializer
serializer_class
=
TopicSerializer
pagination_serializer_class
=
Paginat
edTopic
Serializer
serializer_class
=
Base
TopicSerializer
def
get
(
self
,
request
):
"""GET /api/team/v0/topics/?course_id={course_id}"""
...
...
@@ -531,8 +555,7 @@ class TopicListView(GenericAPIView):
},
status
=
status
.
HTTP_400_BAD_REQUEST
)
page
=
self
.
paginate_queryset
(
topics
)
serializer
=
self
.
get_pagination_serializer
(
page
)
serializer
.
context
=
{
'sort_order'
:
ordering
}
serializer
=
self
.
pagination_serializer_class
(
page
,
context
=
{
'sort_order'
:
ordering
})
return
Response
(
serializer
.
data
)
# pylint: disable=maybe-no-member
...
...
lms/static/js/components/card/views/card.js
View file @
d61d193d
...
...
@@ -81,7 +81,8 @@
description
:
description
,
action_class
:
this
.
callIfFunction
(
this
.
actionClass
),
action_url
:
this
.
callIfFunction
(
this
.
actionUrl
),
action_content
:
this
.
callIfFunction
(
this
.
actionContent
)
action_content
:
this
.
callIfFunction
(
this
.
actionContent
),
configuration
:
this
.
callIfFunction
(
this
.
configuration
)
}));
var
detailsEl
=
this
.
$el
.
find
(
'.card-meta'
);
_
.
each
(
this
.
callIfFunction
(
this
.
details
),
function
(
detail
)
{
...
...
lms/static/js/components/tabbed/views/tabbed_view.js
View file @
d61d193d
...
...
@@ -19,35 +19,59 @@
* following properties:
* view (Backbone.View): the view to render for this tab.
* title (string): The title to display for this tab.
* url (string): The URL fragment which will navigate to this tab.
* url (string): The URL fragment which will
* navigate to this tab when a router is
* provided.
* If a router is passed in (via options.router),
* use that router to keep track of history between
* tabs. Backbone.history.start() must be called
* by the router's instatiator after this view is
* initialized.
*/
initialize
:
function
(
options
)
{
this
.
router
=
new
Backbone
.
Router
();
this
.
$el
.
html
(
this
.
template
({}));
var
self
=
this
;
this
.
router
=
options
.
router
||
null
;
this
.
tabs
=
options
.
tabs
;
this
.
urlMap
=
_
.
reduce
(
this
.
tabs
,
function
(
map
,
value
)
{
map
[
value
.
url
]
=
value
;
return
map
;
},
{});
},
render
:
function
()
{
var
self
=
this
;
this
.
$el
.
html
(
this
.
template
({}));
_
.
each
(
this
.
tabs
,
function
(
tabInfo
,
index
)
{
var
tabEl
=
$
(
_
.
template
(
tabTemplate
,
{
index
:
index
,
title
:
tabInfo
.
title
title
:
tabInfo
.
title
,
url
:
tabInfo
.
url
}));
self
.
$
(
'.page-content-nav'
).
append
(
tabEl
);
self
.
router
.
route
(
tabInfo
.
url
,
function
()
{
self
.
setActiveTab
(
index
);
});
});
this
.
setActiveTab
(
0
);
if
(
Backbone
.
history
.
getHash
()
===
""
)
{
this
.
setActiveTab
(
0
);
}
return
this
;
},
setActiveTab
:
function
(
index
)
{
var
tab
=
this
.
tabs
[
index
],
view
=
tab
.
view
;
var
tab
,
tabEl
,
view
;
if
(
typeof
index
===
'string'
)
{
tab
=
this
.
urlMap
[
index
];
tabEl
=
this
.
$
(
'a[data-url='
+
index
+
']'
);
}
else
{
tab
=
this
.
tabs
[
index
];
tabEl
=
this
.
$
(
'a[data-index='
+
index
+
']'
);
}
view
=
tab
.
view
;
this
.
$
(
'a.is-active'
).
removeClass
(
'is-active'
).
attr
(
'aria-selected'
,
'false'
);
t
his
.
$
(
'a[data-index='
+
index
+
']'
)
.
addClass
(
'is-active'
).
attr
(
'aria-selected'
,
'true'
);
t
abEl
.
addClass
(
'is-active'
).
attr
(
'aria-selected'
,
'true'
);
view
.
setElement
(
this
.
$
(
'.page-content-main'
)).
render
();
this
.
$
(
'.sr-is-focusable.sr-tab'
).
focus
();
this
.
router
.
navigate
(
tab
.
url
,
{
replace
:
true
});
if
(
this
.
router
)
{
this
.
router
.
navigate
(
tab
.
url
,
{
replace
:
true
,
trigger
:
true
});
}
},
switchTab
:
function
(
event
)
{
...
...
lms/static/js/spec/components/card/card_spec.js
View file @
d61d193d
...
...
@@ -18,7 +18,7 @@
it
(
'can render itself as a list card'
,
function
()
{
var
view
=
new
CardView
({
configuration
:
'list_card'
});
expect
(
view
.
$el
).
toHaveClass
(
'list-card'
);
expect
(
view
.
$el
.
find
(
'.wrapper-card-
meta
.action'
).
length
).
toBe
(
1
);
expect
(
view
.
$el
.
find
(
'.wrapper-card-
core
.action'
).
length
).
toBe
(
1
);
});
it
(
'renders a pennant only if the pennant value is truthy'
,
function
()
{
...
...
lms/static/js/spec/components/tabbed/tabbed_view_spec.js
View file @
d61d193d
...
...
@@ -20,23 +20,15 @@
describe
(
'TabbedView component'
,
function
()
{
beforeEach
(
function
()
{
spyOn
(
Backbone
.
history
,
'navigate'
).
andCallThrough
();
Backbone
.
history
.
start
();
view
=
new
TabbedView
({
tabs
:
[{
url
:
'test 1'
,
title
:
'Test 1'
,
view
:
new
TestSubview
({
text
:
'this is test text'
})
},
{
url
:
'test 2'
,
title
:
'Test 2'
,
view
:
new
TestSubview
({
text
:
'other text'
})
}]
});
});
afterEach
(
function
()
{
Backbone
.
history
.
stop
();
}).
render
();
});
it
(
'can render itself'
,
function
()
{
...
...
@@ -59,12 +51,6 @@
expect
(
view
.
$el
.
text
()).
toContain
(
'other text'
);
});
it
(
'changes tabs on navigation'
,
function
()
{
expect
(
view
.
$
(
'.nav-item.is-active'
).
data
(
'index'
)).
toEqual
(
0
);
Backbone
.
history
.
navigate
(
'test 2'
,
{
trigger
:
true
});
expect
(
view
.
$
(
'.nav-item.is-active'
).
data
(
'index'
)).
toEqual
(
1
);
});
it
(
'marks the active tab as selected using aria attributes'
,
function
()
{
expect
(
view
.
$
(
'.nav-item[data-index=0]'
)).
toHaveAttr
(
'aria-selected'
,
'true'
);
expect
(
view
.
$
(
'.nav-item[data-index=1]'
)).
toHaveAttr
(
'aria-selected'
,
'false'
);
...
...
@@ -73,17 +59,59 @@
expect
(
view
.
$
(
'.nav-item[data-index=1]'
)).
toHaveAttr
(
'aria-selected'
,
'true'
);
});
it
(
'updates the page URL on tab switches without adding to browser history'
,
function
()
{
view
.
$
(
'.nav-item[data-index=1]'
).
click
();
expect
(
Backbone
.
history
.
navigate
).
toHaveBeenCalledWith
(
'test 2'
,
{
replace
:
true
});
});
it
(
'sets focus for screen readers'
,
function
()
{
spyOn
(
$
.
fn
,
'focus'
);
view
.
$
(
'.nav-item[data-index=1]'
).
click
();
expect
(
view
.
$
(
'.sr-is-focusable.sr-tab'
).
focus
).
toHaveBeenCalled
();
});
describe
(
'history'
,
function
()
{
beforeEach
(
function
()
{
spyOn
(
Backbone
.
history
,
'navigate'
).
andCallThrough
();
view
=
new
TabbedView
({
tabs
:
[{
url
:
'test 1'
,
title
:
'Test 1'
,
view
:
new
TestSubview
({
text
:
'this is test text'
})
},
{
url
:
'test 2'
,
title
:
'Test 2'
,
view
:
new
TestSubview
({
text
:
'other text'
})
}],
router
:
new
Backbone
.
Router
({
routes
:
{
'test 1'
:
function
()
{
view
.
setActiveTab
(
0
);
},
'test 2'
:
function
()
{
view
.
setActiveTab
(
1
);
}
}
})
}).
render
();
Backbone
.
history
.
start
();
});
afterEach
(
function
()
{
view
.
router
.
navigate
(
''
);
Backbone
.
history
.
stop
();
});
it
(
'updates the page URL on tab switches without adding to browser history'
,
function
()
{
view
.
$
(
'.nav-item[data-index=1]'
).
click
();
expect
(
Backbone
.
history
.
navigate
).
toHaveBeenCalledWith
(
'test 2'
,
{
replace
:
true
,
trigger
:
true
}
);
});
it
(
'changes tabs on URL navigation'
,
function
()
{
expect
(
view
.
$
(
'.nav-item.is-active'
).
data
(
'index'
)).
toEqual
(
0
);
Backbone
.
history
.
navigate
(
'test 2'
,
{
trigger
:
true
});
expect
(
view
.
$
(
'.nav-item.is-active'
).
data
(
'index'
)).
toEqual
(
1
);
});
});
});
}
);
});
}).
call
(
this
,
define
||
RequireJS
.
define
);
lms/static/js/spec/main.js
View file @
d61d193d
...
...
@@ -531,6 +531,8 @@
'lms/include/teams/js/spec/topic_card_spec.js'
,
'lms/include/teams/js/spec/topic_collection_spec.js'
,
'lms/include/teams/js/spec/topics_spec.js'
,
'lms/include/teams/js/spec/teams_spec.js'
,
'lms/include/teams/js/spec/teams_tab_spec.js'
,
'lms/include/js/spec/components/header/header_spec.js'
,
'lms/include/js/spec/components/tabbed/tabbed_view_spec.js'
,
'lms/include/js/spec/components/card/card_spec.js'
,
...
...
lms/static/sass/_developer.scss
View file @
d61d193d
...
...
@@ -76,9 +76,6 @@
// teams temporary
.view-teams
{
.global-new
,
#global-navigation
{
display
:
none
;
}
// Copied from _pagination.scss in cms
.pagination
{
...
...
lms/static/sass/base/_variables.scss
View file @
d61d193d
...
...
@@ -150,7 +150,7 @@ $yellow: rgb(255, 252, 221);
// ====================
// COLORS: old variables
// DEPRECATED: use colors in lists above
// DEPRECATED: use colors in lists above
$error-red
:
rgb
(
253
,
87
,
87
);
$danger-red
:
rgb
(
212
,
64
,
64
);
$light-gray
:
rgb
(
221
,
221
,
221
);
...
...
@@ -163,7 +163,7 @@ $light-gray: rgb(221,221,221); // #dddddd
// ====================
// used by descriptor css
// DEPRECATED: use colors in lists above
// DEPRECATED: use colors in lists above
$lightGrey
:
rgb
(
237
,
241
,
245
);
// #edf1f5
$darkGrey
:
rgb
(
136
,
145
,
161
);
// #8891a1
$lightGrey1
:
$gray-l3
;
...
...
@@ -355,7 +355,7 @@ $highlight-color: rgb(255,255,0);
// Notifications
$notify-banner-bg-1
:
rgb
(
56
,
56
,
56
);
$notify-banner-bg-2
:
rgb
(
136
,
136
,
136
);
$notify-banner-bg-3
:
rgb
(
223
,
223
,
223
)
;
$notify-banner-bg-3
:
$shadow-l2
;
$alert-color
:
rgb
(
212
,
64
,
64
);
//rich red
$warning-color
:
rgb
(
237
,
189
,
60
);
//rich yellow
...
...
lms/static/sass/elements/_system-feedback.scss
View file @
d61d193d
// lms - elements - system feedback
// ====================
// messages
//
pre-pattern library
messages
// UI : message
.wrapper-msg
{
...
...
@@ -111,6 +111,7 @@
&
.urgency-low
{
background
:
$notify-banner-bg-3
;
box-shadow
:
0
1px
2px
$shadow
;
.msg
{
color
:
$black
;
...
...
@@ -132,6 +133,16 @@
&
.success
{
border-top
:
3px
solid
$success-color
;
}
&
.is-incontext
{
margin
:
$baseline
;
.msg
{
max-width
:
unset
;
min-width
:
auto
;
}
}
}
...
...
lms/templates/components/card/card.underscore
View file @
d61d193d
<% if (configuration === 'square_card') { %>
<div class="wrapper-card-core">
<div class="card-core">
<% if (pennant) { %>
...
...
@@ -14,3 +15,21 @@
<a class="action <%= action_class %>" href="<%= action_url %>"><%= action_content %></a>
</div>
</div>
<% } else { %>
<div class="wrapper-card-core">
<div class="card-core">
<% if (pennant) { %>
<small class="card-type"><%- pennant %></small>
<% } %>
<h3 class="card-title"><%- title %></h3>
<p class="card-description"><%- description %></p>
</div>
<div class="card-actions">
<a class="action <%= action_class %>" href="<%= action_url %>"><%= action_content %></a>
</div>
</div>
<div class="wrapper-card-meta">
<div class="card-meta">
</div>
</div>
<% } %>
lms/templates/components/tabbed/tab.underscore
View file @
d61d193d
<a class="nav-item" href="" data-index="<%= index %>" role="tab" aria-selected="false"><%- title %></a>
<a class="nav-item" href="" data-
url="<%= url %>" data-
index="<%= index %>" role="tab" aria-selected="false"><%- title %></a>
lms/templates/components/tabbed/tabbed_view.underscore
View file @
d61d193d
<div class="page-content">
<nav class="page-content-nav" aria-label="Teams"></nav>
<div class="sr-is-focusable sr-tab" tabindex="-1"></div>
<div class="page-content-main"></div>
</div>
<nav class="page-content-nav" aria-label="Teams"></nav>
<div class="sr-is-focusable sr-tab" tabindex="-1"></div>
<div class="page-content-main"></div>
lms/templates/ux/reference/teams-base.html
View file @
d61d193d
...
...
@@ -151,6 +151,15 @@ Teams | Course name
</div>
</div>
</header>
<div
class=
"wrapper-msg is-incontext urgency-low warning"
>
<div
class=
"msg"
>
<div
class=
"msg-content"
>
<div
class=
"copy"
>
<p>
We couldn't find the team "blah".
</p>
</div>
</div>
</div>
</div>
<div
class=
"page-content"
>
<nav
class=
"page-content-nav"
aria-label=
"Team"
>
...
...
@@ -159,7 +168,6 @@ Teams | Course name
</nav>
<div
class=
"page-content-main"
>
<!-- may need a form with submit here -->
<div
class=
"listing-tools"
>
<span
class=
"listing-count"
>
1-10 of 24 topics
</span>
|
<span
class=
"field listing-sort"
>
...
...
lms/templates/ux/reference/teams-create.html
View file @
d61d193d
...
...
@@ -139,7 +139,16 @@ Create New Team | [Course name]
<p
class=
"page-description"
>
If you cannot find an existing team to join or would like to team up with a group of friends, create a new team.
</p>
</div>
</header>
<div
class=
"wrapper-msg is-incontext urgency-low warning"
>
<div
class=
"msg"
>
<div
class=
"msg-content"
>
<h3
class=
"title"
>
Oops!
</h3>
<div
class=
"copy"
>
<p>
We couldn't create your team because something needs to be fixed below.
</p>
</div>
</div>
</div>
</div>
<div
class=
"page-content"
>
<form
class=
"create-team"
>
...
...
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