Commit 7b77f816 by Andy Armstrong

Merge pull request #9472 from edx/andya/team-search-ui

Add searching to the teams view
parents 4de03b64 0145ae7b
......@@ -30,6 +30,8 @@
isZeroIndexed: false,
perPage: 10,
isStale: false,
sortField: '',
sortDirection: 'descending',
sortableFields: {},
......@@ -37,6 +39,8 @@
filterField: '',
filterableFields: {},
searchString: null,
paginator_core: {
type: 'GET',
dataType: 'json',
......@@ -51,9 +55,10 @@
},
server_api: {
'page': function () { return this.currentPage; },
'page_size': function () { return this.perPage; },
'sort_order': function () { return this.sortField; }
page: function () { return this.currentPage; },
page_size: function () { return this.perPage; },
text_search: function () { return this.searchString ? this.searchString : ''; },
sort_order: function () { return this.sortField; }
},
parse: function (response) {
......@@ -61,7 +66,11 @@
this.currentPage = response.current_page;
this.totalPages = response.num_pages;
this.start = response.start;
this.sortField = response.sort_order;
// Note: sort_order is not returned when performing a search
if (response.sort_order) {
this.sortField = response.sort_order;
}
return response.results;
},
......@@ -84,6 +93,7 @@
self = this;
return this.goTo(page - (this.isZeroIndexed ? 1 : 0), {reset: true}).then(
function () {
self.isStale = false;
self.trigger('page_changed');
},
function () {
......@@ -92,6 +102,24 @@
);
},
/**
* Refreshes the collection if it has been marked as stale.
* @returns {promise} Returns a promise representing the refresh.
*/
refresh: function() {
var deferred = $.Deferred();
if (this.isStale) {
this.setPage(1)
.done(function() {
deferred.resolve();
});
} else {
deferred.resolve();
}
return deferred.promise();
},
/**
* Returns true if the collection has a next page, false otherwise.
*/
......@@ -183,7 +211,7 @@
}
}
this.sortField = fieldName;
this.setPage(1);
this.isStale = true;
},
/**
......@@ -193,7 +221,7 @@
*/
setSortDirection: function (direction) {
this.sortDirection = direction;
this.setPage(1);
this.isStale = true;
},
/**
......@@ -203,7 +231,19 @@
*/
setFilterField: function (fieldName) {
this.filterField = fieldName;
this.setPage(1);
this.isStale = true;
},
/**
* Sets the string to use for a text search. If no string is specified then
* the search is cleared.
* @param searchString A string to search on, or null if no search is to be applied.
*/
setSearchString: function(searchString) {
if (searchString !== this.searchString) {
this.searchString = searchString;
this.isStale = true;
}
}
}, {
SortDirection: {
......
......@@ -43,10 +43,16 @@
return this;
},
/**
* Updates the collection's sort order, and fetches an updated set of
* results.
* @returns {*} A promise for the collection being updated
*/
sortCollection: function () {
var selected = this.$('#paging-header-select option:selected');
this.sortOrder = selected.attr('value');
this.collection.setSortField(this.sortOrder);
return this.collection.refresh();
}
});
return PagingHeader;
......
/**
* A search field that works in concert with a paginated collection. When the user
* performs a search, the collection's search string will be updated and then the
* collection will be refreshed to show the first page of results.
*/
;(function (define) {
'use strict';
define(['backbone', 'jquery', 'underscore', 'text!common/templates/components/search-field.underscore'],
function (Backbone, $, _, searchFieldTemplate) {
return Backbone.View.extend({
events: {
'submit .search-form': 'performSearch',
'blur .search-form': 'onFocusOut',
'keyup .search-field': 'refreshState',
'click .action-clear': 'clearSearch'
},
initialize: function(options) {
this.type = options.type;
this.label = options.label;
},
refreshState: function() {
var searchField = this.$('.search-field'),
clearButton = this.$('.action-clear'),
searchString = $.trim(searchField.val());
if (searchString) {
clearButton.removeClass('is-hidden');
} else {
clearButton.addClass('is-hidden');
}
},
render: function() {
this.$el.html(_.template(searchFieldTemplate, {
type: this.type,
searchString: this.collection.searchString,
searchLabel: this.label
}));
this.refreshState();
return this;
},
onFocusOut: function(event) {
// If the focus is going anywhere but the clear search
// button then treat it as a request to search.
if (!$(event.relatedTarget).hasClass('action-clear')) {
this.performSearch(event);
}
},
performSearch: function(event) {
var searchField = this.$('.search-field'),
searchString = $.trim(searchField.val());
event.preventDefault();
this.collection.setSearchString(searchString);
return this.collection.refresh();
},
clearSearch: function(event) {
event.preventDefault();
this.$('.search-field').val('');
this.collection.setSearchString('');
this.refreshState();
return this.collection.refresh();
}
});
});
}).call(this, define || RequireJS.define);
......@@ -10,11 +10,11 @@ define(['jquery',
'use strict';
describe('PagingCollection', function () {
var collection, requests, server, assertQueryParams;
server = {
var collection;
var server = {
isZeroIndexed: false,
count: 43,
respond: function () {
respond: function (requests) {
var params = (new URI(requests[requests.length - 1].url)).query(true),
page = parseInt(params['page'], 10),
page_size = parseInt(params['page_size'], 10),
......@@ -35,7 +35,7 @@ define(['jquery',
}
}
};
assertQueryParams = function (params) {
var assertQueryParams = function (requests, params) {
var urlParams = (new URI(requests[requests.length - 1].url)).query(true);
_.each(params, function (value, key) {
expect(urlParams[key]).toBe(value);
......@@ -45,7 +45,6 @@ define(['jquery',
beforeEach(function () {
collection = new PagingCollection();
collection.perPage = 10;
requests = AjaxHelpers.requests(this);
server.isZeroIndexed = false;
server.count = 43;
});
......@@ -69,10 +68,11 @@ define(['jquery',
});
it('can set the sort field', function () {
var requests = AjaxHelpers.requests(this);
collection.registerSortableField('test_field', 'Test Field');
collection.setSortField('test_field', false);
expect(requests.length).toBe(1);
assertQueryParams({'sort_order': 'test_field'});
collection.refresh();
assertQueryParams(requests, {'sort_order': 'test_field'});
expect(collection.sortField).toBe('test_field');
expect(collection.sortDisplayName()).toBe('Test Field');
});
......@@ -80,7 +80,7 @@ define(['jquery',
it('can set the filter field', function () {
collection.registerFilterableField('test_field', 'Test Field');
collection.setFilterField('test_field');
expect(requests.length).toBe(1);
collection.refresh();
// The default implementation does not send any query params for filtering
expect(collection.filterField).toBe('test_field');
expect(collection.filterDisplayName()).toBe('Test Field');
......@@ -88,11 +88,9 @@ define(['jquery',
it('can set the sort direction', function () {
collection.setSortDirection(PagingCollection.SortDirection.ASCENDING);
expect(requests.length).toBe(1);
// The default implementation does not send any query params for sort direction
expect(collection.sortDirection).toBe(PagingCollection.SortDirection.ASCENDING);
collection.setSortDirection(PagingCollection.SortDirection.DESCENDING);
expect(requests.length).toBe(2);
expect(collection.sortDirection).toBe(PagingCollection.SortDirection.DESCENDING);
});
......@@ -113,11 +111,12 @@ define(['jquery',
'queries with page, page_size, and sort_order parameters when zero indexed': [true, 2],
'queries with page, page_size, and sort_order parameters when one indexed': [false, 3],
}, function (isZeroIndexed, page) {
var requests = AjaxHelpers.requests(this);
collection.isZeroIndexed = isZeroIndexed;
collection.perPage = 5;
collection.sortField = 'test_field';
collection.setPage(3);
assertQueryParams({'page': page.toString(), 'page_size': '5', 'sort_order': 'test_field'});
assertQueryParams(requests, {'page': page.toString(), 'page_size': '5', 'sort_order': 'test_field'});
});
SpecHelpers.withConfiguration({
......@@ -129,27 +128,30 @@ define(['jquery',
}, function () {
describe('setPage', function() {
it('triggers a reset event when the page changes successfully', function () {
var resetTriggered = false;
var requests = AjaxHelpers.requests(this),
resetTriggered = false;
collection.on('reset', function () { resetTriggered = true; });
collection.setPage(3);
server.respond();
server.respond(requests);
expect(resetTriggered).toBe(true);
});
it('triggers an error event when the requested page is out of range', function () {
var errorTriggered = false;
var requests = AjaxHelpers.requests(this),
errorTriggered = false;
collection.on('error', function () { errorTriggered = true; });
collection.setPage(17);
server.respond();
server.respond(requests);
expect(errorTriggered).toBe(true);
});
it('triggers an error event if the server responds with a 500', function () {
var errorTriggered = false;
var requests = AjaxHelpers.requests(this),
errorTriggered = false;
collection.on('error', function () { errorTriggered = true; });
collection.setPage(2);
expect(collection.getPage()).toBe(2);
server.respond();
server.respond(requests);
collection.setPage(3);
AjaxHelpers.respondWithError(requests, 500, {}, requests.length - 1);
expect(errorTriggered).toBe(true);
......@@ -159,11 +161,12 @@ define(['jquery',
describe('getPage', function () {
it('returns the correct page', function () {
var requests = AjaxHelpers.requests(this);
collection.setPage(1);
server.respond();
server.respond(requests);
expect(collection.getPage()).toBe(1);
collection.setPage(3);
server.respond();
server.respond(requests);
expect(collection.getPage()).toBe(3);
});
});
......@@ -177,9 +180,10 @@ define(['jquery',
'returns false on the last page': [5, 43, false]
},
function (page, count, result) {
var requests = AjaxHelpers.requests(this);
server.count = count;
collection.setPage(page);
server.respond();
server.respond(requests);
expect(collection.hasNextPage()).toBe(result);
}
);
......@@ -194,9 +198,10 @@ define(['jquery',
'returns false on the first page': [1, 43, false]
},
function (page, count, result) {
var requests = AjaxHelpers.requests(this);
server.count = count;
collection.setPage(page);
server.respond();
server.respond(requests);
expect(collection.hasPreviousPage()).toBe(result);
}
);
......@@ -209,13 +214,14 @@ define(['jquery',
'silently fails on the last page': [5, 43, 5]
},
function (page, count, newPage) {
var requests = AjaxHelpers.requests(this);
server.count = count;
collection.setPage(page);
server.respond();
server.respond(requests);
expect(collection.getPage()).toBe(page);
collection.nextPage();
if (requests.length > 1) {
server.respond();
server.respond(requests);
}
expect(collection.getPage()).toBe(newPage);
}
......@@ -229,13 +235,14 @@ define(['jquery',
'silently fails on the first page': [1, 43, 1]
},
function (page, count, newPage) {
var requests = AjaxHelpers.requests(this);
server.count = count;
collection.setPage(page);
server.respond();
server.respond(requests);
expect(collection.getPage()).toBe(page);
collection.previousPage();
if (requests.length > 1) {
server.respond();
server.respond(requests);
}
expect(collection.getPage()).toBe(newPage);
}
......
define([
'underscore',
'common/js/components/views/search_field',
'common/js/components/collections/paging_collection',
'common/js/spec_helpers/ajax_helpers'
], function (_, SearchFieldView, PagingCollection, AjaxHelpers) {
'use strict';
describe('SearchFieldView', function () {
var searchFieldView,
mockUrl = '/api/mock_collection';
var newCollection = function (size, perPage) {
var pageSize = 5,
results = _.map(_.range(size), function (i) { return {foo: i}; });
var collection = new PagingCollection(
[],
{
url: mockUrl,
count: results.length,
num_pages: results.length / pageSize,
current_page: 1,
start: 0,
results: _.first(results, perPage)
},
{parse: true}
);
collection.start = 0;
collection.totalCount = results.length;
return collection;
};
var createSearchFieldView = function (options) {
options = _.extend(
{
type: 'test',
collection: newCollection(5, 4),
el: $('.test-search')
},
options || {}
);
return new SearchFieldView(options);
};
beforeEach(function() {
setFixtures('<section class="test-search"></section>');
});
it('correctly displays itself', function () {
searchFieldView = createSearchFieldView().render();
expect(searchFieldView.$('.search-field').val(), '');
expect(searchFieldView.$('.action-clear')).toHaveClass('is-hidden');
});
it('can display with an initial search string', function () {
searchFieldView = createSearchFieldView({
searchString: 'foo'
}).render();
expect(searchFieldView.$('.search-field').val(), 'foo');
});
it('refreshes the collection when performing a search', function () {
var requests = AjaxHelpers.requests(this);
searchFieldView = createSearchFieldView().render();
searchFieldView.$('.search-field').val('foo');
searchFieldView.$('.action-search').click();
AjaxHelpers.expectRequestURL(requests, mockUrl, {
page: '1',
page_size: '10',
sort_order: '',
text_search: 'foo'
});
AjaxHelpers.respondWithJson(requests, {
count: 10,
current_page: 1,
num_pages: 1,
start: 0,
results: []
});
expect(searchFieldView.$('.search-field').val(), 'foo');
});
it('can clear the search', function () {
var requests = AjaxHelpers.requests(this);
searchFieldView = createSearchFieldView({
searchString: 'foo'
}).render();
searchFieldView.$('.action-clear').click();
AjaxHelpers.expectRequestURL(requests, mockUrl, {
page: '1',
page_size: '10',
sort_order: '',
text_search: ''
});
AjaxHelpers.respondWithJson(requests, {
count: 10,
current_page: 1,
num_pages: 1,
start: 0,
results: []
});
expect(searchFieldView.$('.search-field').val(), '');
expect(searchFieldView.$('.action-clear')).toHaveClass('is-hidden');
});
});
});
define(['sinon', 'underscore', 'URI'], function(sinon, _, URI) {
'use strict';
var fakeServer, fakeRequests, expectRequest, expectJsonRequest, expectPostRequest, expectJsonRequestURL,
var fakeServer, fakeRequests, expectRequest, expectJsonRequest, expectPostRequest, expectRequestURL,
respondWithJson, respondWithError, respondWithTextError, respondWithNoContent;
/* These utility methods are used by Jasmine tests to create a mock server or
......@@ -77,7 +77,7 @@ define(['sinon', 'underscore', 'URI'], function(sinon, _, URI) {
* @param expectedParameters An object representing the URL parameters
* @param requestIndex An optional index for the request (by default, the last request is used)
*/
expectJsonRequestURL = function(requests, expectedUrl, expectedParameters, requestIndex) {
expectRequestURL = function(requests, expectedUrl, expectedParameters, requestIndex) {
var request, parameters;
if (_.isUndefined(requestIndex)) {
requestIndex = requests.length - 1;
......@@ -153,15 +153,15 @@ define(['sinon', 'underscore', 'URI'], function(sinon, _, URI) {
};
return {
'server': fakeServer,
'requests': fakeRequests,
'expectRequest': expectRequest,
'expectJsonRequest': expectJsonRequest,
'expectJsonRequestURL': expectJsonRequestURL,
'expectPostRequest': expectPostRequest,
'respondWithJson': respondWithJson,
'respondWithError': respondWithError,
'respondWithTextError': respondWithTextError,
'respondWithNoContent': respondWithNoContent,
server: fakeServer,
requests: fakeRequests,
expectRequest: expectRequest,
expectJsonRequest: expectJsonRequest,
expectPostRequest: expectPostRequest,
expectRequestURL: expectRequestURL,
respondWithJson: respondWithJson,
respondWithError: respondWithError,
respondWithTextError: respondWithTextError,
respondWithNoContent: respondWithNoContent
};
});
<div class="page-header-search wrapper-search-<%= type %>">
<form class="search-form">
<div class="wrapper-search-input">
<label for="search-<%= type %>" class="search-label">><%- searchLabel %></label>
<input id="search-<%= type %>" class="search-field" type="text" value="<%- searchString %>" placeholder="<%- searchLabel %>" />
<button type="button" class="action action-clear <%= searchLabel ? '' : 'is-hidden' %>" aria-label="<%- gettext('Clear search') %>">
<i class="icon fa fa-times-circle" aria-hidden="true"></i><span class="sr"><%- gettext('Search') %></span>
</button>
</div>
<button type="submit" class="action action-search"><span class="icon fa-search" aria-hidden="true"></span><span class="sr"><%- gettext('Search') %></span></button>
</form>
</div>
......@@ -155,13 +155,14 @@
define([
// Run the common tests that use RequireJS.
'common-requirejs/include/common/js/spec/components/feedback_spec.js',
'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-requirejs/include/common/js/spec/components/view_utils_spec.js',
'common-requirejs/include/common/js/spec/components/feedback_spec.js'
'common-requirejs/include/common/js/spec/components/search_field_spec.js',
'common-requirejs/include/common/js/spec/components/view_utils_spec.js'
]);
}).call(this, requirejs, define);
......@@ -142,6 +142,11 @@ class BrowseTopicsPage(CoursePage, PaginatedUIMixin):
"""Return a list of the topic names present on the page."""
return self.q(css=CARD_TITLE_CSS).map(lambda e: e.text).results
@property
def topic_descriptions(self):
"""Return a list of the topic descriptions present on the page."""
return self.q(css='p.card-description').map(lambda e: e.text).results
def browse_teams_for_topic(self, topic_name):
"""
Show the teams list for `topic_name`.
......@@ -159,36 +164,32 @@ class BrowseTopicsPage(CoursePage, PaginatedUIMixin):
self.wait_for_ajax()
class BrowseTeamsPage(CoursePage, PaginatedUIMixin, TeamCardsMixin):
class BaseTeamsPage(CoursePage, PaginatedUIMixin, TeamCardsMixin):
"""
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.
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)
super(BaseTeamsPage, 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."""
self.wait_for_element_presence('.team-actions', 'Wait for the bottom links to be present')
"""Check if we're on a 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):
def header_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):
def header_description(self):
"""Get the topic description displayed by the page header"""
return self.q(css=TEAMS_HEADER_CSS + ' .page-description')[0].text
......@@ -229,6 +230,48 @@ class BrowseTeamsPage(CoursePage, PaginatedUIMixin, TeamCardsMixin):
).click()
self.wait_for_ajax()
@property
def _showing_search_results(self):
"""
Returns true if showing search results.
"""
return self.header_description.startswith(u"Showing results for")
def search(self, string):
"""
Searches for the specified string, and returns a SearchTeamsPage
representing the search results page.
"""
self.q(css='.search-field').first.fill(string)
self.q(css='.action-search').first.click()
self.wait_for(
lambda: self._showing_search_results,
description="Showing search results"
)
page = SearchTeamsPage(self.browser, self.course_id, self.topic)
page.wait_for_page()
return page
class BrowseTeamsPage(BaseTeamsPage):
"""
The paginated UI for browsing teams within a Topic on the Teams
page.
"""
def __init__(self, browser, course_id, topic):
super(BrowseTeamsPage, self).__init__(browser, course_id, topic)
self.url_path = "teams/#topics/{topic_id}".format(topic_id=self.topic['id'])
class SearchTeamsPage(BaseTeamsPage):
"""
The paginated UI for showing team search results.
page.
"""
def __init__(self, browser, course_id, topic):
super(SearchTeamsPage, self).__init__(browser, course_id, topic)
self.url_path = "teams/#topics/{topic_id}/search".format(topic_id=self.topic['id'])
class CreateOrEditTeamPage(CoursePage, FieldsMixin):
"""
......
......@@ -444,7 +444,7 @@ class BrowseTopicsTest(TeamsTabBase):
{u"max_team_size": 1, u"topics": [{"name": "", "id": "", "description": initial_description}]}
)
self.topics_page.visit()
truncated_description = self.topics_page.topic_cards[0].text
truncated_description = self.topics_page.topic_descriptions[0]
self.assertLess(len(truncated_description), len(initial_description))
self.assertTrue(truncated_description.endswith('...'))
self.assertIn(truncated_description.split('...')[0], initial_description)
......@@ -467,8 +467,8 @@ class BrowseTopicsTest(TeamsTabBase):
self.topics_page.browse_teams_for_topic('Example Topic')
browse_teams_page = BrowseTeamsPage(self.browser, self.course_id, topic)
self.assertTrue(browse_teams_page.is_browser_on_page())
self.assertEqual(browse_teams_page.header_topic_name, 'Example Topic')
self.assertEqual(browse_teams_page.header_topic_description, 'Description')
self.assertEqual(browse_teams_page.header_name, 'Example Topic')
self.assertEqual(browse_teams_page.header_description, 'Description')
@attr('shard_5')
......@@ -503,15 +503,24 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
def verify_page_header(self):
"""Verify that the page header correctly reflects the current topic's name and description."""
self.assertEqual(self.browse_teams_page.header_topic_name, self.topic['name'])
self.assertEqual(self.browse_teams_page.header_topic_description, self.topic['description'])
self.assertEqual(self.browse_teams_page.header_name, self.topic['name'])
self.assertEqual(self.browse_teams_page.header_description, self.topic['description'])
def verify_on_page(self, page_num, total_teams, pagination_header_text, footer_visible):
def verify_search_header(self, search_results_page, search_query):
"""Verify that the page header correctly reflects the current topic's name and description."""
self.assertEqual(search_results_page.header_name, 'Team Search')
self.assertEqual(
search_results_page.header_description,
'Showing results for "{search_query}"'.format(search_query=search_query)
)
def verify_on_page(self, teams_page, page_num, total_teams, pagination_header_text, footer_visible):
"""
Verify that we are on the correct team list page.
Arguments:
page_num (int): The one-indexed page we expect to be on
teams_page (BaseTeamsPage): The teams page object that should be the current page.
page_num (int): The one-indexed page number that we expect to be on
total_teams (list): An unsorted list of all the teams for the
current topic
pagination_header_text (str): Text we expect to see in the
......@@ -520,13 +529,13 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
footer controls.
"""
sorted_teams = self.teams_with_default_sort_order(total_teams)
self.assertTrue(self.browse_teams_page.get_pagination_header_text().startswith(pagination_header_text))
self.assertTrue(teams_page.get_pagination_header_text().startswith(pagination_header_text))
self.verify_teams(
self.browse_teams_page,
teams_page,
sorted_teams[(page_num - 1) * self.TEAMS_PAGE_SIZE:page_num * self.TEAMS_PAGE_SIZE]
)
self.assertEqual(
self.browse_teams_page.pagination_controls_visible(),
teams_page.pagination_controls_visible(),
footer_visible,
msg='Expected paging footer to be ' + 'visible' if footer_visible else 'invisible'
)
......@@ -648,11 +657,11 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
teams = self.create_teams(self.topic, self.TEAMS_PAGE_SIZE + 1, time_between_creation=1)
self.browse_teams_page.visit()
self.verify_page_header()
self.verify_on_page(1, teams, 'Showing 1-10 out of 11 total', True)
self.verify_on_page(self.browse_teams_page, 1, teams, 'Showing 1-10 out of 11 total', True)
self.browse_teams_page.press_next_page_button()
self.verify_on_page(2, teams, 'Showing 11-11 out of 11 total', True)
self.verify_on_page(self.browse_teams_page, 2, teams, 'Showing 11-11 out of 11 total', True)
self.browse_teams_page.press_previous_page_button()
self.verify_on_page(1, teams, 'Showing 1-10 out of 11 total', True)
self.verify_on_page(self.browse_teams_page, 1, teams, 'Showing 1-10 out of 11 total', True)
def test_teams_page_input(self):
"""
......@@ -670,25 +679,21 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
teams = self.create_teams(self.topic, self.TEAMS_PAGE_SIZE + 10, time_between_creation=1)
self.browse_teams_page.visit()
self.verify_page_header()
self.verify_on_page(1, teams, 'Showing 1-10 out of 20 total', True)
self.verify_on_page(self.browse_teams_page, 1, teams, 'Showing 1-10 out of 20 total', True)
self.browse_teams_page.go_to_page(2)
self.verify_on_page(2, teams, 'Showing 11-20 out of 20 total', True)
self.verify_on_page(self.browse_teams_page, 2, teams, 'Showing 11-20 out of 20 total', True)
self.browse_teams_page.go_to_page(1)
self.verify_on_page(1, teams, 'Showing 1-10 out of 20 total', True)
self.verify_on_page(self.browse_teams_page, 1, teams, 'Showing 1-10 out of 20 total', True)
def test_navigation_links(self):
def test_browse_team_topics(self):
"""
Scenario: User should be able to navigate to "browse all teams" and "search team description" links.
Given I am enrolled in a course with a team configuration and a topic
containing one team
When I visit the Teams page for that topic
Given I am enrolled in a course with teams enabled
When I visit the Teams page for a topic
Then I should see the correct page header
And I should see the link to "browse all team"
And I should navigate to that link
And I see the relevant page loaded
And I should see the link to "search teams"
And I should navigate to that link
And I see the relevant page loaded
And I should see the link to "browse teams in other topics"
When I should navigate to that link
Then I should see the topic browse page
"""
self.browse_teams_page.visit()
self.verify_page_header()
......@@ -696,10 +701,23 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
self.browse_teams_page.click_browse_all_teams_link()
self.assertTrue(self.topics_page.is_browser_on_page())
def test_search(self):
"""
Scenario: User should be able to search for a team
Given I am enrolled in a course with teams enabled
When I visit the Teams page for that topic
And I search for 'banana'
Then I should see the search result page
And the search header should be shown
And 0 results should be shown
"""
# Note: all searches will return 0 results with the mock search server
# used by Bok Choy.
self.create_teams(self.topic, 5)
self.browse_teams_page.visit()
self.verify_page_header()
self.browse_teams_page.click_search_team_link()
# TODO Add search page expectation once that implemented.
search_results_page = self.browse_teams_page.search('banana')
self.verify_search_header(search_results_page, 'banana')
self.assertTrue(search_results_page.get_pagination_header_text().startswith('Showing 0 out of 0 total'))
@attr('shard_5')
......@@ -726,8 +744,8 @@ class TeamFormActions(TeamsTabBase):
self.browse_teams_page.click_create_team_link()
self.verify_page_header(
title='Create a New Team',
description='Create a new team if you can\'t find existing teams to '
'join, or if you would like to learn with friends you know.',
description='Create a new team if you can\'t find an existing team to join, '
'or if you would like to learn with friends you know.',
breadcrumbs='All Topics {topic_name}'.format(topic_name=self.topic['name'])
)
......
......@@ -53,8 +53,8 @@ class Command(BaseCommand):
if len(args) == 0 and not options.get('all', False):
raise CommandError(u"reindex_course_team requires one or more arguments: <course_team_id>")
elif not settings.FEATURES.get('ENABLE_TEAMS_SEARCH', False):
raise CommandError(u"ENABLE_TEAMS_SEARCH must be enabled")
elif not settings.FEATURES.get('ENABLE_TEAMS', False):
raise CommandError(u"ENABLE_TEAMS must be enabled to use course team indexing")
if options.get('all', False):
course_teams = CourseTeam.objects.all()
......
......@@ -39,9 +39,9 @@ class ReindexCourseTeamTest(SharedModuleStoreTestCase):
def test_teams_search_flag_disabled_raises_command_error(self):
""" Test that raises CommandError for disabled feature flag. """
with mock.patch('django.conf.settings.FEATURES') as features:
features.return_value = {"ENABLE_TEAMS_SEARCH": False}
features.return_value = {"ENABLE_TEAMS": False}
with self.assertRaises(SystemExit), nostderr():
with self.assertRaisesRegexp(CommandError, ".* ENABLE_TEAMS_SEARCH must be enabled .*"):
with self.assertRaisesRegexp(CommandError, ".* ENABLE_TEAMS must be enabled .*"):
call_command('reindex_course_team')
def test_given_invalid_team_id_raises_command_error(self):
......
......@@ -15,7 +15,7 @@ class CourseTeamIndexer(object):
"""
INDEX_NAME = "course_team_index"
DOCUMENT_TYPE_NAME = "course_team"
ENABLE_SEARCH_KEY = "ENABLE_TEAMS_SEARCH"
ENABLE_SEARCH_KEY = "ENABLE_TEAMS"
def __init__(self, course_team):
self.course_team = course_team
......
......@@ -11,31 +11,11 @@
this.teamEvents = options.teamEvents;
this.teamEvents.bind('teams:update', this.onUpdate, this);
this.isStale = false;
},
onUpdate: function(event) {
// Mark the collection as stale so that it knows to refresh when needed.
this.isStale = true;
},
/**
* Refreshes the collection if it has been marked as stale.
* @param force If true, it will always refresh.
* @returns {promise} Returns a promise representing the refresh
*/
refresh: function(force) {
var self = this,
deferred = $.Deferred();
if (force || this.isStale) {
this.setPage(1)
.done(function() {
self.isStale = false;
deferred.resolve();
});
} else {
deferred.resolve();
}
return deferred.promise();
}
});
return BaseCollection;
......
......@@ -14,7 +14,7 @@
topic_id: this.topic_id = options.topic_id,
expand: 'user',
course_id: function () { return encodeURIComponent(self.course_id); },
order_by: function () { return this.sortField; }
order_by: function () { return self.searchString ? '' : this.sortField; }
},
BaseCollection.prototype.server_api
);
......
......@@ -25,7 +25,9 @@
},
onUpdate: function(event) {
this.isStale = this.isStale || event.action === 'create';
if (event.action === 'create') {
this.isStale = true;
}
},
model: TopicModel
......
......@@ -28,7 +28,7 @@ define(['backbone', 'URI', 'underscore', 'common/js/spec_helpers/ajax_helpers',
});
it('passes a course_id to the server', function () {
testRequestParam(this, 'course_id', 'my/course/id');
testRequestParam(this, 'course_id', TeamSpecHelpers.testCourseID);
});
it('URL encodes its course_id ', function () {
......
define(["jquery", "backbone", "teams/js/teams_tab_factory"],
function($, Backbone, TeamsTabFactory) {
define(['jquery', 'backbone', 'teams/js/teams_tab_factory',
'teams/js/spec_helpers/team_spec_helpers'],
function($, Backbone, TeamsTabFactory, TeamSpecHelpers) {
'use strict';
describe("Teams Tab Factory", function() {
var teamsTab;
var initializeTeamsTabFactory = function() {
TeamsTabFactory({
topics: {results: []},
topicsUrl: '',
teamsUrl: '',
maxTeamSize: 9999,
courseID: 'edX/DemoX/Demo_Course',
userInfo: {
username: 'test-user',
privileged: false,
staff: false,
team_memberships_data: null
}
});
TeamsTabFactory(TeamSpecHelpers.createMockContext());
};
beforeEach(function() {
......
......@@ -13,21 +13,21 @@ define([
var teamsUrl = '/api/team/v0/teams/',
createTeamData = {
id: null,
name: "TeamName",
course_id: "a/b/c",
topic_id: "awesomeness",
date_created: "",
description: "TeamDescription",
country: "US",
language: "en",
name: 'TeamName',
course_id: TeamSpecHelpers.testCourseID,
topic_id: TeamSpecHelpers.testTopicID,
date_created: '',
description: 'TeamDescription',
country: 'US',
language: 'en',
membership: [],
last_activity_at: ''
},
editTeamData = {
name: "UpdatedAvengers",
description: "We do not discuss about avengers.",
country: "US",
language: "en"
name: 'UpdatedAvengers',
description: 'We do not discuss about avengers.',
country: 'US',
language: 'en'
},
verifyValidation = function (requests, teamEditView, fieldsData) {
_.each(fieldsData, function (fieldData) {
......@@ -38,17 +38,19 @@ define([
var message = teamEditView.$('.wrapper-msg');
expect(message.hasClass('is-hidden')).toBeFalsy();
var actionMessage = (teamAction === 'create' ? 'Your team could not be created.' : 'Your team could not be updated.');
var actionMessage = (
teamAction === 'create' ? 'Your team could not be created.' : 'Your team could not be updated.'
);
expect(message.find('.title').text().trim()).toBe(actionMessage);
expect(message.find('.copy').text().trim()).toBe(
"Check the highlighted fields below and try again."
'Check the highlighted fields below and try again.'
);
_.each(fieldsData, function (fieldData) {
if (fieldData[2] === 'error') {
expect(teamEditView.$(fieldData[0].split(" ")[0] + '.error').length).toBe(1);
expect(teamEditView.$(fieldData[0].split(' ')[0] + '.error').length).toBe(1);
} else if (fieldData[2] === 'success') {
expect(teamEditView.$(fieldData[0].split(" ")[0] + '.error').length).toBe(0);
expect(teamEditView.$(fieldData[0].split(' ')[0] + '.error').length).toBe(0);
}
});
......@@ -58,9 +60,9 @@ define([
teamAction;
var createEditTeamView = function () {
var teamModel = {};
var testTeam = {};
if (teamAction === 'edit') {
teamModel = new TeamModel(
testTeam = new TeamModel(
{
id: editTeamID,
name: 'Avengers',
......@@ -80,16 +82,9 @@ define([
teamEvents: TeamSpecHelpers.teamEvents,
el: $('.teams-content'),
action: teamAction,
model: teamModel,
teamParams: {
teamsUrl: teamsUrl,
courseID: "a/b/c",
topicID: 'awesomeness',
topicName: 'Awesomeness',
languages: [['aa', 'Afar'], ['fr', 'French'], ['en', 'English']],
countries: [['af', 'Afghanistan'], ['CA', 'Canada'], ['US', 'United States']],
teamsDetailUrl: teamModel.url
}
model: testTeam,
topic: TeamSpecHelpers.createMockTopic(),
context: TeamSpecHelpers.testContext
}).render();
};
......@@ -133,13 +128,13 @@ define([
teamEditView.$('.u-field-name input').val(teamsData.name);
teamEditView.$('.u-field-textarea textarea').val(teamsData.description);
teamEditView.$('.u-field-language select').val(teamsData.language).attr("selected", "selected");
teamEditView.$('.u-field-country select').val(teamsData.country).attr("selected", "selected");
teamEditView.$('.u-field-language select').val(teamsData.language).attr('selected', 'selected');
teamEditView.$('.u-field-country select').val(teamsData.country).attr('selected', 'selected');
teamEditView.$('.create-team.form-actions .action-primary').click();
AjaxHelpers.expectJsonRequest(requests, requestMethod(), teamsUrl, teamsData);
AjaxHelpers.respondWithJson(requests, _.extend(_.extend({}, teamsData), teamAction === 'create' ? {id: '123'} : {}));
AjaxHelpers.respondWithJson(requests, _.extend({}, teamsData, teamAction === 'create' ? {id: '123'} : {}));
expect(teamEditView.$('.create-team.wrapper-msg .copy').text().trim().length).toBe(0);
expect(Backbone.history.navigate.calls[0].args).toContain(expectedUrl);
......@@ -209,10 +204,10 @@ define([
errorCode,
{'user_message': 'User message', 'developer_message': 'Developer message'}
);
expect(teamEditView.$('.wrapper-msg .copy').text().trim()).toBe("User message");
expect(teamEditView.$('.wrapper-msg .copy').text().trim()).toBe('User message');
} else {
AjaxHelpers.respondWithError(requests);
expect(teamEditView.$('.wrapper-msg .copy').text().trim()).toBe("An error occurred. Please try again.");
expect(teamEditView.$('.wrapper-msg .copy').text().trim()).toBe('An error occurred. Please try again.');
}
};
......@@ -233,7 +228,9 @@ define([
});
it('can create a team', function () {
assertTeamCreateUpdateInfo(this, createTeamData, teamsUrl, 'teams/awesomeness/123');
assertTeamCreateUpdateInfo(
this, createTeamData, teamsUrl, 'teams/' + TeamSpecHelpers.testTopicID + '/123'
);
});
it('shows validation error message when field is empty', function () {
......@@ -244,16 +241,16 @@ define([
assertValidationMessagesWhenInvalidData(this);
});
it("shows an error message for HTTP 500", function () {
it('shows an error message for HTTP 500', function () {
assertShowMessageOnError(this, createTeamData, teamsUrl, 500);
});
it("shows correct error message when server returns an error", function () {
it('shows correct error message when server returns an error', function () {
assertShowMessageOnError(this, createTeamData, teamsUrl, 400);
});
it("changes route on cancel click", function () {
assertRedirectsToCorrectUrlOnCancel('topics/awesomeness');
it('changes route on cancel click', function () {
assertRedirectsToCorrectUrlOnCancel('topics/' + TeamSpecHelpers.testTopicID);
});
});
......@@ -272,7 +269,10 @@ define([
copyTeamsData.country = 'CA';
copyTeamsData.language = 'fr';
assertTeamCreateUpdateInfo(this, copyTeamsData, teamsUrl + editTeamID + '?expand=user', 'teams/awesomeness/' + editTeamID);
assertTeamCreateUpdateInfo(
this, copyTeamsData, teamsUrl + editTeamID + '?expand=user',
'teams/' + TeamSpecHelpers.testTopicID + '/' + editTeamID
);
});
it('shows validation error message when field is empty', function () {
......@@ -283,16 +283,16 @@ define([
assertValidationMessagesWhenInvalidData(this);
});
it("shows an error message for HTTP 500", function () {
it('shows an error message for HTTP 500', function () {
assertShowMessageOnError(this, editTeamData, teamsUrl + editTeamID + '?expand=user', 500);
});
it("shows correct error message when server returns an error", function () {
it('shows correct error message when server returns an error', function () {
assertShowMessageOnError(this, editTeamData, teamsUrl + editTeamID + '?expand=user', 400);
});
it("changes route on cancel click", function () {
assertRedirectsToCorrectUrlOnCancel('teams/awesomeness/' + editTeamID);
it('changes route on cancel click', function () {
assertRedirectsToCorrectUrlOnCancel('teams/' + TeamSpecHelpers.testTopicID + '/' + editTeamID);
});
});
});
......
......@@ -13,17 +13,16 @@ define([
});
var createMyTeamsView = function(options) {
return new MyTeamsView({
el: '.teams-container',
collection: options.teams || TeamSpecHelpers.createMockTeams(),
teamMemberships: options.teamMemberships || TeamSpecHelpers.createMockTeamMemberships(),
showActions: true,
teamParams: {
topicID: 'test-topic',
countries: TeamSpecHelpers.testCountries,
languages: TeamSpecHelpers.testLanguages
}
}).render();
return new MyTeamsView(_.extend(
{
el: '.teams-container',
collection: options.teams || TeamSpecHelpers.createMockTeams(),
teamMemberships: TeamSpecHelpers.createMockTeamMemberships(),
showActions: true,
context: TeamSpecHelpers.testContext
},
options
)).render();
};
it('can render itself', function () {
......@@ -62,15 +61,16 @@ define([
expect(myTeamsView.$el.text().trim()).toBe('You are not currently a member of any team.');
teamMemberships.teamEvents.trigger('teams:update', { action: 'create' });
myTeamsView.render();
AjaxHelpers.expectJsonRequestURL(
AjaxHelpers.expectRequestURL(
requests,
'api/teams/team_memberships',
TeamSpecHelpers.testContext.teamMembershipsUrl,
{
expand : 'team',
username : 'testUser',
course_id : 'my/course/id',
username : TeamSpecHelpers.testContext.userInfo.username,
course_id : TeamSpecHelpers.testContext.courseID,
page : '1',
page_size : '10'
page_size : '10',
text_search: ''
}
);
AjaxHelpers.respondWithJson(requests, {});
......
......@@ -10,12 +10,10 @@ define([
createMembershipData,
createHeaderActionsView,
verifyErrorMessage,
ACCOUNTS_API_URL = '/api/user/v1/accounts/',
TEAMS_URL = '/api/team/v0/teams/',
TEAMS_MEMBERSHIP_URL = '/api/team/v0/team_membership/';
ACCOUNTS_API_URL = '/api/user/v1/accounts/';
createTeamsUrl = function (teamId) {
return TEAMS_URL + teamId + '?expand=user';
return TeamSpecHelpers.testContext.teamsUrl + teamId + '?expand=user';
};
createTeamModelData = function (teamId, teamName, membership) {
......@@ -27,21 +25,22 @@ define([
};
};
createHeaderActionsView = function(maxTeamSize, currentUsername, teamModelData, showEditButton) {
var teamId = 'teamA';
var model = new TeamModel(teamModelData, { parse: true });
createHeaderActionsView = function(requests, maxTeamSize, currentUsername, teamModelData, showEditButton) {
var model = new TeamModel(teamModelData, { parse: true }),
context = TeamSpecHelpers.createMockContext({
maxTeamSize: maxTeamSize,
userInfo: TeamSpecHelpers.createMockUserInfo({
username: currentUsername
})
});
return new TeamProfileHeaderActionsView(
{
courseID: TeamSpecHelpers.testCourseID,
teamEvents: TeamSpecHelpers.teamEvents,
context: context,
model: model,
teamsUrl: createTeamsUrl(teamId),
maxTeamSize: maxTeamSize,
currentUsername: currentUsername,
teamMembershipsUrl: TEAMS_MEMBERSHIP_URL,
topicID: '',
topic: TeamSpecHelpers.createMockTopic(),
showEditButton: showEditButton
}
).render();
......@@ -67,7 +66,7 @@ define([
});
verifyErrorMessage = function (requests, errorMessage, expectedMessage, joinTeam) {
var view = createHeaderActionsView(1, 'ma', createTeamModelData('teamA', 'teamAlpha', []));
var view = createHeaderActionsView(requests, 1, 'ma', createTeamModelData('teamA', 'teamAlpha', []));
if (joinTeam) {
// if we want the error to return when user try to join team, respond with no membership
AjaxHelpers.respondWithJson(requests, {"count": 0});
......@@ -78,8 +77,9 @@ define([
};
it('can render itself', function () {
var requests = AjaxHelpers.requests(this);
var teamModelData = createTeamModelData('teamA', 'teamAlpha', createMembershipData('ma'));
var view = createHeaderActionsView(1, 'ma', teamModelData);
var view = createHeaderActionsView(requests, 1, 'ma', teamModelData);
expect(view.$('.join-team').length).toEqual(1);
});
......@@ -90,14 +90,14 @@ define([
var teamId = 'teamA';
var teamName = 'teamAlpha';
var teamModelData = createTeamModelData(teamId, teamName, []);
var view = createHeaderActionsView(1, currentUsername, teamModelData);
var view = createHeaderActionsView(requests, 1, currentUsername, teamModelData);
// a get request will be sent to get user membership info
// because current user is not member of current team
AjaxHelpers.expectRequest(
requests,
'GET',
TEAMS_MEMBERSHIP_URL + '?' + $.param({
TeamSpecHelpers.testContext.teamMembershipsUrl + '?' + $.param({
'username': currentUsername, 'course_id': TeamSpecHelpers.testCourseID
})
);
......@@ -111,7 +111,7 @@ define([
AjaxHelpers.expectRequest(
requests,
'POST',
TEAMS_MEMBERSHIP_URL,
TeamSpecHelpers.testContext.teamMembershipsUrl,
$.param({'username': currentUsername, 'team_id': teamId})
);
AjaxHelpers.respondWithJson(requests, {});
......@@ -135,14 +135,14 @@ define([
it('shows already member message', function () {
var requests = AjaxHelpers.requests(this);
var currentUsername = 'ma1';
var view = createHeaderActionsView(1, currentUsername, createTeamModelData('teamA', 'teamAlpha', []));
var view = createHeaderActionsView(requests, 1, currentUsername, createTeamModelData('teamA', 'teamAlpha', []));
// a get request will be sent to get user membership info
// because current user is not member of current team
AjaxHelpers.expectRequest(
requests,
'GET',
TEAMS_MEMBERSHIP_URL + '?' + $.param({
TeamSpecHelpers.testContext.teamMembershipsUrl + '?' + $.param({
'username': currentUsername, 'course_id': TeamSpecHelpers.testCourseID
})
);
......@@ -156,6 +156,7 @@ define([
it('shows team full message', function () {
var requests = AjaxHelpers.requests(this);
var view = createHeaderActionsView(
requests,
1,
'ma1',
createTeamModelData('teamA', 'teamAlpha', createMembershipData('ma'))
......@@ -199,7 +200,6 @@ define([
});
it('shows correct error message if initializing the view fails', function () {
// Rendering the view sometimes require fetching user's memberships. This may fail.
var requests = AjaxHelpers.requests(this);
// verify user_message
......@@ -225,23 +225,26 @@ define([
view,
createAndAssertView;
createAndAssertView = function(showEditButton) {
createAndAssertView = function(requests, showEditButton) {
teamModelData = createTeamModelData('aveA', 'avengers', createMembershipData('ma'));
view = createHeaderActionsView(1, 'ma', teamModelData, showEditButton);
view = createHeaderActionsView(requests, 1, 'ma', teamModelData, showEditButton);
expect(view.$('.action-edit-team').length).toEqual(showEditButton ? 1 : 0);
};
it('renders when option showEditButton is true', function () {
createAndAssertView(true);
var requests = AjaxHelpers.requests(this);
createAndAssertView(requests, true);
});
it('does not render when option showEditButton is false', function () {
createAndAssertView(false);
var requests = AjaxHelpers.requests(this);
createAndAssertView(requests, false);
});
it("can navigate to correct url", function () {
var requests = AjaxHelpers.requests(this);
spyOn(Backbone.history, 'navigate');
createAndAssertView(true);
createAndAssertView(requests, true);
var editButton = view.$('.action-edit-team');
expect(editButton.length).toEqual(1);
......
......@@ -11,7 +11,7 @@ define([
DEFAULT_MEMBERSHIP = [
{
'user': {
'username': 'bilbo',
'username': TeamSpecHelpers.testUser,
'profile_image': {
'has_image': true,
'image_url_medium': '/image-url'
......@@ -42,20 +42,8 @@ define([
profileView = new TeamProfileView({
teamEvents: TeamSpecHelpers.teamEvents,
courseID: TeamSpecHelpers.testCourseID,
context: TeamSpecHelpers.testContext,
model: teamModel,
maxTeamSize: options.maxTeamSize || 3,
requestUsername: 'bilbo',
countries : [
['', ''],
['US', 'United States'],
['CA', 'Canada']
],
languages : [
['', ''],
['en', 'English'],
['fr', 'French']
],
teamMembershipDetailUrl: 'api/team/v0/team_membership/team_id,bilbo',
setFocusToHeaderFunc: function() {
$('.teams-content').focus();
}
......@@ -88,7 +76,9 @@ define([
$('.prompt.warning .action-primary').click();
// expect a request to DELETE the team membership
AjaxHelpers.expectJsonRequest(requests, 'DELETE', 'api/team/v0/team_membership/test-team,bilbo');
AjaxHelpers.expectJsonRequest(
requests, 'DELETE', '/api/team/v0/team_membership/test-team,' + TeamSpecHelpers.testUser
);
AjaxHelpers.respondWithNoContent(requests);
// expect a request to refetch the user's team memberships
......@@ -135,7 +125,7 @@ define([
expect(view.$('.team-detail-header').text()).toBe('Team Details');
expect(view.$('.team-country').text()).toContain('United States');
expect(view.$('.team-language').text()).toContain('English');
expect(view.$('.team-capacity').text()).toContain(members + ' / 3 Members');
expect(view.$('.team-capacity').text()).toContain(members + ' / 6 Members');
expect(view.$('.team-member').length).toBe(members);
expect(Boolean(view.$('.leave-team-link').length)).toBe(memberOfTeam);
};
......@@ -176,9 +166,9 @@ define([
expect(view.$('.team-user-membership-status').text().trim()).toBe('You are a member of this team.');
// assert tooltip text.
expect(view.$('.member-profile p').text()).toBe('bilbo');
expect(view.$('.member-profile p').text()).toBe(TeamSpecHelpers.testUser);
// assert user profile page url.
expect(view.$('.member-profile').attr('href')).toBe('/u/bilbo');
expect(view.$('.member-profile').attr('href')).toBe('/u/' + TeamSpecHelpers.testUser);
//Verify that the leave team link is present
expect(view.$(leaveTeamLinkSelector).text()).toContain('Leave Team');
......
......@@ -17,11 +17,7 @@ define([
collection: options.teams || TeamSpecHelpers.createMockTeams(),
teamMemberships: options.teamMemberships || TeamSpecHelpers.createMockTeamMemberships(),
showActions: true,
teamParams: {
topicID: 'test-topic',
countries: TeamSpecHelpers.testCountries,
languages: TeamSpecHelpers.testLanguages
}
context: TeamSpecHelpers.testContext
}).render();
};
......
......@@ -8,14 +8,6 @@ define([
'use strict';
describe('TeamsTab', function () {
var expectContent = function (teamsTabView, text) {
expect(teamsTabView.$('.page-content-main').text()).toContain(text);
};
var expectHeader = function (teamsTabView, text) {
expect(teamsTabView.$('.teams-header').text()).toContain(text);
};
var expectError = function (teamsTabView, text) {
expect(teamsTabView.$('.warning').text()).toContain(text);
};
......@@ -26,30 +18,17 @@ define([
var createTeamsTabView = function(options) {
var defaultTopics = {
count: 1,
count: 5,
num_pages: 1,
current_page: 1,
start: 0,
results: [{
description: 'test description',
name: 'test topic',
id: 'test_topic',
team_count: 0
}]
results: TeamSpecHelpers.createMockTopicData(1, 5)
},
teamsTabView = new TeamsTabView(
_.extend(
{
el: $('.teams-content'),
topics: defaultTopics,
userInfo: TeamSpecHelpers.createMockUserInfo(),
topicsUrl: 'api/topics/',
topicUrl: 'api/topics/topic_id,test/course/id',
teamsUrl: 'api/teams/',
courseID: 'test/course/id'
},
options || {}
)
{
el: $('.teams-content'),
context: TeamSpecHelpers.createMockContext(options)
}
);
teamsTabView.start();
return teamsTabView;
......@@ -82,7 +61,7 @@ define([
var requests = AjaxHelpers.requests(this),
teamsTabView = createTeamsTabView();
teamsTabView.router.navigate('topics/no_such_topic', {trigger: true});
AjaxHelpers.expectRequest(requests, 'GET', 'api/topics/no_such_topic,test/course/id', null);
AjaxHelpers.expectRequest(requests, 'GET', '/api/team/v0/topics/no_such_topic,course/1', null);
AjaxHelpers.respondWithError(requests, 404);
expectError(teamsTabView, 'The topic "no_such_topic" could not be found.');
expectFocus(teamsTabView.$('.warning'));
......@@ -91,8 +70,8 @@ define([
it('displays and focuses an error message when trying to navigate to a nonexistent team', function () {
var requests = AjaxHelpers.requests(this),
teamsTabView = createTeamsTabView();
teamsTabView.router.navigate('teams/test_topic/no_such_team', {trigger: true});
AjaxHelpers.expectRequest(requests, 'GET', 'api/teams/no_such_team?expand=user', null);
teamsTabView.router.navigate('teams/' + TeamSpecHelpers.testTopicID + '/no_such_team', {trigger: true});
AjaxHelpers.expectRequest(requests, 'GET', '/api/team/v0/teams/no_such_team?expand=user', null);
AjaxHelpers.respondWithError(requests, 404);
expectError(teamsTabView, 'The team "no_such_team" could not be found.');
expectFocus(teamsTabView.$('.warning'));
......@@ -113,7 +92,7 @@ define([
it('allows access to a team which an unprivileged user is a member of', function () {
var teamsTabView = createTeamsTabView({
userInfo: TeamSpecHelpers.createMockUserInfo({
username: 'test-user',
username: TeamSpecHelpers.testUser,
privileged: false
})
});
......@@ -121,7 +100,7 @@ define([
attributes: {
membership: [{
user: {
username: 'test-user'
username: TeamSpecHelpers.testUser
}
}]
}
......@@ -137,5 +116,103 @@ define([
})).toBe(true);
});
});
describe('Search', function () {
var verifyTeamsRequest = function(requests, options) {
AjaxHelpers.expectRequestURL(requests, TeamSpecHelpers.testContext.teamsUrl,
_.extend(
{
topic_id: TeamSpecHelpers.testTopicID,
expand: 'user',
course_id: TeamSpecHelpers.testCourseID,
order_by: '',
page: '1',
page_size: '10',
text_search: ''
},
options
));
};
it('can search teams', function () {
var requests = AjaxHelpers.requests(this),
teamsTabView = createTeamsTabView();
teamsTabView.browseTopic(TeamSpecHelpers.testTopicID);
verifyTeamsRequest(requests, {
order_by: 'last_activity_at',
text_search: ''
});
AjaxHelpers.respondWithJson(requests, {});
teamsTabView.$('.search-field').val('foo');
teamsTabView.$('.action-search').click();
verifyTeamsRequest(requests, {
order_by: '',
text_search: 'foo'
});
AjaxHelpers.respondWithJson(requests, {});
expect(teamsTabView.$('.page-title').text()).toBe('Team Search');
expect(teamsTabView.$('.page-description').text()).toBe('Showing results for "foo"');
});
it('can clear a search', function () {
var requests = AjaxHelpers.requests(this),
teamsTabView = createTeamsTabView();
teamsTabView.browseTopic(TeamSpecHelpers.testTopicID);
AjaxHelpers.respondWithJson(requests, {});
// Perform a search
teamsTabView.$('.search-field').val('foo');
teamsTabView.$('.action-search').click();
AjaxHelpers.respondWithJson(requests, {});
// Clear the search and submit it again
teamsTabView.$('.search-field').val('');
teamsTabView.$('.action-search').click();
verifyTeamsRequest(requests, {
order_by: 'last_activity_at',
text_search: ''
});
AjaxHelpers.respondWithJson(requests, {});
expect(teamsTabView.$('.page-title').text()).toBe('Test Topic 1');
expect(teamsTabView.$('.page-description').text()).toBe('Test description 1');
});
it('clears the search when navigating away and then back', function () {
var requests = AjaxHelpers.requests(this),
teamsTabView = createTeamsTabView();
teamsTabView.browseTopic(TeamSpecHelpers.testTopicID);
AjaxHelpers.respondWithJson(requests, {});
// Perform a search
teamsTabView.$('.search-field').val('foo');
teamsTabView.$('.action-search').click();
AjaxHelpers.respondWithJson(requests, {});
// Navigate back to the teams list
teamsTabView.$('.breadcrumbs a').last().click();
verifyTeamsRequest(requests, {
order_by: 'last_activity_at',
text_search: ''
});
AjaxHelpers.respondWithJson(requests, {});
expect(teamsTabView.$('.page-title').text()).toBe('Test Topic 1');
expect(teamsTabView.$('.page-description').text()).toBe('Test description 1');
});
it('does not switch to showing results when the search returns an error', function () {
var requests = AjaxHelpers.requests(this),
teamsTabView = createTeamsTabView();
teamsTabView.browseTopic(TeamSpecHelpers.testTopicID);
AjaxHelpers.respondWithJson(requests, {});
// Perform a search
teamsTabView.$('.search-field').val('foo');
teamsTabView.$('.action-search').click();
AjaxHelpers.respondWithError(requests);
expect(teamsTabView.$('.page-title').text()).toBe('Test Topic 1');
expect(teamsTabView.$('.page-description').text()).toBe('Test description 1');
expect(teamsTabView.$('.search-field').val(), 'foo');
});
});
});
});
......@@ -11,14 +11,11 @@ define([
var createTopicTeamsView = function(options) {
return new TopicTeamsView({
el: '.teams-container',
model: TeamSpecHelpers.createMockTopic(),
collection: options.teams || TeamSpecHelpers.createMockTeams(),
teamMemberships: options.teamMemberships || TeamSpecHelpers.createMockTeamMemberships(),
showActions: true,
teamParams: {
topicID: 'test-topic',
countries: TeamSpecHelpers.testCountries,
languages: TeamSpecHelpers.testLanguages
}
context: TeamSpecHelpers.testContext
}).render();
};
......@@ -27,8 +24,8 @@ define([
options = {showActions: true};
}
var expectedTitle = 'Are you having trouble finding a team to join?',
expectedMessage = 'Try browsing all teams or searching team descriptions. If you ' +
'still can\'t find a team to join, create a new team in this topic.',
expectedMessage = 'Browse teams in other topics or search teams in this topic. ' +
'If you still can\'t find a team to join, create a new team in this topic.',
title = teamsView.$('.title').text().trim(),
message = teamsView.$('.copy').text().trim();
if (options.showActions) {
......@@ -65,17 +62,16 @@ define([
var emptyMembership = TeamSpecHelpers.createMockTeamMemberships([]),
teamsView = createTopicTeamsView({ teamMemberships: emptyMembership });
spyOn(Backbone.history, 'navigate');
teamsView.$('a.browse-teams').click();
teamsView.$('.browse-teams').click();
expect(Backbone.history.navigate.calls[0].args).toContain('browse');
});
it('can search teams', function () {
it('gives the search field focus when clicking on the search teams link', function () {
var emptyMembership = TeamSpecHelpers.createMockTeamMemberships([]),
teamsView = createTopicTeamsView({ teamMemberships: emptyMembership });
spyOn(Backbone.history, 'navigate');
teamsView.$('a.search-teams').click();
// TODO! Should be updated once team description search feature is available
expect(Backbone.history.navigate.calls[0].args).toContain('browse');
spyOn($.fn, 'focus').andCallThrough();
teamsView.$('.search-teams').click();
expect(teamsView.$('.search-field').first().focus).toHaveBeenCalled();
});
it('can show the create team modal', function () {
......@@ -83,7 +79,9 @@ define([
teamsView = createTopicTeamsView({ teamMemberships: emptyMembership });
spyOn(Backbone.history, 'navigate');
teamsView.$('a.create-team').click();
expect(Backbone.history.navigate.calls[0].args).toContain('topics/test-topic/create-team');
expect(Backbone.history.navigate.calls[0].args).toContain(
'topics/' + TeamSpecHelpers.testTopicID + '/create-team'
);
});
it('does not show actions for a user already in a team', function () {
......@@ -118,13 +116,13 @@ define([
verifyActions(teamsView, {showActions: true});
teamMemberships.teamEvents.trigger('teams:update', { action: 'create' });
teamsView.render();
AjaxHelpers.expectJsonRequestURL(
AjaxHelpers.expectRequestURL(
requests,
'foo',
{
expand : 'team',
username : 'testUser',
course_id : 'my/course/id',
course_id : TeamSpecHelpers.testCourseID,
page : '1',
page_size : '10'
}
......
......@@ -10,7 +10,8 @@ define([
return new TopicsView({
teamEvents: TeamSpecHelpers.teamEvents,
el: '.topics-container',
collection: topicCollection
collection: topicCollection,
context: TeamSpecHelpers.createMockContext()
}).render();
};
......@@ -48,14 +49,15 @@ define([
topicsView = createTopicsView();
triggerUpdateEvent(topicsView);
AjaxHelpers.expectJsonRequestURL(
AjaxHelpers.expectRequestURL(
requests,
'api/teams/topics',
TeamSpecHelpers.testContext.topicUrl,
{
course_id : 'my/course/id',
page : '1',
page_size : '5', // currently the page size is determined by the size of the collection
order_by : 'name'
course_id: TeamSpecHelpers.testCourseID,
page: '1',
page_size: '5', // currently the page size is determined by the size of the collection
order_by: 'name',
text_search: ''
}
);
});
......@@ -66,14 +68,15 @@ define([
// Staff are not immediately added to the team, but may choose to join after the create event.
triggerUpdateEvent(topicsView, true);
AjaxHelpers.expectJsonRequestURL(
AjaxHelpers.expectRequestURL(
requests,
'api/teams/topics',
TeamSpecHelpers.testContext.topicUrl,
{
course_id : 'my/course/id',
page : '1',
page_size : '5', // currently the page size is determined by the size of the collection
order_by : 'name'
course_id: TeamSpecHelpers.testCourseID,
page: '1',
page_size: '5', // currently the page size is determined by the size of the collection
order_by: 'name',
text_search: ''
}
);
});
......
......@@ -3,13 +3,15 @@ define([
'underscore',
'teams/js/collections/team',
'teams/js/collections/team_membership',
'teams/js/collections/topic'
], function (Backbone, _, TeamCollection, TeamMembershipCollection, TopicCollection) {
'teams/js/collections/topic',
'teams/js/models/topic'
], function (Backbone, _, TeamCollection, TeamMembershipCollection, TopicCollection, TopicModel) {
'use strict';
var createMockPostResponse, createMockDiscussionResponse, createAnnotatedContentInfo, createMockThreadResponse,
createMockTopicData, createMockTopicCollection,
createMockTopicData, createMockTopicCollection, createMockTopic,
testCourseID = 'course/1',
testUser = 'testUser',
testTopicID = 'test-topic-1',
testTeamDiscussionID = "12345",
teamEvents = _.clone(Backbone.Events),
testCountries = [
......@@ -52,7 +54,7 @@ define([
},
{
teamEvents: teamEvents,
course_id: 'my/course/id',
course_id: testCourseID,
parse: true
}
);
......@@ -81,18 +83,22 @@ define([
num_pages: 3,
current_page: 1,
start: 0,
sort_order: 'last_activity_at',
results: teamMembershipData
},
_.extend(_.extend({}, {
_.extend(
{},
{
teamEvents: teamEvents,
course_id: 'my/course/id',
course_id: testCourseID,
parse: true,
url: 'api/teams/team_memberships',
url: testContext.teamMembershipsUrl,
username: testUser,
privileged: false,
staff: false
}),
options)
},
options
)
);
};
......@@ -144,7 +150,7 @@ define([
group_id: 1,
endorsed: false
},
options || {}
options
);
};
......@@ -228,21 +234,56 @@ define([
context: "standalone",
endorsed: false
},
options || {}
options
);
};
createMockTopicData = function (startIndex, stopIndex) {
return _.map(_.range(startIndex, stopIndex + 1), function (i) {
return {
"description": "description " + i,
"name": "topic " + i,
"id": "id " + i,
"description": "Test description " + i,
"name": "Test Topic " + i,
"id": "test-topic-" + i,
"team_count": 0
};
});
};
createMockTopic = function(options) {
return new TopicModel(_.extend(
{
id: testTopicID,
name: 'Test Topic 1',
description: 'Test description 1'
},
options
));
};
var testContext = {
courseID: testCourseID,
topics: {
count: 5,
num_pages: 1,
current_page: 1,
start: 0,
results: createMockTopicData(1, 5)
},
maxTeamSize: 6,
languages: testLanguages,
countries: testCountries,
topicUrl: '/api/team/v0/topics/topic_id,' + testCourseID,
teamsUrl: '/api/team/v0/teams/',
teamsDetailUrl: '/api/team/v0/teams/team_id',
teamMembershipsUrl: '/api/team/v0/team_memberships/',
teamMembershipDetailUrl: '/api/team/v0/team_membership/team_id,' + testUser,
userInfo: createMockUserInfo()
};
var createMockContext = function(options) {
return _.extend({}, testContext, options);
};
createMockTopicCollection = function (topicData) {
topicData = topicData !== undefined ? topicData : createMockTopicData(1, 5);
......@@ -253,13 +294,13 @@ define([
num_pages: 2,
start: 0,
results: topicData,
sort_order: "name"
sort_order: 'name'
},
{
teamEvents: teamEvents,
course_id: 'my/course/id',
course_id: testCourseID,
parse: true,
url: 'api/teams/topics'
url: testContext.topicUrl
}
);
};
......@@ -268,14 +309,18 @@ define([
teamEvents: teamEvents,
testCourseID: testCourseID,
testUser: testUser,
testTopicID: testTopicID,
testCountries: testCountries,
testLanguages: testLanguages,
testTeamDiscussionID: testTeamDiscussionID,
testContext: testContext,
createMockTeamData: createMockTeamData,
createMockTeams: createMockTeams,
createMockTeamMembershipsData: createMockTeamMembershipsData,
createMockTeamMemberships: createMockTeamMemberships,
createMockUserInfo: createMockUserInfo,
createMockContext: createMockContext,
createMockTopic: createMockTopic,
createMockPostResponse: createMockPostResponse,
createMockDiscussionResponse: createMockDiscussionResponse,
createAnnotatedContentInfo: createAnnotatedContentInfo,
......
......@@ -4,7 +4,10 @@
define(['jquery', 'underscore', 'backbone', 'teams/js/views/teams_tab'],
function ($, _, Backbone, TeamsTabView) {
return function (options) {
var teamsTab = new TeamsTabView(_.extend(options, {el: $('.teams-content')}));
var teamsTab = new TeamsTabView({
el: $('.teams-content'),
context: options
});
teamsTab.start();
};
});
......
......@@ -21,22 +21,19 @@
initialize: function(options) {
this.teamEvents = options.teamEvents;
this.courseID = options.teamParams.courseID;
this.topicID = options.teamParams.topicID;
this.context = options.context;
this.topic = options.topic;
this.collection = options.collection;
this.teamsUrl = options.teamParams.teamsUrl;
this.languages = options.teamParams.languages;
this.countries = options.teamParams.countries;
this.teamsDetailUrl = options.teamParams.teamsDetailUrl;
this.action = options.action;
if (this.action === 'create') {
this.teamModel = new TeamModel({});
this.teamModel.url = this.teamsUrl;
this.teamModel.url = this.context.teamsUrl;
this.primaryButtonTitle = gettext("Create");
} else if(this.action === 'edit' ) {
this.teamModel = options.model;
this.teamModel.url = this.teamsDetailUrl.replace('team_id', options.model.get('id')) + '?expand=user';
this.teamModel.url = this.context.teamsDetailUrl.replace('team_id', options.model.get('id')) +
'?expand=user';
this.primaryButtonTitle = gettext("Update");
}
......@@ -63,7 +60,7 @@
required: false,
showMessages: false,
titleIconName: 'fa-comment-o',
options: this.languages,
options: this.context.languages,
helpMessage: gettext('The language that team members primarily use to communicate with each other.')
});
......@@ -74,7 +71,7 @@
required: false,
showMessages: false,
titleIconName: 'fa-globe',
options: this.countries,
options: this.context.countries,
helpMessage: gettext('The country that team members primarily identify with.')
});
},
......@@ -117,8 +114,8 @@
};
if (this.action === 'create') {
data.course_id = this.courseID;
data.topic_id = this.topicID;
data.course_id = this.context.courseID;
data.topic_id = this.topic.id;
} else if (this.action === 'edit' ) {
saveOptions.patch = true;
saveOptions.contentType = 'application/merge-patch+json';
......@@ -137,7 +134,7 @@
team: result
});
Backbone.history.navigate(
'teams/' + view.topicID + '/' + view.teamModel.id,
'teams/' + view.topic.id + '/' + view.teamModel.id,
{trigger: true}
);
})
......@@ -208,9 +205,9 @@
event.preventDefault();
var url;
if (this.action === 'create') {
url = 'topics/' + this.topicID;
url = 'topics/' + this.topic.id;
} else if (this.action === 'edit' ) {
url = 'teams/' + this.topicID + '/' + this.teamModel.get('id');
url = 'teams/' + this.topic.id + '/' + this.teamModel.get('id');
}
Backbone.history.navigate(url, {trigger: true});
}
......
......@@ -18,15 +18,11 @@
},
initialize: function (options) {
this.teamEvents = options.teamEvents;
this.courseID = options.courseID;
this.maxTeamSize = options.maxTeamSize;
this.requestUsername = options.requestUsername;
this.isPrivileged = options.isPrivileged;
this.teamMembershipDetailUrl = options.teamMembershipDetailUrl;
this.context = options.context;
this.setFocusToHeaderFunc = options.setFocusToHeaderFunc;
this.countries = TeamUtils.selectorOptionsArrayToHashWithBlank(options.countries);
this.languages = TeamUtils.selectorOptionsArrayToHashWithBlank(options.languages);
this.countries = TeamUtils.selectorOptionsArrayToHashWithBlank(this.context.countries);
this.languages = TeamUtils.selectorOptionsArrayToHashWithBlank(this.context.languages);
this.listenTo(this.model, "change", this.render);
},
......@@ -34,18 +30,17 @@
render: function () {
var memberships = this.model.get('membership'),
discussionTopicID = this.model.get('discussion_topic_id'),
isMember = TeamUtils.isUserMemberOfTeam(memberships, this.requestUsername);
isMember = TeamUtils.isUserMemberOfTeam(memberships, this.context.userInfo.username);
this.$el.html(_.template(teamTemplate, {
courseID: this.courseID,
courseID: this.context.courseID,
discussionTopicID: discussionTopicID,
readOnly: !(this.isPrivileged || isMember),
readOnly: !(this.context.userInfo.privileged || isMember),
country: this.countries[this.model.get('country')],
language: this.languages[this.model.get('language')],
membershipText: TeamUtils.teamCapacityText(memberships.length, this.maxTeamSize),
membershipText: TeamUtils.teamCapacityText(memberships.length, this.context.maxTeamSize),
isMember: isMember,
hasCapacity: memberships.length < this.maxTeamSize,
hasCapacity: memberships.length < this.context.maxTeamSize,
hasMembers: memberships.length >= 1
}));
this.discussionView = new TeamDiscussionView({
el: this.$('.discussion-module')
......@@ -84,7 +79,7 @@
function() {
$.ajax({
type: 'DELETE',
url: view.teamMembershipDetailUrl.replace('team_id', view.model.get('id'))
url: view.context.teamMembershipDetailUrl.replace('team_id', view.model.get('id'))
}).done(function (data) {
view.model.fetch()
.done(function() {
......
......@@ -21,21 +21,19 @@
initialize: function(options) {
this.teamEvents = options.teamEvents;
this.template = _.template(teamProfileHeaderActionsTemplate);
this.courseID = options.courseID;
this.maxTeamSize = options.maxTeamSize;
this.currentUsername = options.currentUsername;
this.teamMembershipsUrl = options.teamMembershipsUrl;
this.context = options.context;
this.showEditButton = options.showEditButton;
this.topicID = options.topicID;
this.topic = options.topic;
this.listenTo(this.model, "change", this.render);
},
render: function() {
var view = this,
username = this.context.userInfo.username,
message,
showJoinButton,
teamHasSpace;
this.getUserTeamInfo(this.currentUsername, view.maxTeamSize).done(function (info) {
this.getUserTeamInfo(username, this.context.maxTeamSize).done(function (info) {
teamHasSpace = info.teamHasSpace;
// if user is the member of current team then we wouldn't show anything
......@@ -62,8 +60,8 @@
var view = this;
$.ajax({
type: 'POST',
url: view.teamMembershipsUrl,
data: {'username': view.currentUsername, 'team_id': view.model.get('id')}
url: view.context.teamMembershipsUrl,
data: {'username': view.context.userInfo.username, 'team_id': view.model.get('id')}
}).done(function (data) {
view.model.fetch()
.done(function() {
......@@ -97,8 +95,8 @@
var view = this;
$.ajax({
type: 'GET',
url: view.teamMembershipsUrl,
data: {'username': username, 'course_id': view.courseID}
url: view.context.teamMembershipsUrl,
data: {'username': username, 'course_id': view.context.courseID}
}).done(function (data) {
info.alreadyMember = (data.count > 0);
info.memberOfCurrentTeam = false;
......@@ -115,9 +113,13 @@
return deferred.promise();
},
editTeam: function (event) {
event.preventDefault();
Backbone.history.navigate('topics/' + this.topicID + '/' + this.model.get('id') +'/edit-team', {trigger: true});
Backbone.history.navigate(
'topics/' + this.topic.id + '/' + this.model.get('id') +'/edit-team',
{trigger: true}
);
}
});
});
......
......@@ -18,14 +18,14 @@
initialize: function (options) {
this.topic = options.topic;
this.teamMemberships = options.teamMemberships;
this.teamParams = options.teamParams;
this.context = options.context;
this.itemViewClass = TeamCardView.extend({
router: options.router,
topic: options.topic,
maxTeamSize: options.maxTeamSize,
maxTeamSize: this.context.maxTeamSize,
srInfo: this.srInfo,
countries: TeamUtils.selectorOptionsArrayToHashWithBlank(options.teamParams.countries),
languages: TeamUtils.selectorOptionsArrayToHashWithBlank(options.teamParams.languages)
countries: TeamUtils.selectorOptionsArrayToHashWithBlank(this.context.countries),
languages: TeamUtils.selectorOptionsArrayToHashWithBlank(this.context.languages)
});
PaginatedView.prototype.initialize.call(this);
}
......
......@@ -36,6 +36,7 @@
configuration: 'square_card',
cardClass: 'topic-card',
pennant: gettext('Topic'),
title: function () { return this.model.get('name'); },
description: function () { return this.model.get('description'); },
details: function () { return this.detailViews; },
......
......@@ -15,6 +15,7 @@
},
initialize: function(options) {
this.showSortControls = options.showSortControls;
TeamsView.prototype.initialize.call(this, options);
},
......@@ -24,21 +25,29 @@
this.collection.refresh(),
this.teamMemberships.refresh()
).done(function() {
TeamsView.prototype.render.call(self);
TeamsView.prototype.render.call(self);
if (self.teamMemberships.canUserCreateTeam()) {
var message = interpolate_text(
_.escape(gettext("Try {browse_span_start}browsing all teams{span_end} or {search_span_start}searching team descriptions{span_end}. If you still can't find a team to join, {create_span_start}create a new team in this topic{span_end}.")),
{
'browse_span_start': '<a class="browse-teams" href="">',
'search_span_start': '<a class="search-teams" href="">',
'create_span_start': '<a class="create-team" href="">',
'span_end': '</a>'
}
);
self.$el.append(_.template(teamActionsTemplate, {message: message}));
}
});
if (self.teamMemberships.canUserCreateTeam()) {
var message = interpolate_text(
// Translators: this string is shown at the bottom of the teams page
// to find a team to join or else to create a new one. There are three
// links that need to be included in the message:
// 1. Browse teams in other topics
// 2. search teams
// 3. create a new team
// Be careful to start each link with the appropriate start indicator
// (e.g. {browse_span_start} for #1) and finish it with {span_end}.
_.escape(gettext("{browse_span_start}Browse teams in other topics{span_end} or {search_span_start}search teams{span_end} in this topic. If you still can't find a team to join, {create_span_start}create a new team in this topic{span_end}.")),
{
'browse_span_start': '<a class="browse-teams" href="">',
'search_span_start': '<a class="search-teams" href="">',
'create_span_start': '<a class="create-team" href="">',
'span_end': '</a>'
}
);
self.$el.append(_.template(teamActionsTemplate, {message: message}));
}
});
return this;
},
......@@ -48,21 +57,25 @@
},
searchTeams: function (event) {
var searchField = $('.page-header-search .search-field');
event.preventDefault();
// TODO! Will navigate to correct place once required functionality is available
Backbone.history.navigate('browse', {trigger: true});
searchField.focus();
searchField.select();
$('html, body').animate({
scrollTop: 0
}, 500);
},
showCreateTeamForm: function (event) {
event.preventDefault();
Backbone.history.navigate('topics/' + this.teamParams.topicID + '/create-team', {trigger: true});
Backbone.history.navigate('topics/' + this.model.id + '/create-team', {trigger: true});
},
createHeaderView: function () {
return new PagingHeader({
collection: this.options.collection,
srInfo: this.srInfo,
showSortControls: true
showSortControls: this.showSortControls
});
}
});
......
......@@ -175,7 +175,7 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView):
* text_search: Searches for full word matches on the name, description,
country, and language fields. NOTES: Search is on full names for countries
and languages, not the ISO codes. Text_search cannot be requested along with
with order_by. Searching relies on the ENABLE_TEAMS_SEARCH flag being set to True.
with order_by.
* order_by: Cannot be called along with with text_search. Must be one of the following:
......@@ -311,7 +311,8 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView):
status=status.HTTP_400_BAD_REQUEST
)
if 'text_search' in request.QUERY_PARAMS and 'order_by' in request.QUERY_PARAMS:
text_search = request.QUERY_PARAMS.get('text_search', None)
if text_search and request.QUERY_PARAMS.get('order_by', None):
return Response(
build_api_error(ugettext_noop("text_search and order_by cannot be provided together")),
status=status.HTTP_400_BAD_REQUEST
......@@ -327,13 +328,12 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView):
return Response(error, status=status.HTTP_400_BAD_REQUEST)
result_filter.update({'topic_id': request.QUERY_PARAMS['topic_id']})
if 'text_search' in request.QUERY_PARAMS and CourseTeamIndexer.search_is_enabled():
if text_search and CourseTeamIndexer.search_is_enabled():
search_engine = CourseTeamIndexer.engine()
text_search = request.QUERY_PARAMS['text_search'].encode('utf-8')
result_filter.update({'course_id': course_id_string})
search_results = search_engine.search(
query_string=text_search,
query_string=text_search.encode('utf-8'),
field_dictionary=result_filter,
size=MAXIMUM_SEARCH_SIZE,
)
......
......@@ -631,7 +631,7 @@ PDF_RECEIPT_COBRAND_LOGO_HEIGHT_MM = ENV_TOKENS.get(
if FEATURES.get('ENABLE_COURSEWARE_SEARCH') or \
FEATURES.get('ENABLE_DASHBOARD_SEARCH') or \
FEATURES.get('ENABLE_COURSE_DISCOVERY') or \
FEATURES.get('ENABLE_TEAMS_SEARCH'):
FEATURES.get('ENABLE_TEAMS'):
# Use ElasticSearch as the search engine herein
SEARCH_ENGINE = "search.elastic.ElasticSearchEngine"
......
......@@ -401,9 +401,6 @@ FEATURES = {
# Teams feature
'ENABLE_TEAMS': True,
# Enable indexing teams for search
'ENABLE_TEAMS_SEARCH': False,
# Show video bumper in LMS
'ENABLE_VIDEO_BUMPER': False,
......
......@@ -485,9 +485,6 @@ FEATURES['ENABLE_EDXNOTES'] = True
# Enable teams feature for tests.
FEATURES['ENABLE_TEAMS'] = True
# Enable indexing teams for search
FEATURES['ENABLE_TEAMS_SEARCH'] = True
# Add milestones to Installed apps for testing
INSTALLED_APPS += ('milestones', 'openedx.core.djangoapps.call_stack_manager')
......
......@@ -17,7 +17,7 @@
var json = this.model.attributes;
this.$el.html(this.template(json));
if (this.headerActionsView) {
this.headerActionsView.setElement(this.$('.header-action-view')).render();
this.headerActionsView.setElement(this.$('.page-header-secondary')).render();
}
return this;
}
......
......@@ -49,16 +49,17 @@
.page-header.has-secondary {
.page-header-main {
display: inline-block;
width: flex-grid(8,12);
}
.page-header-main {
display: inline-block;
width: flex-grid(8,12);
}
.page-header-secondary {
display: inline-block;
width: flex-grid(4,12);
@include text-align(right);
}
.page-header-secondary {
@include text-align(right);
display: inline-block;
width: flex-grid(4,12);
vertical-align: text-bottom;
}
}
// ui bits
......@@ -83,41 +84,56 @@
.page-header-search {
.wrapper-search-input {
display: inline-block;
position: relative;
vertical-align: middle;
}
.search-label {
@extend %text-sr;
}
.search-field {
transition: all $tmg-f2 ease-in-out;
border: 0;
border-bottom: 2px solid transparent;
border: 1px solid $gray-l4;
border-radius: 3px;
padding: ($baseline/4) ($baseline/2);
font-family: inherit;
color: $gray;
@include text-align(right);
&:focus {
border-bottom: 2px solid $black;
color: $black;
}
}
.action-search {
@extend %button-reset;
padding: ($baseline/4) ($baseline/2);
background-color: $gray-l3;
padding: ($baseline/5) ($baseline/2);
text-shadow: none;
vertical-align: middle;
.icon {
color: $gray-l3;
color: $white;
}
// STATE: hover and focus
&:hover,
&:focus {
background-color: $blue;
}
}
.icon {
color: $black;
}
.action-clear {
@include right(0);
@include margin(0, ($baseline/4), 0, 0);
@extend %button-reset;
position: absolute;
top: 0;
padding: ($baseline/4);
color: $gray-l3;
// STATE: hover and focus
&:hover,
&:focus {
color: $black;
}
}
}
......@@ -252,6 +268,11 @@
@include margin-right ($baseline*.75);
color: $gray-d1;
abbr {
border: 0;
text-decoration: none;
}
.icon {
@include margin-right ($baseline/4);
}
......@@ -325,10 +346,6 @@
&.has-pennant {
.wrapper-card-core {
padding-top: ($baseline*2);
}
.pennant {
@extend %t-copy-sub2;
@extend %t-strong;
......
......@@ -12,5 +12,5 @@
<h2 class="page-title"><%- title %></h2>
<p class="page-description"><%- description %></p>
</div>
<div class="header-action-view"></div>
<div class="page-header-secondary"></div>
</header>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment