Course Discovery feature using edx-search
This commit is contained in:
committed by
Dino Cikatic
parent
69e274151d
commit
7f633fc045
@@ -33,7 +33,7 @@ class CoursewareSearchPage(CoursePage):
|
||||
|
||||
def search_for_term(self, text):
|
||||
"""
|
||||
Search and return results
|
||||
Fill input and do search
|
||||
"""
|
||||
self.enter_search_term(text)
|
||||
self.search()
|
||||
|
||||
47
common/test/acceptance/pages/lms/discovery.py
Normal file
47
common/test/acceptance/pages/lms/discovery.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""
|
||||
Course discovery page.
|
||||
"""
|
||||
|
||||
from . import BASE_URL
|
||||
from bok_choy.page_object import PageObject
|
||||
|
||||
|
||||
class CourseDiscoveryPage(PageObject):
|
||||
"""
|
||||
Find courses page (main page of the LMS).
|
||||
"""
|
||||
|
||||
url = BASE_URL + "/courses"
|
||||
form = "#discovery-form"
|
||||
|
||||
def is_browser_on_page(self):
|
||||
return "Courses" in self.browser.title
|
||||
|
||||
@property
|
||||
def result_items(self):
|
||||
"""
|
||||
Return search result items.
|
||||
"""
|
||||
return self.q(css=".courses-listing-item")
|
||||
|
||||
@property
|
||||
def clear_button(self):
|
||||
"""
|
||||
Clear all button.
|
||||
"""
|
||||
return self.q(css="#clear-all-filters")
|
||||
|
||||
def search(self, string):
|
||||
"""
|
||||
Search and wait for ajax.
|
||||
"""
|
||||
self.q(css=self.form + ' input[type="text"]').fill(string)
|
||||
self.q(css=self.form + ' [type="submit"]').click()
|
||||
self.wait_for_ajax()
|
||||
|
||||
def clear_search(self):
|
||||
"""
|
||||
Clear search results.
|
||||
"""
|
||||
self.clear_button.click()
|
||||
self.wait_for_ajax()
|
||||
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
Test course discovery.
|
||||
"""
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
|
||||
from bok_choy.web_app_test import WebAppTest
|
||||
from ...pages.common.logout import LogoutPage
|
||||
from ...pages.studio.auto_auth import AutoAuthPage
|
||||
from ...pages.lms.discovery import CourseDiscoveryPage
|
||||
from ...fixtures.course import CourseFixture
|
||||
|
||||
|
||||
class CourseDiscoveryTest(WebAppTest):
|
||||
"""
|
||||
Test searching for courses.
|
||||
"""
|
||||
|
||||
STAFF_USERNAME = "STAFF_TESTER"
|
||||
STAFF_EMAIL = "staff101@example.com"
|
||||
TEST_INDEX_FILENAME = "test_root/index_file.dat"
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create course page and courses to find
|
||||
"""
|
||||
# create index file
|
||||
with open(self.TEST_INDEX_FILENAME, "w+") as index_file:
|
||||
json.dump({}, index_file)
|
||||
|
||||
self.addCleanup(os.remove, self.TEST_INDEX_FILENAME)
|
||||
|
||||
super(CourseDiscoveryTest, self).setUp()
|
||||
self.page = CourseDiscoveryPage(self.browser)
|
||||
|
||||
for i in range(10):
|
||||
org = self.unique_id
|
||||
number = unicode(i)
|
||||
run = "test_run"
|
||||
name = "test course"
|
||||
settings = {'enrollment_start': datetime.datetime(1970, 1, 1).isoformat()}
|
||||
CourseFixture(org, number, run, name, settings=settings).install()
|
||||
|
||||
for i in range(2):
|
||||
org = self.unique_id
|
||||
number = unicode(i)
|
||||
run = "test_run"
|
||||
name = "grass is always greener"
|
||||
CourseFixture(
|
||||
org,
|
||||
number,
|
||||
run,
|
||||
name,
|
||||
settings={
|
||||
'enrollment_start': datetime.datetime(1970, 1, 1).isoformat()
|
||||
}
|
||||
).install()
|
||||
|
||||
def _auto_auth(self, username, email, staff):
|
||||
"""
|
||||
Logout and login with given credentials.
|
||||
"""
|
||||
LogoutPage(self.browser).visit()
|
||||
AutoAuthPage(self.browser, username=username, email=email, staff=staff).visit()
|
||||
|
||||
def test_page_existence(self):
|
||||
"""
|
||||
Make sure that the page is accessible.
|
||||
"""
|
||||
self.page.visit()
|
||||
|
||||
def test_search(self):
|
||||
"""
|
||||
Make sure you can search for courses.
|
||||
"""
|
||||
self.page.visit()
|
||||
self.assertEqual(len(self.page.result_items), 12)
|
||||
|
||||
self.page.search("grass")
|
||||
self.assertEqual(len(self.page.result_items), 2)
|
||||
|
||||
self.page.clear_search()
|
||||
self.assertEqual(len(self.page.result_items), 12)
|
||||
@@ -200,6 +200,7 @@ class IndexPageCourseCardsSortingTests(ModuleStoreTestCase):
|
||||
|
||||
@patch('student.views.render_to_response', RENDER_MOCK)
|
||||
@patch('courseware.views.render_to_response', RENDER_MOCK)
|
||||
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_COURSE_DISCOVERY': False})
|
||||
def test_course_cards_sorted_by_default_sorting(self):
|
||||
response = self.client.get('/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@@ -225,6 +226,7 @@ class IndexPageCourseCardsSortingTests(ModuleStoreTestCase):
|
||||
@patch('student.views.render_to_response', RENDER_MOCK)
|
||||
@patch('courseware.views.render_to_response', RENDER_MOCK)
|
||||
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_COURSE_SORTING_BY_START_DATE': False})
|
||||
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_COURSE_DISCOVERY': False})
|
||||
def test_course_cards_sorted_by_start_date_disabled(self):
|
||||
response = self.client.get('/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@@ -116,15 +116,21 @@ def courses(request):
|
||||
"""
|
||||
Render "find courses" page. The course selection work is done in courseware.courses.
|
||||
"""
|
||||
courses = get_courses(request.user, request.META.get('HTTP_HOST'))
|
||||
courses_list = []
|
||||
course_discovery_meanings = getattr(settings, 'COURSE_DISCOVERY_MEANINGS', {})
|
||||
if not settings.FEATURES.get('ENABLE_COURSE_DISCOVERY'):
|
||||
courses_list = get_courses(request.user, request.META.get('HTTP_HOST'))
|
||||
|
||||
if microsite.get_value("ENABLE_COURSE_SORTING_BY_START_DATE",
|
||||
settings.FEATURES["ENABLE_COURSE_SORTING_BY_START_DATE"]):
|
||||
courses = sort_by_start_date(courses)
|
||||
else:
|
||||
courses = sort_by_announcement(courses)
|
||||
if microsite.get_value("ENABLE_COURSE_SORTING_BY_START_DATE",
|
||||
settings.FEATURES["ENABLE_COURSE_SORTING_BY_START_DATE"]):
|
||||
courses_list = sort_by_start_date(courses_list)
|
||||
else:
|
||||
courses_list = sort_by_announcement(courses_list)
|
||||
|
||||
return render_to_response("courseware/courses.html", {'courses': courses})
|
||||
return render_to_response(
|
||||
"courseware/courses.html",
|
||||
{'courses': courses_list, 'course_discovery_meanings': course_discovery_meanings}
|
||||
)
|
||||
|
||||
|
||||
def render_accordion(request, course, chapter, section, field_data_cache):
|
||||
|
||||
@@ -1303,6 +1303,8 @@ reverify_js = [
|
||||
|
||||
ccx_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'js/ccx/**/*.js'))
|
||||
|
||||
discovery_js = ['js/discovery/main.js']
|
||||
|
||||
|
||||
PIPELINE_CSS = {
|
||||
'style-vendor': {
|
||||
@@ -1504,7 +1506,11 @@ PIPELINE_JS = {
|
||||
},
|
||||
'footer_edx': {
|
||||
'source_filenames': ['js/footer-edx.js'],
|
||||
'output_filename': 'js/footer-edx.js',
|
||||
'output_filename': 'js/footer-edx.js'
|
||||
},
|
||||
'discovery': {
|
||||
'source_filenames': discovery_js,
|
||||
'output_filename': 'js/discovery.js'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -133,6 +133,22 @@ FEATURES['CERTIFICATES_HTML_VIEW'] = True
|
||||
|
||||
|
||||
########################## Course Discovery #######################
|
||||
from django.utils.translation import ugettext as _
|
||||
LANGUAGE_MAP = {'terms': {lang: display for lang, display in ALL_LANGUAGES}, 'name': _('Language')}
|
||||
COURSE_DISCOVERY_MEANINGS = {
|
||||
'org': {
|
||||
'name': _('Organization'),
|
||||
},
|
||||
'modes': {
|
||||
'name': _('Course Type'),
|
||||
'terms': {
|
||||
'honor': _('Honor'),
|
||||
'verified': _('Verified'),
|
||||
},
|
||||
},
|
||||
'language': LANGUAGE_MAP,
|
||||
}
|
||||
|
||||
FEATURES['ENABLE_COURSE_DISCOVERY'] = True
|
||||
FEATURES['COURSES_ARE_BROWSEABLE'] = True
|
||||
HOMEPAGE_COURSE_MAX = 9
|
||||
|
||||
70
lms/static/js/discovery/app.js
Normal file
70
lms/static/js/discovery/app.js
Normal file
@@ -0,0 +1,70 @@
|
||||
;(function (define) {
|
||||
|
||||
define(['backbone', 'course_discovery_meanings'], function(Backbone, meanings) {
|
||||
'use strict';
|
||||
|
||||
return function (Collection, Form, ResultListView, FilterBarView, FacetsBarView, searchQuery) {
|
||||
//facet types configuration - set default display names
|
||||
var facetsTypes = meanings;
|
||||
|
||||
var collection = new Collection([]);
|
||||
var results = new ResultListView({ collection: collection });
|
||||
var dispatcher = _.clone(Backbone.Events);
|
||||
var form = new Form();
|
||||
var filters = new FilterBarView();
|
||||
var facetsBarView = new FacetsBarView(facetsTypes);
|
||||
|
||||
dispatcher.listenTo(form, 'search', function (query) {
|
||||
form.showLoadingIndicator();
|
||||
filters.changeQueryFilter(query);
|
||||
});
|
||||
|
||||
dispatcher.listenTo(filters, 'search', function (searchTerm, facets) {
|
||||
collection.performSearch(searchTerm, facets);
|
||||
form.showLoadingIndicator();
|
||||
});
|
||||
|
||||
dispatcher.listenTo(filters, 'clear', function () {
|
||||
form.clearSearch();
|
||||
collection.performSearch();
|
||||
filters.hideClearAllButton();
|
||||
});
|
||||
|
||||
dispatcher.listenTo(results, 'next', function () {
|
||||
collection.loadNextPage();
|
||||
form.showLoadingIndicator();
|
||||
});
|
||||
|
||||
dispatcher.listenTo(collection, 'search', function () {
|
||||
if (collection.length > 0) {
|
||||
results.render();
|
||||
}
|
||||
else {
|
||||
form.showNotFoundMessage(collection.searchTerm);
|
||||
}
|
||||
facetsBarView.renderFacets(collection.facets);
|
||||
form.hideLoadingIndicator();
|
||||
});
|
||||
|
||||
dispatcher.listenTo(collection, 'next', function () {
|
||||
results.renderNext();
|
||||
form.hideLoadingIndicator();
|
||||
});
|
||||
|
||||
dispatcher.listenTo(collection, 'error', function () {
|
||||
form.showErrorMessage();
|
||||
form.hideLoadingIndicator();
|
||||
});
|
||||
|
||||
dispatcher.listenTo(facetsBarView, 'addFilter', function (data) {
|
||||
filters.addFilter(data);
|
||||
});
|
||||
|
||||
// kick off search on page refresh
|
||||
form.doSearch(searchQuery);
|
||||
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
})(define || RequireJS.define);
|
||||
108
lms/static/js/discovery/collection.js
Normal file
108
lms/static/js/discovery/collection.js
Normal file
@@ -0,0 +1,108 @@
|
||||
;(function (define) {
|
||||
|
||||
define([
|
||||
'backbone',
|
||||
'js/discovery/result'
|
||||
], function (Backbone, Result) {
|
||||
'use strict';
|
||||
|
||||
return Backbone.Collection.extend({
|
||||
|
||||
model: Result,
|
||||
pageSize: 20,
|
||||
totalCount: 0,
|
||||
latestModelsCount: 0,
|
||||
searchTerm: '',
|
||||
selectedFacets: {},
|
||||
facets: {},
|
||||
page: 0,
|
||||
url: '/search/course_discovery/',
|
||||
fetchXhr: null,
|
||||
|
||||
performSearch: function (searchTerm, facets) {
|
||||
this.fetchXhr && this.fetchXhr.abort();
|
||||
this.searchTerm = searchTerm || '';
|
||||
this.selectedFacets = facets || {};
|
||||
var data = this.preparePostData(0);
|
||||
this.resetState();
|
||||
this.fetchXhr = this.fetch({
|
||||
data: data,
|
||||
type: 'POST',
|
||||
success: function (self, xhr) {
|
||||
self.trigger('search');
|
||||
},
|
||||
error: function (self, xhr) {
|
||||
self.trigger('error');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
loadNextPage: function () {
|
||||
this.fetchXhr && this.fetchXhr.abort();
|
||||
var data = this.preparePostData(this.page + 1);
|
||||
this.fetchXhr = this.fetch({
|
||||
data: data,
|
||||
type: 'POST',
|
||||
success: function (self, xhr) {
|
||||
self.page += 1;
|
||||
self.trigger('next');
|
||||
},
|
||||
error: function (self, xhr) {
|
||||
self.trigger('error');
|
||||
},
|
||||
add: true,
|
||||
reset: false,
|
||||
remove: false
|
||||
});
|
||||
},
|
||||
|
||||
preparePostData: function(pageNumber) {
|
||||
var data = {
|
||||
search_string: this.searchTerm,
|
||||
page_size: this.pageSize,
|
||||
page_index: pageNumber
|
||||
};
|
||||
if(this.selectedFacets.length > 0) {
|
||||
this.selectedFacets.each(function(facet) {
|
||||
data[facet.get('type')] = facet.get('query');
|
||||
});
|
||||
}
|
||||
return data;
|
||||
},
|
||||
|
||||
parse: function(response) {
|
||||
var results = response['results'] || [];
|
||||
this.latestModelsCount = results.length;
|
||||
this.totalCount = response.total;
|
||||
if (typeof response.facets !== 'undefined') {
|
||||
this.facets = response.facets;
|
||||
}
|
||||
else {
|
||||
this.facets = [];
|
||||
}
|
||||
return _.map(results, function (result) {
|
||||
return result.data;
|
||||
});
|
||||
},
|
||||
|
||||
resetState: function () {
|
||||
this.reset();
|
||||
this.page = 0;
|
||||
this.totalCount = 0;
|
||||
this.latestModelsCount = 0;
|
||||
},
|
||||
|
||||
hasNextPage: function () {
|
||||
return this.totalCount - ((this.page + 1) * this.pageSize) > 0;
|
||||
},
|
||||
|
||||
latestModels: function () {
|
||||
return this.last(this.latestModelsCount);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
})(define || RequireJS.define);
|
||||
36
lms/static/js/discovery/facet_view.js
Normal file
36
lms/static/js/discovery/facet_view.js
Normal file
@@ -0,0 +1,36 @@
|
||||
;(function (define) {
|
||||
|
||||
define([
|
||||
'jquery',
|
||||
'underscore',
|
||||
'backbone',
|
||||
'gettext',
|
||||
], function ($, _, Backbone, gettext) {
|
||||
'use strict';
|
||||
|
||||
return Backbone.View.extend({
|
||||
|
||||
tagName: 'li',
|
||||
templateId: '#search_facet-tpl',
|
||||
className: '',
|
||||
|
||||
initialize: function () {
|
||||
this.tpl = _.template($(this.templateId).html());
|
||||
},
|
||||
|
||||
render: function (type, name, term, count) {
|
||||
this.$el.html(this.tpl({name: name, term: term, count: count}));
|
||||
this.$el.attr('data-facet', type);
|
||||
return this;
|
||||
},
|
||||
|
||||
remove: function() {
|
||||
this.stopListening();
|
||||
this.$el.remove();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
})(define || RequireJS.define);
|
||||
46
lms/static/js/discovery/facets_view.js
Normal file
46
lms/static/js/discovery/facets_view.js
Normal file
@@ -0,0 +1,46 @@
|
||||
;(function (define) {
|
||||
|
||||
define([
|
||||
'jquery',
|
||||
'underscore',
|
||||
'backbone',
|
||||
'gettext',
|
||||
], function ($, _, Backbone, gettext) {
|
||||
'use strict';
|
||||
|
||||
return Backbone.View.extend({
|
||||
|
||||
tagName: 'section',
|
||||
templateId: '#search_facets_section-tpl',
|
||||
className: '',
|
||||
total: 0,
|
||||
terms: {},
|
||||
other: 0,
|
||||
list: [],
|
||||
views: {},
|
||||
attributes: {'data-parent-element' : 'sidebar'},
|
||||
|
||||
initialize: function () {
|
||||
this.tpl = _.template($(this.templateId).html());
|
||||
},
|
||||
|
||||
render: function (facetName, displayName, facetStats) {
|
||||
this.$el.html(this.tpl({name: facetName, displayName: displayName, stats: facetStats}));
|
||||
this.$el.attr('data-facet', facetName);
|
||||
this.$views = this.$el.find('ul');
|
||||
return this;
|
||||
},
|
||||
|
||||
remove: function() {
|
||||
$.each(this.list, function(key, facet) {
|
||||
facet.remove();
|
||||
});
|
||||
this.stopListening();
|
||||
this.$el.remove();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
})(define || RequireJS.define);
|
||||
19
lms/static/js/discovery/filter.js
Normal file
19
lms/static/js/discovery/filter.js
Normal file
@@ -0,0 +1,19 @@
|
||||
;(function (define) {
|
||||
|
||||
define(['backbone'], function (Backbone) {
|
||||
'use strict';
|
||||
|
||||
return Backbone.Model.extend({
|
||||
defaults: {
|
||||
query: '',
|
||||
type: 'search_string'
|
||||
},
|
||||
|
||||
cleanModelView: function() {
|
||||
this.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
})(define || RequireJS.define);
|
||||
119
lms/static/js/discovery/filter_bar_view.js
Normal file
119
lms/static/js/discovery/filter_bar_view.js
Normal file
@@ -0,0 +1,119 @@
|
||||
;(function (define) {
|
||||
|
||||
define([
|
||||
'jquery',
|
||||
'underscore',
|
||||
'backbone',
|
||||
'gettext',
|
||||
'js/discovery/filters',
|
||||
'js/discovery/filter',
|
||||
'js/discovery/filter_view'
|
||||
], function ($, _, Backbone, gettext, FiltersCollection, Filter, FilterView) {
|
||||
'use strict';
|
||||
|
||||
return Backbone.View.extend({
|
||||
|
||||
el: '#filter-bar',
|
||||
|
||||
tagName: 'div',
|
||||
templateId: '#filter_bar-tpl',
|
||||
className: 'filters hidden',
|
||||
|
||||
events: {
|
||||
'click #clear-all-filters': 'clearAll',
|
||||
'click li .discovery-button': 'clearFilter'
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
this.collection = new FiltersCollection([]);
|
||||
this.tpl = _.template($(this.templateId).html());
|
||||
this.$el.html(this.tpl());
|
||||
this.hideClearAllButton();
|
||||
this.filtersList = this.$el.find('ul');
|
||||
},
|
||||
|
||||
render: function () {
|
||||
return this;
|
||||
},
|
||||
|
||||
changeQueryFilter: function(query) {
|
||||
var queryModel = this.collection.getQueryModel();
|
||||
if (typeof queryModel !== 'undefined') {
|
||||
this.collection.remove(queryModel);
|
||||
}
|
||||
|
||||
if (query) {
|
||||
var data = {query: query, type: 'search_string'};
|
||||
this.addFilter(data);
|
||||
}
|
||||
else {
|
||||
this.startSearch();
|
||||
}
|
||||
},
|
||||
|
||||
addFilter: function(data) {
|
||||
var currentfilter = this.collection.findWhere(data);
|
||||
if(typeof currentfilter === 'undefined') {
|
||||
var filter = new Filter(data);
|
||||
var filterView = new FilterView({model: filter});
|
||||
this.collection.add(filter);
|
||||
this.filtersList.append(filterView.render().el);
|
||||
this.trigger('search', this.getSearchTerm(), this.collection);
|
||||
if (this.$el.hasClass('hidden')) {
|
||||
this.showClearAllButton();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
clearFilter: function (event) {
|
||||
event.preventDefault();
|
||||
var $target = $(event.currentTarget);
|
||||
var clearModel = this.collection.findWhere({
|
||||
query: $target.data('value'),
|
||||
type: $target.data('type')
|
||||
});
|
||||
this.collection.remove(clearModel);
|
||||
this.startSearch();
|
||||
},
|
||||
|
||||
clearFilters: function() {
|
||||
this.collection.reset([]);
|
||||
this.filtersList.empty();
|
||||
},
|
||||
|
||||
clearAll: function(event) {
|
||||
event.preventDefault();
|
||||
this.clearFilters();
|
||||
this.trigger('clear');
|
||||
},
|
||||
|
||||
showClearAllButton: function () {
|
||||
this.$el.removeClass('hidden');
|
||||
},
|
||||
|
||||
hideClearAllButton: function() {
|
||||
this.$el.addClass('hidden');
|
||||
},
|
||||
|
||||
getSearchTerm: function() {
|
||||
var queryModel = this.collection.getQueryModel();
|
||||
if (typeof queryModel !== 'undefined') {
|
||||
return queryModel.get('query');
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
||||
startSearch: function() {
|
||||
if (this.collection.length === 0) {
|
||||
this.trigger('clear');
|
||||
}
|
||||
else {
|
||||
this.trigger('search', this.getSearchTerm(), this.collection);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
})(define || RequireJS.define);
|
||||
39
lms/static/js/discovery/filter_view.js
Normal file
39
lms/static/js/discovery/filter_view.js
Normal file
@@ -0,0 +1,39 @@
|
||||
;(function (define) {
|
||||
|
||||
define([
|
||||
'jquery',
|
||||
'underscore',
|
||||
'backbone',
|
||||
'gettext',
|
||||
], function ($, _, Backbone, gettext) {
|
||||
'use strict';
|
||||
|
||||
return Backbone.View.extend({
|
||||
|
||||
tagName: 'li',
|
||||
templateId: '#filter-tpl',
|
||||
className: 'active-filter',
|
||||
|
||||
initialize: function () {
|
||||
this.tpl = _.template($(this.templateId).html());
|
||||
this.listenTo(this.model, 'destroy', this.remove);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.className = this.model.get('type');
|
||||
var data = this.model.attributes;
|
||||
data.name = data.name || data.query;
|
||||
this.$el.html(this.tpl(data));
|
||||
return this;
|
||||
},
|
||||
|
||||
remove: function() {
|
||||
this.stopListening();
|
||||
this.$el.remove();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
})(define || RequireJS.define);
|
||||
30
lms/static/js/discovery/filters.js
Normal file
30
lms/static/js/discovery/filters.js
Normal file
@@ -0,0 +1,30 @@
|
||||
;(function (define) {
|
||||
|
||||
define([
|
||||
'backbone',
|
||||
'js/discovery/filter'
|
||||
], function (Backbone, Filter) {
|
||||
'use strict';
|
||||
|
||||
return Backbone.Collection.extend({
|
||||
|
||||
model: Filter,
|
||||
url: '',
|
||||
|
||||
initialize: function () {
|
||||
this.bind('remove', this.onModelRemoved, this);
|
||||
},
|
||||
|
||||
onModelRemoved: function (model, collection, options) {
|
||||
model.cleanModelView();
|
||||
},
|
||||
|
||||
getQueryModel: function() {
|
||||
return this.findWhere({'type': 'search_string'});
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
})(define || RequireJS.define);
|
||||
66
lms/static/js/discovery/form.js
Normal file
66
lms/static/js/discovery/form.js
Normal file
@@ -0,0 +1,66 @@
|
||||
;(function (define) {
|
||||
|
||||
define(['jquery', 'backbone'], function ($, Backbone) {
|
||||
'use strict';
|
||||
|
||||
return Backbone.View.extend({
|
||||
|
||||
el: '#discovery-form',
|
||||
events: {
|
||||
'submit form': 'submitForm',
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
this.$searchField = this.$el.find('input');
|
||||
this.$searchButton = this.$el.find('button');
|
||||
this.$message = this.$el.find('#discovery-message');
|
||||
this.$loadingIndicator = this.$el.find('#loading-indicator');
|
||||
},
|
||||
|
||||
submitForm: function (event) {
|
||||
event.preventDefault();
|
||||
this.doSearch();
|
||||
},
|
||||
|
||||
doSearch: function (term) {
|
||||
if (term) {
|
||||
this.$searchField.val(term);
|
||||
}
|
||||
else {
|
||||
term = this.$searchField.val();
|
||||
}
|
||||
this.trigger('search', $.trim(term));
|
||||
this.$message.empty();
|
||||
},
|
||||
|
||||
clearSearch: function () {
|
||||
this.$message.empty();
|
||||
this.$searchField.val('');
|
||||
},
|
||||
|
||||
showLoadingIndicator: function () {
|
||||
this.$message.empty();
|
||||
this.$loadingIndicator.removeClass('hidden');
|
||||
},
|
||||
|
||||
hideLoadingIndicator: function () {
|
||||
this.$loadingIndicator.addClass('hidden');
|
||||
},
|
||||
|
||||
showNotFoundMessage: function (term) {
|
||||
var msg = interpolate(
|
||||
gettext('We couldn\'t find any results for "%s".'),
|
||||
[_.escape(term)]
|
||||
);
|
||||
this.$message.html(msg);
|
||||
},
|
||||
|
||||
showErrorMessage: function () {
|
||||
this.$message.html(gettext('There was an error, try searching again.'));
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
})(define || RequireJS.define);
|
||||
22
lms/static/js/discovery/main.js
Normal file
22
lms/static/js/discovery/main.js
Normal file
@@ -0,0 +1,22 @@
|
||||
RequireJS.require([
|
||||
'jquery',
|
||||
'backbone',
|
||||
'js/discovery/app',
|
||||
'js/discovery/collection',
|
||||
'js/discovery/form',
|
||||
'js/discovery/result_list_view',
|
||||
'js/discovery/filter_bar_view',
|
||||
'js/discovery/search_facets_view'
|
||||
], function ($, Backbone, App, Collection, DiscoveryForm, ResultListView, FilterBarView, FacetsBarView) {
|
||||
'use strict';
|
||||
|
||||
var app = new App(
|
||||
Collection,
|
||||
DiscoveryForm,
|
||||
ResultListView,
|
||||
FilterBarView,
|
||||
FacetsBarView,
|
||||
getParameterByName('search_query')
|
||||
);
|
||||
|
||||
});
|
||||
26
lms/static/js/discovery/result.js
Normal file
26
lms/static/js/discovery/result.js
Normal file
@@ -0,0 +1,26 @@
|
||||
;(function (define) {
|
||||
|
||||
define(['backbone'], function (Backbone) {
|
||||
'use strict';
|
||||
|
||||
return Backbone.Model.extend({
|
||||
defaults: {
|
||||
modes: [],
|
||||
course: '',
|
||||
enrollment_start: '',
|
||||
number: '',
|
||||
content: {
|
||||
overview: '',
|
||||
display_name: '',
|
||||
number: ''
|
||||
},
|
||||
start: '',
|
||||
image_url: '',
|
||||
org: '',
|
||||
id: ''
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
})(define || RequireJS.define);
|
||||
50
lms/static/js/discovery/result_item_view.js
Normal file
50
lms/static/js/discovery/result_item_view.js
Normal file
@@ -0,0 +1,50 @@
|
||||
;(function (define) {
|
||||
|
||||
define([
|
||||
'jquery',
|
||||
'underscore',
|
||||
'backbone',
|
||||
'gettext',
|
||||
'date'
|
||||
], function ($, _, Backbone, gettext, Date) {
|
||||
'use strict';
|
||||
|
||||
function formatDate(date) {
|
||||
return dateUTC(date).toString('MMM dd, yyyy');
|
||||
}
|
||||
|
||||
// Return a date object using UTC time instead of local time
|
||||
function dateUTC(date) {
|
||||
return new Date(
|
||||
date.getUTCFullYear(),
|
||||
date.getUTCMonth(),
|
||||
date.getUTCDate(),
|
||||
date.getUTCHours(),
|
||||
date.getUTCMinutes(),
|
||||
date.getUTCSeconds()
|
||||
);
|
||||
}
|
||||
|
||||
return Backbone.View.extend({
|
||||
|
||||
tagName: 'li',
|
||||
templateId: '#result_item-tpl',
|
||||
className: 'courses-listing-item',
|
||||
|
||||
initialize: function () {
|
||||
this.tpl = _.template($(this.templateId).html());
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var data = _.clone(this.model.attributes);
|
||||
data.start = formatDate(new Date(data.start));
|
||||
data.enrollment_start = formatDate(new Date(data.enrollment_start));
|
||||
this.$el.html(this.tpl(data));
|
||||
return this;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
})(define || RequireJS.define);
|
||||
84
lms/static/js/discovery/result_list_view.js
Normal file
84
lms/static/js/discovery/result_list_view.js
Normal file
@@ -0,0 +1,84 @@
|
||||
;(function (define) {
|
||||
|
||||
define([
|
||||
'jquery',
|
||||
'underscore',
|
||||
'backbone',
|
||||
'gettext',
|
||||
'js/discovery/result_item_view'
|
||||
], function ($, _, Backbone, gettext, ResultItemView) {
|
||||
'use strict';
|
||||
|
||||
return Backbone.View.extend({
|
||||
|
||||
el: 'section.courses',
|
||||
$window: $(window),
|
||||
$document: $(document),
|
||||
|
||||
initialize: function () {
|
||||
this.$list = this.$el.find('.courses-listing');
|
||||
this.attachScrollHandler();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$list.empty();
|
||||
this.renderItems();
|
||||
return this;
|
||||
},
|
||||
|
||||
renderNext: function () {
|
||||
this.renderItems();
|
||||
this.isLoading = false;
|
||||
},
|
||||
|
||||
renderItems: function () {
|
||||
var latest = this.collection.latestModels();
|
||||
var items = latest.map(function (result) {
|
||||
var item = new ResultItemView({ model: result });
|
||||
return item.render().el;
|
||||
}, this);
|
||||
this.$list.append(items);
|
||||
},
|
||||
|
||||
attachScrollHandler: function () {
|
||||
this.nextScrollEvent = true;
|
||||
this.$window.on('scroll', this.scrollHandler.bind(this));
|
||||
},
|
||||
|
||||
scrollHandler: function () {
|
||||
if (this.nextScrollEvent) {
|
||||
setTimeout(this.throttledScrollHandler.bind(this), 400);
|
||||
this.nextScrollEvent = false;
|
||||
}
|
||||
},
|
||||
|
||||
throttledScrollHandler: function () {
|
||||
if (this.isNearBottom()) {
|
||||
this.scrolledToBottom();
|
||||
}
|
||||
this.nextScrollEvent = true;
|
||||
},
|
||||
|
||||
isNearBottom: function () {
|
||||
var scrollBottom = this.$window.scrollTop() + this.$window.height();
|
||||
var threshold = this.$document.height() - 200;
|
||||
return scrollBottom >= threshold;
|
||||
},
|
||||
|
||||
scrolledToBottom: function () {
|
||||
if (this.thereIsMore() && !this.isLoading) {
|
||||
this.trigger('next');
|
||||
this.isLoading = true;
|
||||
}
|
||||
},
|
||||
|
||||
thereIsMore: function () {
|
||||
return this.collection.hasNextPage();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
})(define || RequireJS.define);
|
||||
127
lms/static/js/discovery/search_facets_view.js
Normal file
127
lms/static/js/discovery/search_facets_view.js
Normal file
@@ -0,0 +1,127 @@
|
||||
;(function (define) {
|
||||
|
||||
define([
|
||||
'jquery',
|
||||
'underscore',
|
||||
'backbone',
|
||||
'gettext',
|
||||
'js/discovery/facets_view',
|
||||
'js/discovery/facet_view'
|
||||
], function ($, _, Backbone, gettext, FacetsView, FacetView) {
|
||||
'use strict';
|
||||
|
||||
return Backbone.View.extend({
|
||||
|
||||
el: '.search-facets',
|
||||
|
||||
tagName: 'div',
|
||||
templateId: '#search_facets_list-tpl',
|
||||
className: 'facets',
|
||||
facetsTypes: {},
|
||||
moreLessLinksTpl: '#more_less_links-tpl',
|
||||
|
||||
events: {
|
||||
'click li': 'addFacet',
|
||||
'click .show-less': 'collapse',
|
||||
'click .show-more': 'expand',
|
||||
},
|
||||
|
||||
initialize: function (facetsTypes) {
|
||||
if(facetsTypes) {
|
||||
this.facetsTypes = facetsTypes;
|
||||
}
|
||||
this.tpl = _.template($(this.templateId).html());
|
||||
this.moreLessTpl = _.template($(this.moreLessLinksTpl).html());
|
||||
this.$el.html(this.tpl());
|
||||
this.facetViews = [];
|
||||
this.$facetViewsEl = this.$el.find('.search-facets-lists');
|
||||
},
|
||||
|
||||
render: function () {
|
||||
return this;
|
||||
},
|
||||
|
||||
collapse: function(event) {
|
||||
var $el = $(event.currentTarget),
|
||||
$more = $el.siblings('.show-more'),
|
||||
$ul = $el.parent('div').siblings('ul');
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
$ul.addClass('collapse');
|
||||
$el.addClass('hidden');
|
||||
$more.removeClass('hidden');
|
||||
},
|
||||
|
||||
expand: function(event) {
|
||||
var $el = $(event.currentTarget),
|
||||
$ul = $el.parent('div').siblings('ul'),
|
||||
facets = $ul.find('li').length,
|
||||
itemHeight = 34;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
$el.addClass('hidden');
|
||||
$ul.removeClass('collapse');
|
||||
$el.siblings('.show-less').removeClass('hidden');
|
||||
},
|
||||
|
||||
addFacet: function(event) {
|
||||
event.preventDefault();
|
||||
var $target = $(event.currentTarget);
|
||||
var value = $target.find('.facet-option').data('value');
|
||||
var name = $target.find('.facet-option').data('text');
|
||||
var data = {type: $target.data('facet'), query: value, name: name};
|
||||
this.trigger('addFilter', data);
|
||||
},
|
||||
|
||||
displayName: function(name, term){
|
||||
if(this.facetsTypes.hasOwnProperty(name)) {
|
||||
if(term) {
|
||||
if (typeof this.facetsTypes[name].terms !== 'undefined') {
|
||||
return this.facetsTypes[name].terms.hasOwnProperty(term) ? this.facetsTypes[name].terms[term] : term;
|
||||
}
|
||||
else {
|
||||
return term;
|
||||
}
|
||||
}
|
||||
else if(this.facetsTypes[name].hasOwnProperty('name')) {
|
||||
return this.facetsTypes[name]['name'];
|
||||
}
|
||||
else {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
else{
|
||||
return term ? term : name;
|
||||
}
|
||||
},
|
||||
|
||||
renderFacets: function(facets) {
|
||||
var self = this;
|
||||
// Remove old facets
|
||||
$.each(this.facetViews, function(key, facetsList) {
|
||||
facetsList.remove();
|
||||
});
|
||||
self.facetViews = [];
|
||||
// Render new facets
|
||||
$.each(facets, function(name, stats) {
|
||||
var facetsView = new FacetsView();
|
||||
self.facetViews.push(facetsView);
|
||||
self.$facetViewsEl.append(facetsView.render(name, self.displayName(name), stats).el);
|
||||
$.each(stats.terms, function(term, count) {
|
||||
var facetView = new FacetView();
|
||||
facetsView.$views.append(facetView.render(name, self.displayName(name, term), term, count).el);
|
||||
facetsView.list.push(facetView);
|
||||
});
|
||||
if(_.size(stats.terms) > 9) {
|
||||
facetsView.$el.append(self.moreLessTpl());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
})(define || RequireJS.define);
|
||||
26
lms/static/js/fixtures/discovery.html
Normal file
26
lms/static/js/fixtures/discovery.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<section class="courses-container">
|
||||
<div id="discovery-form">
|
||||
<form>
|
||||
<input class="discovery-input" placeholder="Search for a course" type="text"/><!-- removes spacing
|
||||
--><button type="submit" class="button postfix discovery-submit" aria-label="Search">
|
||||
<i class="icon fa fa-search" aria-hidden="true"></i>
|
||||
</button>
|
||||
</form>
|
||||
<div id="discovery-message"></div>
|
||||
<div aria-live="polite" aria-relevant="all">
|
||||
<div id="loading-indicator" class="hidden">
|
||||
<i class="icon fa fa-spinner fa-spin"></i> Loading
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="filter-bar" class="filters hide-phone">
|
||||
</div>
|
||||
|
||||
<section class="courses">
|
||||
<ul class="courses-listing"></ul>
|
||||
</section>
|
||||
|
||||
<aside aria-label="Refine your search" class="search-facets phone-menu">
|
||||
</aside>
|
||||
</section>
|
||||
19
lms/static/js/spec/discovery/course_discovery_meanings.js
Normal file
19
lms/static/js/spec/discovery/course_discovery_meanings.js
Normal file
@@ -0,0 +1,19 @@
|
||||
define({
|
||||
org: {
|
||||
name: 'Organization',
|
||||
terms: {
|
||||
edX1: "edX_1"
|
||||
}
|
||||
},
|
||||
modes: {
|
||||
name: 'Course Type',
|
||||
terms: {
|
||||
honor: 'Honor',
|
||||
verified: 'Verified'
|
||||
}
|
||||
},
|
||||
language: {
|
||||
en: 'English',
|
||||
hr: 'Croatian'
|
||||
}
|
||||
});
|
||||
595
lms/static/js/spec/discovery/discovery_spec.js
Normal file
595
lms/static/js/spec/discovery/discovery_spec.js
Normal file
@@ -0,0 +1,595 @@
|
||||
define([
|
||||
'jquery',
|
||||
'backbone',
|
||||
'logger',
|
||||
'js/common_helpers/ajax_helpers',
|
||||
'js/common_helpers/template_helpers',
|
||||
'js/discovery/app',
|
||||
'js/discovery/collection',
|
||||
'js/discovery/form',
|
||||
'js/discovery/result',
|
||||
'js/discovery/result_item_view',
|
||||
'js/discovery/result_list_view',
|
||||
'js/discovery/filter',
|
||||
'js/discovery/filters',
|
||||
'js/discovery/filter_bar_view',
|
||||
'js/discovery/filter_view',
|
||||
'js/discovery/search_facets_view',
|
||||
'js/discovery/facet_view',
|
||||
'js/discovery/facets_view'
|
||||
], function(
|
||||
$,
|
||||
Backbone,
|
||||
Logger,
|
||||
AjaxHelpers,
|
||||
TemplateHelpers,
|
||||
App,
|
||||
Collection,
|
||||
DiscoveryForm,
|
||||
ResultItem,
|
||||
ResultItemView,
|
||||
ResultListView,
|
||||
FilterModel,
|
||||
FiltersCollection,
|
||||
FiltersBarView,
|
||||
FilterView,
|
||||
SearchFacetView,
|
||||
FacetView,
|
||||
FacetsView
|
||||
) {
|
||||
'use strict';
|
||||
|
||||
var JSON_RESPONSE = {
|
||||
"total": 365,
|
||||
"results": [
|
||||
{
|
||||
"data": {
|
||||
"modes": [
|
||||
"honor"
|
||||
],
|
||||
"course": "edX/DemoX/Demo_Course",
|
||||
"enrollment_start": "2015-04-21T00:00:00+00:00",
|
||||
"number": "DemoX",
|
||||
"content": {
|
||||
"overview": " About This Course Include your long course description here.",
|
||||
"display_name": "edX Demonstration Course",
|
||||
"number": "DemoX"
|
||||
},
|
||||
"start": "1970-01-01T05:00:00+00:00",
|
||||
"image_url": "/c4x/edX/DemoX/asset/images_course_image.jpg",
|
||||
"org": "edX",
|
||||
"id": "edX/DemoX/Demo_Course"
|
||||
}
|
||||
}
|
||||
],
|
||||
"facets": {
|
||||
"org": {
|
||||
"total": 26,
|
||||
"terms": {
|
||||
"edX1": 1,
|
||||
"edX2": 1,
|
||||
"edX3": 1,
|
||||
"edX4": 1,
|
||||
"edX5": 1,
|
||||
"edX6": 1,
|
||||
"edX7": 1,
|
||||
"edX8": 1,
|
||||
"edX9": 1,
|
||||
"edX10": 1,
|
||||
"edX11": 1,
|
||||
"edX12": 1,
|
||||
"edX13": 1,
|
||||
"edX14": 1,
|
||||
"edX15": 1,
|
||||
"edX16": 1,
|
||||
"edX17": 1,
|
||||
"edX18": 1,
|
||||
"edX19": 1,
|
||||
"edX20": 1,
|
||||
"edX21": 1,
|
||||
"edX22": 1,
|
||||
"edX23": 1,
|
||||
"edX24": 1,
|
||||
"edX25": 1,
|
||||
"edX26": 1
|
||||
},
|
||||
"other": 0
|
||||
},
|
||||
"modes": {
|
||||
"total": 1,
|
||||
"terms": {
|
||||
"honor": 1
|
||||
},
|
||||
"other": 0
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var FACET_LIST = [
|
||||
{"type": "example1", "query": "search1"},
|
||||
{"type": "example2", "query": "search2"}
|
||||
];
|
||||
|
||||
var SEARCH_FILTER = {"type": "search_string", "query": "search3"};
|
||||
|
||||
|
||||
describe('Collection', function () {
|
||||
|
||||
beforeEach(function () {
|
||||
this.collection = new Collection();
|
||||
|
||||
this.onSearch = jasmine.createSpy('onSearch');
|
||||
this.collection.on('search', this.onSearch);
|
||||
|
||||
this.onNext = jasmine.createSpy('onNext');
|
||||
this.collection.on('next', this.onNext);
|
||||
|
||||
this.onError = jasmine.createSpy('onError');
|
||||
this.collection.on('error', this.onError);
|
||||
});
|
||||
|
||||
it('sends a request and parses the json result', function () {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
this.collection.performSearch('search string');
|
||||
AjaxHelpers.respondWithJson(requests, JSON_RESPONSE);
|
||||
expect(this.onSearch).toHaveBeenCalled();
|
||||
expect(this.collection.totalCount).toEqual(365);
|
||||
expect(this.collection.latestModels()[0].attributes).toEqual(JSON_RESPONSE.results[0].data);
|
||||
expect(this.collection.page).toEqual(0);
|
||||
});
|
||||
|
||||
it('handles errors', function () {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
this.collection.performSearch('search string');
|
||||
AjaxHelpers.respondWithError(requests);
|
||||
expect(this.onSearch).not.toHaveBeenCalled();
|
||||
expect(this.onError).toHaveBeenCalled();
|
||||
this.collection.loadNextPage();
|
||||
AjaxHelpers.respondWithError(requests);
|
||||
expect(this.onSearch).not.toHaveBeenCalled();
|
||||
expect(this.onError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('loads next page', function () {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
var response = { total: 35, results: [] };
|
||||
this.collection.loadNextPage();
|
||||
AjaxHelpers.respondWithJson(requests, response);
|
||||
expect(this.onNext).toHaveBeenCalled();
|
||||
expect(this.onError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sends correct paging parameters', function () {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
var response = { total: 52, results: [] };
|
||||
this.collection.performSearch('search string');
|
||||
AjaxHelpers.respondWithJson(requests, response);
|
||||
this.collection.loadNextPage();
|
||||
AjaxHelpers.respondWithJson(requests, response);
|
||||
spyOn($, 'ajax');
|
||||
this.collection.loadNextPage();
|
||||
expect($.ajax.mostRecentCall.args[0].url).toEqual(this.collection.url);
|
||||
expect($.ajax.mostRecentCall.args[0].data).toEqual({
|
||||
search_string : 'search string',
|
||||
page_size : this.collection.pageSize,
|
||||
page_index : 2
|
||||
});
|
||||
});
|
||||
|
||||
it('has next page', function () {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
var response = { total: 35, access_denied_count: 5, results: [] };
|
||||
this.collection.performSearch('search string');
|
||||
AjaxHelpers.respondWithJson(requests, response);
|
||||
expect(this.collection.hasNextPage()).toEqual(true);
|
||||
this.collection.loadNextPage();
|
||||
AjaxHelpers.respondWithJson(requests, response);
|
||||
expect(this.collection.hasNextPage()).toEqual(false);
|
||||
});
|
||||
|
||||
it('resets state when performing new search', function () {
|
||||
this.collection.add(new ResultItem());
|
||||
expect(this.collection.length).toEqual(1);
|
||||
this.collection.performSearch('search string');
|
||||
expect(this.collection.length).toEqual(0);
|
||||
expect(this.collection.page).toEqual(0);
|
||||
expect(this.collection.totalCount).toEqual(0);
|
||||
expect(this.collection.latestModelsCount).toEqual(0);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('ResultItem', function () {
|
||||
|
||||
beforeEach(function () {
|
||||
this.result = new ResultItem();
|
||||
});
|
||||
|
||||
it('has properties', function () {
|
||||
expect(this.result.get('modes')).toBeDefined();
|
||||
expect(this.result.get('course')).toBeDefined();
|
||||
expect(this.result.get('enrollment_start')).toBeDefined();
|
||||
expect(this.result.get('number')).toBeDefined();
|
||||
expect(this.result.get('content')).toEqual({
|
||||
display_name: '',
|
||||
number: '',
|
||||
overview: ''
|
||||
});
|
||||
expect(this.result.get('start')).toBeDefined();
|
||||
expect(this.result.get('image_url')).toBeDefined();
|
||||
expect(this.result.get('org')).toBeDefined();
|
||||
expect(this.result.get('id')).toBeDefined();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('ResultItemView', function () {
|
||||
|
||||
beforeEach(function () {
|
||||
TemplateHelpers.installTemplate('templates/discovery/result_item');
|
||||
this.item = new ResultItemView({
|
||||
model: new ResultItem(JSON_RESPONSE.results[0].data)
|
||||
});
|
||||
});
|
||||
|
||||
it('renders correctly', function () {
|
||||
var data = this.item.model.attributes;
|
||||
this.item.render();
|
||||
expect(this.item.$el).toContainHtml(data.content.display_name);
|
||||
expect(this.item.$el).toContain('a[href="/courses/' + data.course + '/info"]');
|
||||
expect(this.item.$el).toContain('img[src="' + data.image_url + '"]');
|
||||
expect(this.item.$el.find('.course-name')).toContainHtml(data.org);
|
||||
expect(this.item.$el.find('.course-name')).toContainHtml(data.content.number);
|
||||
expect(this.item.$el.find('.course-name')).toContainHtml(data.content.display_name);
|
||||
expect(this.item.$el.find('.course-date')).toContainHtml('Jan 01, 1970');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('DiscoveryForm', function () {
|
||||
|
||||
beforeEach(function () {
|
||||
loadFixtures('js/fixtures/discovery.html');
|
||||
this.form = new DiscoveryForm();
|
||||
this.onSearch = jasmine.createSpy('onSearch');
|
||||
this.form.on('search', this.onSearch);
|
||||
});
|
||||
|
||||
it('trims input string', function () {
|
||||
var term = ' search string ';
|
||||
$('.discovery-input').val(term);
|
||||
$('form').trigger('submit');
|
||||
expect(this.onSearch).toHaveBeenCalledWith($.trim(term));
|
||||
});
|
||||
|
||||
it('handles calls to doSearch', function () {
|
||||
var term = ' search string ';
|
||||
$('.discovery-input').val(term);
|
||||
this.form.doSearch(term);
|
||||
expect(this.onSearch).toHaveBeenCalledWith($.trim(term));
|
||||
expect($('.discovery-input').val()).toEqual(term);
|
||||
expect($('#discovery-message')).toBeEmpty();
|
||||
});
|
||||
|
||||
it('clears search', function () {
|
||||
$('.discovery-input').val('somethig');
|
||||
this.form.clearSearch();
|
||||
expect($('.discovery-input').val()).toEqual('');
|
||||
});
|
||||
|
||||
it('shows/hides loading indicator', function () {
|
||||
this.form.showLoadingIndicator();
|
||||
expect($('#loading-indicator')).not.toHaveClass('hidden');
|
||||
this.form.hideLoadingIndicator();
|
||||
expect($('#loading-indicator')).toHaveClass('hidden');
|
||||
});
|
||||
|
||||
it('shows messages', function () {
|
||||
this.form.showNotFoundMessage();
|
||||
expect($('#discovery-message')).not.toBeEmpty();
|
||||
this.form.showErrorMessage();
|
||||
expect($('#discovery-message')).not.toBeEmpty();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('FilterBarView', function () {
|
||||
beforeEach(function () {
|
||||
loadFixtures('js/fixtures/discovery.html');
|
||||
TemplateHelpers.installTemplates(
|
||||
['templates/discovery/filter_bar',
|
||||
'templates/discovery/filter']
|
||||
);
|
||||
this.filterBar = new FiltersBarView();
|
||||
this.onClear = jasmine.createSpy('onClear');
|
||||
this.filterBar.on('clear', this.onClear);
|
||||
});
|
||||
|
||||
it('view searches for sent facet object', function () {
|
||||
expect(this.filterBar.$el.length).toBe(1);
|
||||
this.filterBar.addFilter(FACET_LIST[0]);
|
||||
expect(this.filterBar.$el.find('#clear-all-filters')).toBeVisible();
|
||||
});
|
||||
|
||||
it('view searches for entered search string', function () {
|
||||
spyOn(this.filterBar, 'addFilter').andCallThrough();
|
||||
expect(this.filterBar.$el.length).toBe(1);
|
||||
this.filterBar.changeQueryFilter(SEARCH_FILTER.query);
|
||||
expect(this.filterBar.$el.find('#clear-all-filters')).toBeVisible();
|
||||
expect(this.filterBar.addFilter).toHaveBeenCalledWith(SEARCH_FILTER);
|
||||
});
|
||||
|
||||
it('model cleans view on destruction correctly', function () {
|
||||
this.filterBar.addFilter(SEARCH_FILTER);
|
||||
var model = this.filterBar.collection.findWhere(SEARCH_FILTER);
|
||||
expect(this.filterBar.$el.find('.active-filter').length).toBe(1);
|
||||
model.cleanModelView();
|
||||
expect(this.filterBar.$el.find('.active-filter').length).toBe(0);
|
||||
});
|
||||
|
||||
it('view removes all filters and hides bar if clear all', function () {
|
||||
spyOn(this.filterBar, 'clearAll').andCallThrough();
|
||||
this.filterBar.delegateEvents();
|
||||
this.filterBar.addFilter(SEARCH_FILTER);
|
||||
var clearAll = this.filterBar.$el.find('#clear-all-filters');
|
||||
expect(clearAll).toBeVisible();
|
||||
clearAll.trigger('click');
|
||||
expect(this.filterBar.clearAll).toHaveBeenCalled();
|
||||
expect(this.onClear).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('view hides bar if all filters removed', function () {
|
||||
spyOn(this.filterBar, 'clearFilter').andCallThrough();
|
||||
this.filterBar.delegateEvents();
|
||||
this.filterBar.addFilter(SEARCH_FILTER);
|
||||
var clearAll = this.filterBar.$el.find('#clear-all-filters');
|
||||
expect(clearAll).toBeVisible();
|
||||
var filter = this.filterBar.$el.find('li .discovery-button');
|
||||
filter.trigger('click');
|
||||
expect(this.filterBar.clearFilter).toHaveBeenCalled();
|
||||
expect(this.onClear).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('view changes query filter', function () {
|
||||
this.filterBar.addFilter(SEARCH_FILTER);
|
||||
var filter = $(this.filterBar.$el.find('li .discovery-button')[0]);
|
||||
expect(filter.text().trim()).toBe(SEARCH_FILTER.query);
|
||||
// Have to explicitly remove model because events not dispatched
|
||||
var model = this.filterBar.collection.findWhere(SEARCH_FILTER);
|
||||
model.cleanModelView();
|
||||
this.filterBar.changeQueryFilter(SEARCH_FILTER.query + '2');
|
||||
filter = $(this.filterBar.$el.find('li .discovery-button')[0]);
|
||||
expect(filter.text().trim()).toBe(SEARCH_FILTER.query + '2');
|
||||
});
|
||||
|
||||
it('view returns correct search term', function () {
|
||||
this.filterBar.addFilter(SEARCH_FILTER);
|
||||
expect(this.filterBar.getSearchTerm()).toBe(SEARCH_FILTER.query);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('SearchFacetView', function () {
|
||||
beforeEach(function () {
|
||||
loadFixtures('js/fixtures/discovery.html');
|
||||
TemplateHelpers.installTemplates([
|
||||
'templates/discovery/search_facet',
|
||||
'templates/discovery/search_facets_section',
|
||||
'templates/discovery/search_facets_list',
|
||||
'templates/discovery/more_less_links'
|
||||
]);
|
||||
var facetsTypes = {org: 'Organization', modes: 'Course Type'};
|
||||
this.searchFacetView = new SearchFacetView(facetsTypes);
|
||||
this.searchFacetView.renderFacets(JSON_RESPONSE.facets);
|
||||
this.onAddFilter = jasmine.createSpy('onAddFilter');
|
||||
this.searchFacetView.on('addFilter', this.onAddFilter);
|
||||
});
|
||||
|
||||
it('view expands more content on show more click', function () {
|
||||
var $showMore = this.searchFacetView.$el.find('.show-more');
|
||||
var $showLess = this.searchFacetView.$el.find('.show-less');
|
||||
var $ul = $showMore.parent('div').siblings('ul');
|
||||
expect($showMore).not.toHaveClass('hidden');
|
||||
expect($showLess).toHaveClass('hidden');
|
||||
expect($ul).toHaveClass('collapse');
|
||||
$showMore.trigger('click');
|
||||
expect($showMore).toHaveClass('hidden');
|
||||
expect($showLess).not.toHaveClass('hidden');
|
||||
expect($ul).not.toHaveClass('collapse');
|
||||
});
|
||||
|
||||
it('view collapses content on show less click', function () {
|
||||
var $showMore = this.searchFacetView.$el.find('.show-more');
|
||||
var $showLess = this.searchFacetView.$el.find('.show-less');
|
||||
var $ul = $showMore.parent('div').siblings('ul');
|
||||
$showMore.trigger('click');
|
||||
expect($showMore).toHaveClass('hidden');
|
||||
expect($showLess).not.toHaveClass('hidden');
|
||||
expect($ul).not.toHaveClass('collapse');
|
||||
$showLess.trigger('click');
|
||||
expect($showMore).not.toHaveClass('hidden');
|
||||
expect($showLess).toHaveClass('hidden');
|
||||
expect($ul).toHaveClass('collapse');
|
||||
});
|
||||
|
||||
it('view triggers addFilter event if facet is clicked', function () {
|
||||
this.searchFacetView.delegateEvents();
|
||||
var $facetLink = this.searchFacetView.$el.find('li [data-value="edX1"]');
|
||||
var $facet = $facetLink.parent('li');
|
||||
$facet.trigger('click');
|
||||
expect(this.onAddFilter).toHaveBeenCalledWith(
|
||||
{
|
||||
type: $facet.data('facet'),
|
||||
query: $facetLink.data('value'),
|
||||
name : $facetLink.data('text')
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('re-render facets on second click', function () {
|
||||
// First search
|
||||
this.searchFacetView.delegateEvents();
|
||||
this.searchFacetView.renderFacets(JSON_RESPONSE.facets);
|
||||
expect(this.searchFacetView.facetViews.length).toBe(2);
|
||||
// Setup spy
|
||||
var customView = this.searchFacetView.facetViews[0];
|
||||
spyOn(customView, 'remove').andCallThrough();
|
||||
// Second search
|
||||
this.searchFacetView.renderFacets(JSON_RESPONSE.facets);
|
||||
expect(this.searchFacetView.facetViews.length).toBe(2);
|
||||
expect(customView.remove).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('ResultListView', function () {
|
||||
|
||||
beforeEach(function () {
|
||||
jasmine.Clock.useMock();
|
||||
loadFixtures('js/fixtures/discovery.html');
|
||||
TemplateHelpers.installTemplate('templates/discovery/result_item');
|
||||
var collection = new Collection([JSON_RESPONSE.results[0].data]);
|
||||
collection.latestModelsCount = 1;
|
||||
this.view = new ResultListView({ collection: collection });
|
||||
});
|
||||
|
||||
it('renders search results', function () {
|
||||
this.view.render();
|
||||
expect($('.courses-listing article').length).toEqual(1);
|
||||
expect($('.courses-listing .course-title')).toContainHtml('edX Demonstration Course');
|
||||
this.view.renderNext();
|
||||
expect($('.courses-listing article').length).toEqual(2);
|
||||
});
|
||||
|
||||
it('scrolling triggers an event for next page', function () {
|
||||
this.onNext = jasmine.createSpy('onNext');
|
||||
this.view.on('next', this.onNext);
|
||||
spyOn(this.view.collection, 'hasNextPage').andCallFake(function () {
|
||||
return true;
|
||||
});
|
||||
this.view.render();
|
||||
window.scroll(0, $(document).height());
|
||||
$(window).trigger('scroll');
|
||||
jasmine.Clock.tick(500);
|
||||
expect(this.onNext).toHaveBeenCalled();
|
||||
|
||||
// should not be triggered again (while it is loading)
|
||||
$(window).trigger('scroll');
|
||||
jasmine.Clock.tick(500);
|
||||
expect(this.onNext.calls.length).toEqual(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('Discovery App', function () {
|
||||
|
||||
beforeEach(function () {
|
||||
loadFixtures('js/fixtures/discovery.html');
|
||||
TemplateHelpers.installTemplates([
|
||||
'templates/discovery/result_item',
|
||||
'templates/discovery/filter',
|
||||
'templates/discovery/filter_bar',
|
||||
'templates/discovery/search_facet',
|
||||
'templates/discovery/search_facets_section',
|
||||
'templates/discovery/search_facets_list',
|
||||
'templates/discovery/more_less_links'
|
||||
]);
|
||||
|
||||
this.app = new App(
|
||||
Collection,
|
||||
DiscoveryForm,
|
||||
ResultListView,
|
||||
FiltersBarView,
|
||||
SearchFacetView
|
||||
);
|
||||
});
|
||||
|
||||
it('performs search', function () {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
$('.discovery-input').val('test');
|
||||
$('.discovery-submit').trigger('click');
|
||||
AjaxHelpers.respondWithJson(requests, JSON_RESPONSE);
|
||||
expect($('.courses-listing article').length).toEqual(1);
|
||||
expect($('.courses-listing .course-title')).toContainHtml('edX Demonstration Course');
|
||||
expect($('.active-filter').length).toBe(1);
|
||||
});
|
||||
|
||||
it('loads more', function () {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
jasmine.Clock.useMock();
|
||||
$('.discovery-input').val('test');
|
||||
$('.discovery-submit').trigger('click');
|
||||
AjaxHelpers.respondWithJson(requests, JSON_RESPONSE);
|
||||
expect($('.courses-listing article').length).toEqual(1);
|
||||
expect($('.courses-listing .course-title')).toContainHtml('edX Demonstration Course');
|
||||
window.scroll(0, $(document).height());
|
||||
$(window).trigger('scroll');
|
||||
jasmine.Clock.tick(500);
|
||||
AjaxHelpers.respondWithJson(requests, JSON_RESPONSE);
|
||||
expect($('.courses-listing article').length).toEqual(2);
|
||||
});
|
||||
|
||||
it('displays not found message', function () {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
$('.discovery-input').val('asdfasdf');
|
||||
$('.discovery-submit').trigger('click');
|
||||
AjaxHelpers.respondWithJson(requests, {});
|
||||
expect($('#discovery-message')).not.toBeEmpty();
|
||||
expect($('.courses-listing')).toBeEmpty();
|
||||
});
|
||||
|
||||
it('displays error message', function () {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
$('.discovery-input').val('asdfasdf');
|
||||
$('.discovery-submit').trigger('click');
|
||||
AjaxHelpers.respondWithError(requests, 404);
|
||||
expect($('#discovery-message')).not.toBeEmpty();
|
||||
expect($('.courses-listing')).toBeEmpty();
|
||||
});
|
||||
|
||||
it('check filters and bar removed on clear all', function () {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
$('.discovery-input').val('test');
|
||||
$('.discovery-submit').trigger('click');
|
||||
AjaxHelpers.respondWithJson(requests, JSON_RESPONSE);
|
||||
expect($('.active-filter').length).toBe(1);
|
||||
expect($('#filter-bar')).not.toHaveClass('hidden');
|
||||
$('#clear-all-filters').trigger('click');
|
||||
expect($('.active-filter').length).toBe(0);
|
||||
expect($('#filter-bar')).toHaveClass('hidden');
|
||||
});
|
||||
|
||||
it('check filters and bar removed on last filter cleared', function () {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
$('.discovery-input').val('test');
|
||||
$('.discovery-submit').trigger('click');
|
||||
AjaxHelpers.respondWithJson(requests, JSON_RESPONSE);
|
||||
expect($('.active-filter').length).toBe(1);
|
||||
var $filter = $('.active-filter');
|
||||
$filter.find('.discovery-button').trigger('click');
|
||||
expect($('.active-filter').length).toBe(0);
|
||||
});
|
||||
|
||||
it('filter results by named facet', function () {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
$('.discovery-input').val('test');
|
||||
$('.discovery-submit').trigger('click');
|
||||
AjaxHelpers.respondWithJson(requests, JSON_RESPONSE);
|
||||
expect($('.active-filter').length).toBe(1);
|
||||
var $facetLink = $('.search-facets li [data-value="edX1"]');
|
||||
var $facet = $facetLink.parent('li');
|
||||
$facet.trigger('click');
|
||||
expect($('.active-filter').length).toBe(2);
|
||||
expect($('.active-filter [data-value="edX1"]').length).toBe(1);
|
||||
expect($('.active-filter [data-value="edX1"]').text().trim()).toBe("edX_1");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
@@ -95,7 +95,9 @@
|
||||
'js/student_profile/views/learner_profile_view': 'js/student_profile/views/learner_profile_view',
|
||||
|
||||
// edxnotes
|
||||
'annotator_1.2.9': 'xmodule_js/common_static/js/vendor/edxnotes/annotator-full.min'
|
||||
'annotator_1.2.9': 'xmodule_js/common_static/js/vendor/edxnotes/annotator-full.min',
|
||||
|
||||
'course_discovery_meanings': 'js/spec/discovery/course_discovery_meanings'
|
||||
},
|
||||
shim: {
|
||||
'gettext': {
|
||||
@@ -626,7 +628,8 @@
|
||||
'lms/include/js/spec/edxnotes/plugins/scroller_spec.js',
|
||||
'lms/include/js/spec/edxnotes/plugins/caret_navigation_spec.js',
|
||||
'lms/include/js/spec/edxnotes/collections/notes_spec.js',
|
||||
'lms/include/js/spec/search/search_spec.js'
|
||||
'lms/include/js/spec/search/search_spec.js',
|
||||
'lms/include/js/spec/discovery/discovery_spec.js'
|
||||
]);
|
||||
|
||||
}).call(this, requirejs, define);
|
||||
|
||||
@@ -91,6 +91,7 @@ fixture_paths:
|
||||
- js/fixtures/edxnotes
|
||||
- js/fixtures/search
|
||||
- templates/search
|
||||
- templates/discovery
|
||||
|
||||
requirejs:
|
||||
paths:
|
||||
|
||||
@@ -416,6 +416,12 @@ $homepage__header--gradient__color--alpha: lighten($gray, 15%);
|
||||
$homepage__header--gradient__color--bravo: saturate($gray, 30%);
|
||||
$homepage__header--background: lighten($gray, 15%);
|
||||
|
||||
// VIEWS: homepage and courses
|
||||
$course-card-height: ($baseline*18);
|
||||
$course-image-height: ($baseline*8);
|
||||
$course-info-height: ($baseline*10);
|
||||
$course-title-height: ($baseline*3.6);
|
||||
|
||||
// ====================
|
||||
|
||||
// IMAGES: backgrounds
|
||||
|
||||
@@ -1,7 +1,83 @@
|
||||
@import '../base/grid-settings';
|
||||
@import 'neat/neat'; // lib - Neat
|
||||
|
||||
$facet-text-color: #3d3e3f;
|
||||
$facet-background-color: #007db8;
|
||||
|
||||
.find-courses, .university-profile {
|
||||
background: $course-profile-bg;
|
||||
padding-bottom: ($baseline*3);
|
||||
|
||||
.discovery-button:not(:disabled) {
|
||||
@extend %t-action2;
|
||||
outline: 0 none;
|
||||
box-shadow:none;
|
||||
border: 0;
|
||||
background: none;
|
||||
padding: 0 ($baseline*0.6);
|
||||
text-align: left;
|
||||
text-decoration: none;
|
||||
text-shadow: none;
|
||||
text-transform: none;
|
||||
|
||||
//STATE: hover
|
||||
&::hover {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.courses-container {
|
||||
|
||||
#discovery-form {
|
||||
* {
|
||||
display:inline;
|
||||
}
|
||||
|
||||
#discovery-message,
|
||||
#loading-indicator {
|
||||
@include line-height(37.84);
|
||||
}
|
||||
}
|
||||
|
||||
.courses {
|
||||
@include rtl() { $layout-direction: "RTL"; }
|
||||
@include span-columns(9);
|
||||
|
||||
@include media($bp-medium) {
|
||||
@include span-columns(4);
|
||||
}
|
||||
|
||||
@include media($bp-large) {
|
||||
@include span-columns(8);
|
||||
}
|
||||
|
||||
@include media($bp-huge) {
|
||||
@include span-columns(9);
|
||||
}
|
||||
|
||||
.courses-listing .courses-listing-item {
|
||||
@include fill-parent();
|
||||
margin: ($baseline*0.75) 0 ($baseline*1.5) 0;
|
||||
max-height: $course-card-height;
|
||||
|
||||
@include media($bp-medium) {
|
||||
@include span-columns(8); // 4 of 8
|
||||
@include omega(1n);
|
||||
}
|
||||
|
||||
@include media($bp-large) {
|
||||
@include span-columns(6); // 6 of 12
|
||||
@include omega(2n);
|
||||
}
|
||||
|
||||
@include media($bp-huge) {
|
||||
@include span-columns(4); // 4 of 12
|
||||
@include omega(3n);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header.search {
|
||||
background: $course-profile-bg;
|
||||
background-size: cover;
|
||||
@@ -90,4 +166,251 @@
|
||||
padding-top: ($baseline*3);
|
||||
@include columns(2 20px);
|
||||
}
|
||||
|
||||
.discovery-input {
|
||||
@extend %ui-depth1;
|
||||
@extend %t-icon4;
|
||||
@extend %t-demi-strong;
|
||||
@include border-radius(0);
|
||||
@include border-top-left-radius(3px);
|
||||
@include border-bottom-left-radius(3px);
|
||||
border: 2px solid $gray-l3;
|
||||
height: $course-search-input-height;
|
||||
color: $black;
|
||||
font-style: normal;
|
||||
|
||||
//STATE: focus
|
||||
&:focus {
|
||||
@extend %no-outline;
|
||||
box-shadow: none;
|
||||
border-color: $m-blue-d1;
|
||||
}
|
||||
}
|
||||
|
||||
.discovery-submit {
|
||||
@extend %ui-depth2;
|
||||
@extend %t-icon3;
|
||||
@extend %t-strong;
|
||||
@include margin-left(-2px);
|
||||
position: relative;
|
||||
border: 2px solid $m-blue-d1;
|
||||
border-radius: ($baseline*0.1);
|
||||
box-shadow: none;
|
||||
background: $m-blue-d5;
|
||||
padding: 0 ($baseline*0.7);
|
||||
height: $course-search-input-height;
|
||||
color: $white;
|
||||
text-shadow: none;
|
||||
|
||||
//STATE: hover, focus
|
||||
&:hover, &:focus {
|
||||
background: $m-blue-l1;
|
||||
}
|
||||
}
|
||||
|
||||
.filters {
|
||||
@include clearfix();
|
||||
margin-top: ($baseline/2);
|
||||
border-top: 1px solid $courseware-button-border-color;
|
||||
border-bottom: 1px solid $courseware-button-border-color;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: ($baseline*10);
|
||||
|
||||
ul {
|
||||
@include padding-left(0);
|
||||
margin: 0;
|
||||
list-style: outside none none;
|
||||
}
|
||||
|
||||
li {
|
||||
@include float(left);
|
||||
@include margin(($baseline/2), $baseline, ($baseline/2), 0);
|
||||
position: relative;
|
||||
padding: ($baseline/2) ($baseline*0.75);
|
||||
background: $courseware-button-border-color;
|
||||
width: auto;
|
||||
|
||||
.facet-option {
|
||||
@extend %t-strong;
|
||||
color: $gray-d2;
|
||||
text-decoration: none;
|
||||
|
||||
i {
|
||||
color: $gray-l2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.clear-filters {
|
||||
@include line-height(29.73);
|
||||
@extend %t-icon5;
|
||||
@extend %t-strong;
|
||||
margin: ($baseline/2) 0;
|
||||
width: auto;
|
||||
text-align: center;
|
||||
color: $m-blue-d1;
|
||||
}
|
||||
|
||||
.flt-right {
|
||||
@include float(right);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.search-facets{
|
||||
@include fill-parent();
|
||||
@include omega();
|
||||
@include box-sizing(border-box);
|
||||
@extend %ui-depth1;
|
||||
position: relative;
|
||||
margin: ($baseline*2) 0 ($baseline*3.5) 0;
|
||||
box-shadow: 1px 2px 5px $black-t0;
|
||||
border-top: 1px solid $black;
|
||||
border-bottom: 2px solid $black;
|
||||
background-color: $white;
|
||||
max-height: ($baseline*100);
|
||||
|
||||
@include media($bp-tiny) {
|
||||
@include span-columns(4);
|
||||
}
|
||||
|
||||
@include media($bp-small) {
|
||||
@include span-columns(3);
|
||||
}
|
||||
|
||||
@include media($bp-medium) {
|
||||
@include span-columns(4);
|
||||
}
|
||||
|
||||
@include media($bp-large) {
|
||||
@include span-columns(4);
|
||||
}
|
||||
|
||||
@include media($bp-huge) {
|
||||
@include span-columns(3);
|
||||
}
|
||||
|
||||
&.phone-menu {
|
||||
border: medium none;
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
&:before {
|
||||
@include right(0);
|
||||
position: absolute;
|
||||
top: (-$baseline*0.15);
|
||||
opacity: 0;
|
||||
background-color: $white;
|
||||
padding: ($baseline*2) ($baseline*0.75) 0 ($baseline*0.75);
|
||||
width: ($baseline*2.5);
|
||||
height: ($baseline/4);
|
||||
content: "";
|
||||
}
|
||||
|
||||
h2,
|
||||
section {
|
||||
@extend %t-icon5;
|
||||
@extend %t-strong;
|
||||
margin: 0 ($baseline/2);
|
||||
border: medium none;
|
||||
padding: ($baseline/2);
|
||||
color: $facet-text-color;
|
||||
font-family: $sans-serif;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@extend %t-icon6;
|
||||
@extend %t-strong;
|
||||
margin: 0 ($baseline/2) ($baseline/2) ($baseline/2);
|
||||
color: $facet-text-color;
|
||||
font-family: $sans-serif;
|
||||
}
|
||||
|
||||
section {
|
||||
margin: 0;
|
||||
padding: ($baseline/2) 0;
|
||||
}
|
||||
|
||||
.facet-option {
|
||||
@include float(left);
|
||||
@include box-sizing(border-box);
|
||||
@include line-height(18.92);
|
||||
@include transition(all $tmg-f2 ease-out 0s);
|
||||
@extend %t-action3;
|
||||
opacity: 1;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: $facet-text-color;
|
||||
|
||||
//STATE: hover, visited
|
||||
&:hover,
|
||||
&:visited {
|
||||
color: $facet-text-color;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
list-style: outside none none;
|
||||
|
||||
&.collapse {
|
||||
max-height: ($baseline*14);
|
||||
}
|
||||
|
||||
li {
|
||||
@include clearfix();
|
||||
@include line-height(18.92);
|
||||
@extend %t-icon6;
|
||||
position: relative;
|
||||
clear: both;
|
||||
border-top: 1px solid $white;
|
||||
padding: 0;
|
||||
height: ($baseline*1.5);
|
||||
overflow: hidden;
|
||||
|
||||
.count {
|
||||
@include right($baseline*0.6);
|
||||
@include box-sizing(border-box);
|
||||
@include transition(all 0.2s ease-out);
|
||||
@include line-height(18.92);
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
//STATE: hover
|
||||
&:hover {
|
||||
background: $facet-background-color;
|
||||
color: $white;
|
||||
text-decoration: none;
|
||||
|
||||
.count,
|
||||
.facet-option {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-facets-lists section {
|
||||
border-top: 1px solid $courseware-button-border-color;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
@include clearfix();
|
||||
|
||||
button {
|
||||
@extend %t-icon6;
|
||||
@extend %t-strong;
|
||||
color: $facet-background-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
// lms - views - homepage view
|
||||
// ====================
|
||||
|
||||
$course-card-height: ($baseline*18);
|
||||
$course-image-height: ($baseline*8);
|
||||
$course-info-height: ($baseline*10);
|
||||
$course-title-height: ($baseline*3.6);
|
||||
$learn-more-horizontal-position: calc(50% - 100px); // calculate the left position for "LEARN MORE" content
|
||||
|
||||
.courses-container {
|
||||
@@ -13,30 +9,15 @@ $learn-more-horizontal-position: calc(50% - 100px); // calculate the left positi
|
||||
|
||||
.courses {
|
||||
@include row();
|
||||
@include float(left);
|
||||
width:100%;
|
||||
|
||||
.courses-listing {
|
||||
@extend %ui-no-list;
|
||||
|
||||
.courses-listing-item {
|
||||
@include rtl() { $layout-direction: "RTL"; }
|
||||
@include fill-parent();
|
||||
margin: ($baseline*0.75) 0 ($baseline*1.5) 0;
|
||||
max-height: $course-card-height;
|
||||
|
||||
@include media($bp-medium) {
|
||||
@include span-columns(4); // 4 of 8
|
||||
@include omega(2n);
|
||||
}
|
||||
|
||||
@include media($bp-large) {
|
||||
@include span-columns(4); // 4 of 12
|
||||
@include omega(3n);
|
||||
}
|
||||
|
||||
@include media($bp-huge) {
|
||||
@include span-columns(3); // 3 of 12
|
||||
@include omega(4n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,3 +143,26 @@ $learn-more-horizontal-position: calc(50% - 100px); // calculate the left positi
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Set homepage specific media queries */
|
||||
.home .courses-container .courses .courses-listing .courses-listing-item {
|
||||
|
||||
@include rtl() { $layout-direction: "RTL"; }
|
||||
@include fill-parent();
|
||||
|
||||
@include media($bp-medium) {
|
||||
@include span-columns(4); // 4 of 8
|
||||
@include omega(2n);
|
||||
}
|
||||
|
||||
@include media($bp-large) {
|
||||
@include span-columns(4); // 4 of 12
|
||||
@include omega(3n);
|
||||
}
|
||||
|
||||
@include media($bp-huge) {
|
||||
@include span-columns(3); // 3 of 12
|
||||
@include omega(4n);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<%!
|
||||
import json
|
||||
from django.utils.translation import ugettext as _
|
||||
from microsite_configuration import microsite
|
||||
%>
|
||||
@@ -6,6 +7,25 @@
|
||||
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
|
||||
<%block name="header_extras">
|
||||
% for template_name in ["result_item", "filter_bar", "filter", "search_facets_list", "search_facets_section", "search_facet", "more_less_links"]:
|
||||
<script type="text/template" id="${template_name}-tpl">
|
||||
<%static:include path="discovery/${template_name}.underscore" />
|
||||
</script>
|
||||
% endfor
|
||||
<script type="text/javascript">;(function (define) {{
|
||||
define('course_discovery_meanings', function() {{
|
||||
'use strict';
|
||||
return ${json.dumps(course_discovery_meanings)};
|
||||
}});
|
||||
}})(define || RequireJS.define);
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
<%block name="js_extra">
|
||||
<%static:js group='discovery'/>
|
||||
</%block>
|
||||
|
||||
<%block name="pagetitle">${_("Courses")}</%block>
|
||||
<%
|
||||
platform_name = microsite.get_value('platform_name', settings.PLATFORM_NAME)
|
||||
@@ -45,15 +65,38 @@
|
||||
</header>
|
||||
|
||||
<section class="courses-container">
|
||||
|
||||
<div id="discovery-form" role="search" aria-label="course">
|
||||
<form>
|
||||
<input class="discovery-input" placeholder="${_('Search for a course')}" type="text"/><!-- removes spacing
|
||||
--><button type="submit" class="button postfix discovery-submit" aria-label="${_('Search')}">
|
||||
<i class="icon fa fa-search" aria-hidden="true"></i>
|
||||
</button>
|
||||
</form>
|
||||
<div id="discovery-message"></div>
|
||||
<div aria-live="polite" aria-relevant="all">
|
||||
<div id="loading-indicator" class="hidden">
|
||||
<i class="icon fa fa-spinner fa-spin"></i> ${_('Loading')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="filter-bar" class="filters hide-phone">
|
||||
</div>
|
||||
|
||||
<section class="courses">
|
||||
<ul class="courses-listing">
|
||||
%for course in courses:
|
||||
%for course in courses:
|
||||
<li class="courses-listing-item">
|
||||
<%include file="../course.html" args="course=course" />
|
||||
</li>
|
||||
%endfor
|
||||
</ul>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
|
||||
<aside aria-label="${_('Refine your search')}" class="search-facets phone-menu">
|
||||
</aside>
|
||||
|
||||
</section>
|
||||
</section>
|
||||
|
||||
4
lms/templates/discovery/filter.underscore
Normal file
4
lms/templates/discovery/filter.underscore
Normal file
@@ -0,0 +1,4 @@
|
||||
<button data-value="<%- query %>" class="facet-option discovery-button" data-type="<%- type %>">
|
||||
<span class="query"><%- name %></span>
|
||||
<i aria-hidden="true" class="fa fa-times"></i>
|
||||
</button>
|
||||
5
lms/templates/discovery/filter_bar.underscore
Normal file
5
lms/templates/discovery/filter_bar.underscore
Normal file
@@ -0,0 +1,5 @@
|
||||
<ul class="active-filters facet-list">
|
||||
</ul>
|
||||
<span>
|
||||
<button id="clear-all-filters" class="clear-filters flt-right discovery-button"><%= gettext('CLEAR ALL') %></button>
|
||||
</span>
|
||||
8
lms/templates/discovery/more_less_links.underscore
Normal file
8
lms/templates/discovery/more_less_links.underscore
Normal file
@@ -0,0 +1,8 @@
|
||||
<div class="toggle ">
|
||||
<button class="show-more discovery-button">
|
||||
<%= gettext("MORE...") %>
|
||||
</button>
|
||||
<button class="show-less hidden discovery-button">
|
||||
<%= gettext("LESS...") %>
|
||||
</button>
|
||||
</div>
|
||||
25
lms/templates/discovery/result_item.underscore
Normal file
25
lms/templates/discovery/result_item.underscore
Normal file
@@ -0,0 +1,25 @@
|
||||
<article class="course" role="region" aria-label="<%= content.display_name %>">
|
||||
<a href="/courses/<%- course %>/info">
|
||||
<header class="course-image">
|
||||
<div class="cover-image">
|
||||
<img src="<%- image_url %>" alt="<%= content.display_name %> <%= content.number %>" />
|
||||
<div class="learn-more" aria-hidden=true><%= gettext("LEARN MORE") %></div>
|
||||
</div>
|
||||
</header>
|
||||
<section class="course-info" aria-hidden=true>
|
||||
<h2 class="course-name">
|
||||
<span class="course-organization"><%= org %></span>
|
||||
<span class="course-code"><%= content.number %></span>
|
||||
<span class="course-title"><%= content.display_name %></span>
|
||||
</h2>
|
||||
<div class="course-date" aria-hidden="true"><%= interpolate(gettext("Starts: %s"), [start]) %></div>
|
||||
</section>
|
||||
<div class="sr">
|
||||
<ul>
|
||||
<li><%= org %></li>
|
||||
<li><%= content.number %></li>
|
||||
<li><%= gettext("Starts") %><time itemprop="startDate" datetime="<%- start %>"><%- start %></time></li>
|
||||
</ul>
|
||||
</div>
|
||||
</a>
|
||||
</article>
|
||||
6
lms/templates/discovery/search_facet.underscore
Normal file
6
lms/templates/discovery/search_facet.underscore
Normal file
@@ -0,0 +1,6 @@
|
||||
<button data-value="<%= term %>" data-text="<%= name %>" class="facet-option discovery-button">
|
||||
<%= name %>
|
||||
<span class="count">
|
||||
<%= count %>
|
||||
</span>
|
||||
</button>
|
||||
5
lms/templates/discovery/search_facets_list.underscore
Normal file
5
lms/templates/discovery/search_facets_list.underscore
Normal file
@@ -0,0 +1,5 @@
|
||||
<h2>
|
||||
<%= gettext('Refine your search') %>
|
||||
</h2>
|
||||
<section class="search-facets-lists">
|
||||
</section>
|
||||
5
lms/templates/discovery/search_facets_section.underscore
Normal file
5
lms/templates/discovery/search_facets_section.underscore
Normal file
@@ -0,0 +1,5 @@
|
||||
<h3>
|
||||
<%= displayName %>
|
||||
</h3>
|
||||
<ul data-facet="<%= name %>" class="facet-list collapse">
|
||||
</ul>
|
||||
@@ -57,6 +57,7 @@
|
||||
</a>
|
||||
% endif
|
||||
</div>
|
||||
|
||||
</header>
|
||||
<section class="courses-container">
|
||||
<section class="highlighted-courses">
|
||||
|
||||
@@ -46,7 +46,7 @@ git+https://github.com/hmarr/django-debug-toolbar-mongo.git@b0686a76f1ce3532088c
|
||||
-e git+https://github.com/edx/edx-val.git@b1e11c9af3233bc06a17acbb33179f46d43c3b87#egg=edx-val
|
||||
-e git+https://github.com/pmitros/RecommenderXBlock.git@e1697b648bc347a65fc24265501355375ff739a2#egg=recommender-xblock
|
||||
-e git+https://github.com/edx/edx-milestones.git@547f2250ee49e73ce8d7ff4e78ecf1b049892510#egg=edx-milestones
|
||||
-e git+https://github.com/edx/edx-search.git@ae459ead41962c656ce794619f58cdae46eb7896#egg=edx-search
|
||||
-e git+https://github.com/edx/edx-search.git@e8b7c262adb500dbb0eced5434a26d9fa2d99dc3#egg=edx-search
|
||||
git+https://github.com/edx/edx-lint.git@8bf82a32ecb8598c415413df66f5232ab8d974e9#egg=edx_lint==0.2.1
|
||||
-e git+https://github.com/edx/xblock-utils.git@db22bc40fd2a75458a3c66d057f88aff5a7383e6#egg=xblock-utils
|
||||
-e git+https://github.com/edx-solutions/xblock-google-drive.git@138e6fa0bf3a2013e904a085b9fed77dab7f3f21#egg=xblock-google-drive
|
||||
|
||||
Reference in New Issue
Block a user