Commit 7f633fc0 by Martyn James Committed by Dino Cikatic

Course Discovery feature using edx-search

parent 69e27415
......@@ -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()
......
"""
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()
"""
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'))
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)
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_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
......
;(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);
;(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);
;(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);
;(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);
;(function (define) {
define(['backbone'], function (Backbone) {
'use strict';
return Backbone.Model.extend({
defaults: {
query: '',
type: 'search_string'
},
cleanModelView: function() {
this.destroy();
}
});
});
})(define || RequireJS.define);
;(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);
;(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);
;(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);
;(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);
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')
);
});
;(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);
;(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);
;(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);
;(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);
<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>
define({
org: {
name: 'Organization',
terms: {
edX1: "edX_1"
}
},
modes: {
name: 'Course Type',
terms: {
honor: 'Honor',
verified: 'Verified'
}
},
language: {
en: 'English',
hr: 'Croatian'
}
});
......@@ -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
......
@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;
}
}
}
}
// 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);
}
}
<%!
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>
<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>
<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>
<div class="toggle ">
<button class="show-more discovery-button">
<%= gettext("MORE...") %>
</button>
<button class="show-less hidden discovery-button">
<%= gettext("LESS...") %>
</button>
</div>
<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>
<button data-value="<%= term %>" data-text="<%= name %>" class="facet-option discovery-button">
<%= name %>
<span class="count">
<%= count %>
</span>
</button>
<h2>
<%= gettext('Refine your search') %>
</h2>
<section class="search-facets-lists">
</section>
<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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment