diff --git a/common/static/common/js/components/collections/paging_collection.js b/common/static/common/js/components/collections/paging_collection.js index 4ac4bf93b3..4bf4ff3066 100644 --- a/common/static/common/js/components/collections/paging_collection.js +++ b/common/static/common/js/components/collections/paging_collection.js @@ -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: { diff --git a/common/static/common/js/components/views/paging_header.js b/common/static/common/js/components/views/paging_header.js index 8cd01bb9a1..c49af8bc4b 100644 --- a/common/static/common/js/components/views/paging_header.js +++ b/common/static/common/js/components/views/paging_header.js @@ -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; diff --git a/common/static/common/js/components/views/search_field.js b/common/static/common/js/components/views/search_field.js new file mode 100644 index 0000000000..7599edee35 --- /dev/null +++ b/common/static/common/js/components/views/search_field.js @@ -0,0 +1,71 @@ +/** + * 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); diff --git a/common/static/common/js/spec/components/paging_collection_spec.js b/common/static/common/js/spec/components/paging_collection_spec.js index 89062d24d2..0d5f668e97 100644 --- a/common/static/common/js/spec/components/paging_collection_spec.js +++ b/common/static/common/js/spec/components/paging_collection_spec.js @@ -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); } diff --git a/common/static/common/js/spec/components/search_field_spec.js b/common/static/common/js/spec/components/search_field_spec.js new file mode 100644 index 0000000000..3464ef1036 --- /dev/null +++ b/common/static/common/js/spec/components/search_field_spec.js @@ -0,0 +1,105 @@ +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(''); + }); + + 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'); + }); + }); +}); diff --git a/common/static/common/js/spec_helpers/ajax_helpers.js b/common/static/common/js/spec_helpers/ajax_helpers.js index 7f0ce09ecc..e699805512 100644 --- a/common/static/common/js/spec_helpers/ajax_helpers.js +++ b/common/static/common/js/spec_helpers/ajax_helpers.js @@ -1,7 +1,7 @@ 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 }; }); diff --git a/common/static/common/templates/components/search-field.underscore b/common/static/common/templates/components/search-field.underscore new file mode 100644 index 0000000000..aac29f640d --- /dev/null +++ b/common/static/common/templates/components/search-field.underscore @@ -0,0 +1,12 @@ + diff --git a/common/static/js/spec/main_requirejs.js b/common/static/js/spec/main_requirejs.js index ac29c68b3c..24b00f2185 100644 --- a/common/static/js/spec/main_requirejs.js +++ b/common/static/js/spec/main_requirejs.js @@ -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); diff --git a/common/test/acceptance/pages/lms/teams.py b/common/test/acceptance/pages/lms/teams.py index 351e091b6b..9ad67de7c8 100644 --- a/common/test/acceptance/pages/lms/teams.py +++ b/common/test/acceptance/pages/lms/teams.py @@ -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): """ diff --git a/common/test/acceptance/tests/lms/test_teams.py b/common/test/acceptance/tests/lms/test_teams.py index 0315311254..11366b142b 100644 --- a/common/test/acceptance/tests/lms/test_teams.py +++ b/common/test/acceptance/tests/lms/test_teams.py @@ -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']) ) diff --git a/lms/djangoapps/teams/management/commands/reindex_course_team.py b/lms/djangoapps/teams/management/commands/reindex_course_team.py index 7bf21a9f2f..901789180f 100644 --- a/lms/djangoapps/teams/management/commands/reindex_course_team.py +++ b/lms/djangoapps/teams/management/commands/reindex_course_team.py @@ -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: ") - 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() diff --git a/lms/djangoapps/teams/management/commands/tests/test_reindex_course_team.py b/lms/djangoapps/teams/management/commands/tests/test_reindex_course_team.py index 885bf48188..63b7593bcd 100644 --- a/lms/djangoapps/teams/management/commands/tests/test_reindex_course_team.py +++ b/lms/djangoapps/teams/management/commands/tests/test_reindex_course_team.py @@ -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): diff --git a/lms/djangoapps/teams/search_indexes.py b/lms/djangoapps/teams/search_indexes.py index 22af7227d4..7532b48ac6 100644 --- a/lms/djangoapps/teams/search_indexes.py +++ b/lms/djangoapps/teams/search_indexes.py @@ -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 diff --git a/lms/djangoapps/teams/static/teams/js/collections/base.js b/lms/djangoapps/teams/static/teams/js/collections/base.js index 01410af938..9a11592a3b 100644 --- a/lms/djangoapps/teams/static/teams/js/collections/base.js +++ b/lms/djangoapps/teams/static/teams/js/collections/base.js @@ -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; diff --git a/lms/djangoapps/teams/static/teams/js/collections/team.js b/lms/djangoapps/teams/static/teams/js/collections/team.js index a67dbd1fd9..3b0fac856a 100644 --- a/lms/djangoapps/teams/static/teams/js/collections/team.js +++ b/lms/djangoapps/teams/static/teams/js/collections/team.js @@ -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 ); diff --git a/lms/djangoapps/teams/static/teams/js/collections/topic.js b/lms/djangoapps/teams/static/teams/js/collections/topic.js index b3b60849ec..b88392a838 100644 --- a/lms/djangoapps/teams/static/teams/js/collections/topic.js +++ b/lms/djangoapps/teams/static/teams/js/collections/topic.js @@ -25,7 +25,9 @@ }, onUpdate: function(event) { - this.isStale = this.isStale || event.action === 'create'; + if (event.action === 'create') { + this.isStale = true; + } }, model: TopicModel diff --git a/lms/djangoapps/teams/static/teams/js/spec/collections/topic_collection_spec.js b/lms/djangoapps/teams/static/teams/js/spec/collections/topic_collection_spec.js index ee929edf69..da43b583d6 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/collections/topic_collection_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/collections/topic_collection_spec.js @@ -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 () { diff --git a/lms/djangoapps/teams/static/teams/js/spec/teams_tab_factory_spec.js b/lms/djangoapps/teams/static/teams/js/spec/teams_tab_factory_spec.js index cab815b09a..c32a0827b5 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/teams_tab_factory_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/teams_tab_factory_spec.js @@ -1,24 +1,13 @@ -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() { diff --git a/lms/djangoapps/teams/static/teams/js/spec/views/edit_team_spec.js b/lms/djangoapps/teams/static/teams/js/spec/views/edit_team_spec.js index 97c959000c..843c5ce186 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/views/edit_team_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/views/edit_team_spec.js @@ -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); }); }); }); diff --git a/lms/djangoapps/teams/static/teams/js/spec/views/my_teams_spec.js b/lms/djangoapps/teams/static/teams/js/spec/views/my_teams_spec.js index 92367ef589..eb0ae73a1a 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/views/my_teams_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/views/my_teams_spec.js @@ -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, {}); diff --git a/lms/djangoapps/teams/static/teams/js/spec/views/team_profile_header_actions_spec.js b/lms/djangoapps/teams/static/teams/js/spec/views/team_profile_header_actions_spec.js index 5fc597c7f1..af509310ba 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/views/team_profile_header_actions_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/views/team_profile_header_actions_spec.js @@ -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); diff --git a/lms/djangoapps/teams/static/teams/js/spec/views/team_profile_spec.js b/lms/djangoapps/teams/static/teams/js/spec/views/team_profile_spec.js index d2582fcd01..8e295fb611 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/views/team_profile_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/views/team_profile_spec.js @@ -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'); diff --git a/lms/djangoapps/teams/static/teams/js/spec/views/teams_spec.js b/lms/djangoapps/teams/static/teams/js/spec/views/teams_spec.js index 7815235301..6809e4adba 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/views/teams_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/views/teams_spec.js @@ -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(); }; diff --git a/lms/djangoapps/teams/static/teams/js/spec/views/teams_tab_spec.js b/lms/djangoapps/teams/static/teams/js/spec/views/teams_tab_spec.js index 83fccf20c5..9d7f402335 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/views/teams_tab_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/views/teams_tab_spec.js @@ -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'); + }); + }); }); }); diff --git a/lms/djangoapps/teams/static/teams/js/spec/views/topic_teams_spec.js b/lms/djangoapps/teams/static/teams/js/spec/views/topic_teams_spec.js index 415ada244c..a67a5298dc 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/views/topic_teams_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/views/topic_teams_spec.js @@ -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' } diff --git a/lms/djangoapps/teams/static/teams/js/spec/views/topics_spec.js b/lms/djangoapps/teams/static/teams/js/spec/views/topics_spec.js index abf39839c4..049339675f 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/views/topics_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/views/topics_spec.js @@ -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: '' } ); }); diff --git a/lms/djangoapps/teams/static/teams/js/spec_helpers/team_spec_helpers.js b/lms/djangoapps/teams/static/teams/js/spec_helpers/team_spec_helpers.js index 08d3ada6ba..2c51ef5ae2 100644 --- a/lms/djangoapps/teams/static/teams/js/spec_helpers/team_spec_helpers.js +++ b/lms/djangoapps/teams/static/teams/js/spec_helpers/team_spec_helpers.js @@ -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, diff --git a/lms/djangoapps/teams/static/teams/js/teams_tab_factory.js b/lms/djangoapps/teams/static/teams/js/teams_tab_factory.js index 441e1e11f6..d736d4da99 100644 --- a/lms/djangoapps/teams/static/teams/js/teams_tab_factory.js +++ b/lms/djangoapps/teams/static/teams/js/teams_tab_factory.js @@ -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(); }; }); diff --git a/lms/djangoapps/teams/static/teams/js/views/edit_team.js b/lms/djangoapps/teams/static/teams/js/views/edit_team.js index 34f20d6e0f..fc6ca4616e 100644 --- a/lms/djangoapps/teams/static/teams/js/views/edit_team.js +++ b/lms/djangoapps/teams/static/teams/js/views/edit_team.js @@ -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}); } diff --git a/lms/djangoapps/teams/static/teams/js/views/team_profile.js b/lms/djangoapps/teams/static/teams/js/views/team_profile.js index 10beac0a7e..0f49d7c934 100644 --- a/lms/djangoapps/teams/static/teams/js/views/team_profile.js +++ b/lms/djangoapps/teams/static/teams/js/views/team_profile.js @@ -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() { diff --git a/lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js b/lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js index 292ee796f3..e05783ca71 100644 --- a/lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js +++ b/lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js @@ -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} + ); } }); }); diff --git a/lms/djangoapps/teams/static/teams/js/views/teams.js b/lms/djangoapps/teams/static/teams/js/views/teams.js index 697dae049a..be8d8b14f6 100644 --- a/lms/djangoapps/teams/static/teams/js/views/teams.js +++ b/lms/djangoapps/teams/static/teams/js/views/teams.js @@ -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); } diff --git a/lms/djangoapps/teams/static/teams/js/views/teams_tab.js b/lms/djangoapps/teams/static/teams/js/views/teams_tab.js index 461159ecc3..309082bbc9 100644 --- a/lms/djangoapps/teams/static/teams/js/views/teams_tab.js +++ b/lms/djangoapps/teams/static/teams/js/views/teams_tab.js @@ -4,6 +4,7 @@ define(['backbone', 'underscore', 'gettext', + 'common/js/components/views/search_field', 'js/components/header/views/header', 'js/components/header/models/header', 'js/components/tabbed/views/tabbed_view', @@ -19,12 +20,12 @@ 'teams/js/views/edit_team', 'teams/js/views/team_profile_header_actions', 'text!teams/templates/teams_tab.underscore'], - function (Backbone, _, gettext, HeaderView, HeaderModel, TabbedView, + function (Backbone, _, gettext, SearchFieldView, HeaderView, HeaderModel, TabbedView, TopicModel, TopicCollection, TeamModel, TeamCollection, TeamMembershipCollection, TopicsView, TeamProfileView, MyTeamsView, TopicTeamsView, TeamEditView, TeamProfileHeaderActionsView, teamsTemplate) { var TeamsHeaderModel = HeaderModel.extend({ - initialize: function (attributes) { + initialize: function () { _.extend(this.defaults, {nav_aria_label: gettext('teams')}); HeaderModel.prototype.initialize.call(this); } @@ -48,18 +49,7 @@ var TeamTabView = Backbone.View.extend({ initialize: function(options) { var router; - this.courseID = options.courseID; - this.topics = options.topics; - this.topicUrl = options.topicUrl; - this.teamsUrl = options.teamsUrl; - this.teamsDetailUrl = options.teamsDetailUrl; - this.teamMembershipsUrl = options.teamMembershipsUrl; - this.teamMembershipDetailUrl = options.teamMembershipDetailUrl; - this.maxTeamSize = options.maxTeamSize; - this.languages = options.languages; - this.countries = options.countries; - this.userInfo = options.userInfo; - this.teamsBaseUrl = options.teamsBaseUrl; + this.context = options.context; // This slightly tedious approach is necessary // to use regular expressions within Backbone // routes, allowing us to capture which tab @@ -74,6 +64,7 @@ // being picked up by the backbone router. }, this)], ['topics/:topic_id(/)', _.bind(this.browseTopic, this)], + ['topics/:topic_id/search(/)', _.bind(this.searchTeams, this)], ['topics/:topic_id/create-team(/)', _.bind(this.newTeam, this)], ['topics/:topic_id/:team_id/edit-team(/)', _.bind(this.editTeam, this)], ['teams/:topic_id/:team_id(/)', _.bind(this.browseTeam, this)], @@ -87,14 +78,14 @@ this.teamEvents = _.clone(Backbone.Events); this.teamMemberships = new TeamMembershipCollection( - this.userInfo.team_memberships_data, + this.context.userInfo.team_memberships_data, { teamEvents: this.teamEvents, - url: this.teamMembershipsUrl, - course_id: this.courseID, - username: this.userInfo.username, - privileged: this.userInfo.privileged, - staff: this.userInfo.staff, + url: this.context.teamMembershipsUrl, + course_id: this.context.courseID, + username: this.context.userInfo.username, + privileged: this.context.userInfo.privileged, + staff: this.context.userInfo.staff, parse: true } ).bootstrap(); @@ -102,23 +93,17 @@ this.myTeamsView = new MyTeamsView({ router: this.router, teamEvents: this.teamEvents, + context: this.context, collection: this.teamMemberships, - teamMemberships: this.teamMemberships, - maxTeamSize: this.maxTeamSize, - teamParams: { - courseID: this.courseID, - teamsUrl: this.teamsUrl, - languages: this.languages, - countries: this.countries - } + teamMemberships: this.teamMemberships }); this.topicsCollection = new TopicCollection( - this.topics, + this.context.topics, { teamEvents: this.teamEvents, - url: options.topicsUrl, - course_id: this.courseID, + url: this.context.topicsUrl, + course_id: this.context.courseID, parse: true } ).bootstrap(); @@ -129,21 +114,19 @@ collection: this.topicsCollection }); - this.mainView = this.tabbedView = new ViewWithHeader({ - header: new HeaderView({ - model: new TeamsHeaderModel({ - description: gettext("See all teams in your course, organized by topic. Join a team to collaborate with other learners who are interested in the same topic as you are."), - title: gettext("Teams") - }) - }), - main: new TabbedView({ + this.mainView = this.tabbedView = this.createViewWithHeader({ + title: gettext("Teams"), + description: gettext("See all teams in your course, organized by topic. Join a team to collaborate with other learners who are interested in the same topic as you are."), + mainView: new TabbedView({ tabs: [{ title: gettext('My Team'), url: 'my-teams', view: this.myTeamsView }, { title: interpolate( - // Translators: sr_start and sr_end surround text meant only for screen readers. The whole string will be shown to users as "Browse teams" if they are using a screenreader, and "Browse" otherwise. + // Translators: sr_start and sr_end surround text meant only for screen readers. + // The whole string will be shown to users as "Browse teams" if they are using a + // screenreader, and "Browse" otherwise. gettext("Browse %(sr_start)s teams %(sr_end)s"), {"sr_start": '', "sr_end": ''}, true ), @@ -190,36 +173,50 @@ }); }, + /** + * Show the search results for a team. + */ + searchTeams: function (topicID) { + var view = this; + if (!this.teamsCollection) { + this.router.navigate('topics/' + topicID, {trigger: true}); + } else { + this.getTopic(topicID).done(function (topic) { + view.mainView = view.createTeamsListView({ + topic: topic, + collection: view.teamsCollection, + title: gettext('Team Search'), + description: interpolate( + gettext('Showing results for "%(searchString)s"'), + { searchString: view.teamsCollection.searchString }, + true + ), + breadcrumbs: view.createBreadcrumbs(topic), + showSortControls: false + }); + view.render(); + }); + } + }, + /** * Render the create new team form. */ newTeam: function (topicID) { - var self = this, - createViewWithHeader; + var view = this; this.getTopic(topicID).done(function (topic) { - var view = new TeamEditView({ - action: 'create', - teamEvents: self.teamEvents, - teamParams: { - courseID: self.courseID, - topicID: topic.get('id'), - teamsUrl: self.teamsUrl, - topicName: topic.get('name'), - languages: self.languages, - countries: self.countries, - teamsDetailUrl: self.teamsDetailUrl - } + view.mainView = view.createViewWithHeader({ + parentTopic: topic, + title: gettext("Create a New Team"), + description: gettext("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."), + mainView: new TeamEditView({ + action: 'create', + teamEvents: view.teamEvents, + context: view.context, + topic: topic + }) }); - createViewWithHeader = self.createViewWithHeader({ - mainView: view, - subject: { - name: gettext("Create a New Team"), - description: gettext("Create a new team if you can't find existing teams to join, or if you would like to learn with friends you know.") - }, - parentTopic: topic - }); - self.mainView = createViewWithHeader; - self.render(); + view.render(); }); }, @@ -234,27 +231,17 @@ var view = new TeamEditView({ action: 'edit', teamEvents: self.teamEvents, - teamParams: { - courseID: self.courseID, - topicID: topic.get('id'), - teamsUrl: self.teamsUrl, - topicName: topic.get('name'), - languages: self.languages, - countries: self.countries, - teamsDetailUrl: self.teamsDetailUrl - }, + context: self.context, + topic: topic, model: team }); editViewWithHeader = self.createViewWithHeader({ - mainView: view, - subject: { - name: gettext("Edit Team"), - description: gettext("If you make significant changes, make sure you notify members of the team before making these changes.") - }, - parentTeam: team, - parentTopic: topic - } - ); + title: gettext("Edit Team"), + description: gettext("If you make significant changes, make sure you notify members of the team before making these changes."), + mainView: view, + topic: topic, + team: team + }); self.mainView = editViewWithHeader; self.render(); }); @@ -267,54 +254,74 @@ getTeamsView: function (topicID) { // Lazily load the teams-for-topic view in // order to avoid making an extra AJAX call. - var self = this, - router = this.router, + var view = this, deferred = $.Deferred(); - if (this.teamsCollection && this.teamsCollection.topic_id === topicID && this.teamsView) { + if (this.teamsView && this.teamsCollection && this.teamsCollection.topic_id === topicID) { + this.teamsCollection.setSearchString(''); deferred.resolve(this.teamsView); } else { this.getTopic(topicID) .done(function(topic) { var collection = new TeamCollection([], { - teamEvents: self.teamEvents, - course_id: self.courseID, + teamEvents: view.teamEvents, + course_id: view.context.courseID, topic_id: topicID, - url: self.teamsUrl, + url: view.context.teamsUrl, per_page: 10 }); - self.teamsCollection = collection; + view.teamsCollection = collection; collection.goTo(1) .done(function() { - var teamsView = new TopicTeamsView({ - router: self.router, + var teamsView = view.createTeamsListView({ topic: topic, collection: collection, - teamMemberships: self.teamMemberships, - maxTeamSize: self.maxTeamSize, - teamParams: { - courseID: self.courseID, - topicID: topic.get('id'), - teamsUrl: self.teamsUrl, - topicName: topic.get('name'), - languages: self.languages, - countries: self.countries, - teamsDetailUrl: self.teamsDetailUrl - } + showSortControls: true }); - deferred.resolve( - self.createViewWithHeader( - { - mainView: teamsView, - subject: topic - } - ) - ); + deferred.resolve(teamsView); }); }); } return deferred.promise(); }, + createTeamsListView: function(options) { + var topic = options.topic, + collection = options.collection, + teamsView = new TopicTeamsView({ + router: this.router, + context: this.context, + model: topic, + collection: collection, + teamMemberships: this.teamMemberships, + showSortControls: options.showSortControls + }), + searchFieldView = new SearchFieldView({ + type: 'teams', + label: gettext('Search teams'), + collection: collection + }), + viewWithHeader = this.createViewWithHeader({ + subject: topic, + mainView: teamsView, + headerActionsView: searchFieldView, + title: options.title, + description: options.description, + breadcrumbs: options.breadcrumbs + }); + // Listen to requests to sync the collection and redirect it as follows: + // 1. If the collection includes a search, show the search results page + // 2. If not, then show the regular topic teams page + // Note: Backbone makes this a no-op if redirecting to the current page. + this.listenTo(collection, 'sync', function() { + if (collection.searchString) { + Backbone.history.navigate('topics/' + topic.get('id') + '/search', {trigger: true}); + } else { + Backbone.history.navigate('topics/' + topic.get('id'), {trigger: true}); + } + }); + return viewWithHeader; + }, + /** * Browse to the team with the specified team ID belonging to the specified topic. */ @@ -338,34 +345,23 @@ */ getBrowseTeamView: function (topicID, teamID) { var self = this, - deferred = $.Deferred(), - courseID = this.courseID; + deferred = $.Deferred(); self.getTopic(topicID).done(function(topic) { self.getTeam(teamID, true).done(function(team) { var view = new TeamProfileView({ teamEvents: self.teamEvents, router: self.router, - courseID: courseID, + context: self.context, model: team, - maxTeamSize: self.maxTeamSize, - isPrivileged: self.userInfo.privileged, - requestUsername: self.userInfo.username, - countries: self.countries, - languages: self.languages, - teamMembershipDetailUrl: self.teamMembershipDetailUrl, setFocusToHeaderFunc: self.setFocusToHeader }); var TeamProfileActionsView = new TeamProfileHeaderActionsView({ teamEvents: self.teamEvents, - courseID: courseID, + context: self.context, model: team, - teamsUrl: self.teamsUrl, - maxTeamSize: self.maxTeamSize, - currentUsername: self.userInfo.username, - teamMembershipsUrl: self.teamMembershipsUrl, - topicID: topicID, - showEditButton: self.userInfo.privileged || self.userInfo.staff + topic: topic, + showEditButton: self.context.userInfo.privileged || self.context.userInfo.staff }); deferred.resolve( self.createViewWithHeader( @@ -382,52 +378,55 @@ return deferred.promise(); }, - createViewWithHeader: function (options) { - var router = this.router, - breadcrumbs, headerView, - viewDescription, viewTitle; - breadcrumbs = [{ + createBreadcrumbs: function(topic, team) { + var breadcrumbs = [{ title: gettext('All Topics'), url: '#browse' }]; - if (options.parentTopic) { + if (topic) { breadcrumbs.push({ - title: options.parentTopic.get('name'), - url: '#topics/' + options.parentTopic.id + title: topic.get('name'), + url: '#topics/' + topic.id }); - } - if (options.parentTeam) { - breadcrumbs.push({ - title: options.parentTeam.get('name'), - url: '#teams/' + options.parentTopic.id + '/' + options.parentTeam.id - }); - } - if (options.subject instanceof Backbone.Model) { - viewDescription = options.subject.get('description'); - viewTitle = options.subject.get('name'); - - } else if (options.subject) { - viewDescription = options.subject.description; - viewTitle = options.subject.name; - } - - headerView = new HeaderView({ - model: new TeamsHeaderModel({ - description: viewDescription, - title: viewTitle, - breadcrumbs: breadcrumbs - }), - headerActionsView: options.headerActionsView, - events: { - 'click nav.breadcrumbs a.nav-item': function (event) { - var url = $(event.currentTarget).attr('href'); - event.preventDefault(); - router.navigate(url, {trigger: true}); - } + if (team) { + breadcrumbs.push({ + title: team.get('name'), + url: '#teams/' + topic.id + '/' + team.id + }); } + } + return breadcrumbs; + }, + + createHeaderModel: function(options) { + var subject = options.subject, + breadcrumbs = options.breadcrumbs, + title = options.title || subject.get('name'), + description = options.description || subject.get('description'); + if (!breadcrumbs) { + breadcrumbs = this.createBreadcrumbs(options.topic, options.team); + } + return new TeamsHeaderModel({ + breadcrumbs: breadcrumbs, + title: title, + description: description }); + }, + + createViewWithHeader: function(options) { + var router = this.router; return new ViewWithHeader({ - header: headerView, + header: new HeaderView({ + model: this.createHeaderModel(options), + headerActionsView: options.headerActionsView, + events: { + 'click nav.breadcrumbs a.nav-item': function (event) { + var url = $(event.currentTarget).attr('href'); + event.preventDefault(); + router.navigate(url, {trigger: true}); + } + } + }), main: options.mainView }); }, @@ -450,7 +449,7 @@ } else { topic = new TopicModel({ id: topicID, - url: self.topicUrl.replace('topic_id', topicID) + url: self.context.topicUrl.replace('topic_id', topicID) }); topic.fetch() .done(function() { @@ -476,7 +475,7 @@ var team = this.teamsCollection ? this.teamsCollection.get(teamID) : null, self = this, deferred = $.Deferred(), - teamUrl = this.teamsUrl + teamID + (expandUser ? '?expand=user': ''); + teamUrl = this.context.teamsUrl + teamID + (expandUser ? '?expand=user': ''); if (team) { team.url = teamUrl; deferred.resolve(team); @@ -570,11 +569,11 @@ * belongs to the team. */ readOnlyDiscussion: function (team) { - var self = this; + var userInfo = this.context.userInfo; return !( - self.userInfo.privileged || + userInfo.privileged || _.any(team.attributes.membership, function (membership) { - return membership.user.username === self.userInfo.username; + return membership.user.username === userInfo.username; }) ); } diff --git a/lms/djangoapps/teams/static/teams/js/views/topic_card.js b/lms/djangoapps/teams/static/teams/js/views/topic_card.js index c64ebcd619..b1d5292dde 100644 --- a/lms/djangoapps/teams/static/teams/js/views/topic_card.js +++ b/lms/djangoapps/teams/static/teams/js/views/topic_card.js @@ -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; }, diff --git a/lms/djangoapps/teams/static/teams/js/views/topic_teams.js b/lms/djangoapps/teams/static/teams/js/views/topic_teams.js index 26457b54f6..b545483d5d 100644 --- a/lms/djangoapps/teams/static/teams/js/views/topic_teams.js +++ b/lms/djangoapps/teams/static/teams/js/views/topic_teams.js @@ -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': '', - 'search_span_start': '', - 'create_span_start': '', - 'span_end': '' - } - ); - 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': '', + 'search_span_start': '', + 'create_span_start': '', + 'span_end': '' + } + ); + 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 }); } }); diff --git a/lms/djangoapps/teams/views.py b/lms/djangoapps/teams/views.py index 1e0b392ef7..325a076900 100644 --- a/lms/djangoapps/teams/views.py +++ b/lms/djangoapps/teams/views.py @@ -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, ) diff --git a/lms/envs/aws.py b/lms/envs/aws.py index 50d593591b..77b2779eda 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -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" diff --git a/lms/envs/common.py b/lms/envs/common.py index 0fe7508b63..86d0a47ce7 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -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, diff --git a/lms/envs/test.py b/lms/envs/test.py index f237a9af23..77f9282572 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -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') diff --git a/lms/static/js/components/header/views/header.js b/lms/static/js/components/header/views/header.js index 73910aefb1..1686040c27 100644 --- a/lms/static/js/components/header/views/header.js +++ b/lms/static/js/components/header/views/header.js @@ -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; } diff --git a/lms/static/sass/views/_teams.scss b/lms/static/sass/views/_teams.scss index fee03e9455..393ae96873 100644 --- a/lms/static/sass/views/_teams.scss +++ b/lms/static/sass/views/_teams.scss @@ -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 { + display: inline-block; + width: flex-grid(4,12); + @include text-align(right); + vertical-align: text-bottom; + } } // ui bits @@ -104,8 +105,10 @@ .action-search { @extend %button-reset; - padding: ($baseline/4) ($baseline/2); - + padding: ($baseline/5) ($baseline/2); + vertical-align: middle; + background-color: $gray-l3; + text-shadow: none; .icon { color: $gray-l3; diff --git a/lms/templates/components/header/header.underscore b/lms/templates/components/header/header.underscore index 5fd91299da..4ab011b38f 100644 --- a/lms/templates/components/header/header.underscore +++ b/lms/templates/components/header/header.underscore @@ -12,5 +12,5 @@

<%- title %>

<%- description %>

-
+