Repository URL to install this package:
|
Version:
5.21.0 ▾
|
## mako
<%!
from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse
%>
<%inherit file="/funsite/parts/base-fluid.html" />
<%block name="title">${_("Courses")}</%block>
<%namespace name="breadcrumbs" file="/funsite/parts/breadcrumbs.html"/>
<%namespace name='static' file='/static_content.html'/>
<%block name="js_extra">
<script src="${static.url('funsite/jquery/dist/jquery.min.js')}"></script>
<script src="${static.url('js/vendor/underscore-min.js')}"></script>
<script src="${static.url('js/vendor/backbone-min.js')}"></script>
<%def name="static_include(path)">
<%
from django.template.engine import Engine
from django.template.loaders.app_directories import Loader
engine = Engine(dirs=settings.DEFAULT_TEMPLATE_ENGINE['DIRS'])
source, template_path = Loader(engine).load_template_source(path)
%>
${source}
</%def>
% for template_name in ['small-course-block', 'criterion-value', 'filter']:
<script type="text/template" id="${template_name}-tpl">
${static_include("course_pages/js/" + template_name + ".underscore")}
</script>
% endfor
<script>
(function ($, _, Backbone) {
var loadTemplate = function(name) {
var templateSelector = '#' + name + '-tpl',
templateText = $(templateSelector).text();
if (!templateText) {
console.error('Failed to load ' + name + ' template');
}
return _.template(templateText);
};
var FILTERS = [
{
title: "${_('New courses')}", name: 'status', type: 'select', select: '#status-select',
values: [
{value: 'new', title: "${_('First session')}", count: ${courses_count_new}},
]
},
{
title: "${_('Availability')}", name: 'availability', type: 'select', select: '#availability-select',
values: [
{value: 'starting_soon', title: "${_('Starting soon')}", count: ${courses_count_starting_soon}},
{value: 'enrollment_ending_soon', title: "${_('Imminent end of enrollment')}", count: ${courses_count_enrollment_ending_soon}},
{value: 'opened', title: "${_('Open for enrollment')}", count: ${courses_count_opened}},
{value: 'started', title: "${_('Already started')}", count: ${courses_count_started}},
{value: 'browsable', title: "${_('Browsable')}", count: ${courses_count_browsable}},
{value: 'archived', title: "${_('Archived')}", count: ${courses_count_archived}},
]
},
{
title: '${_("Themes")}', name: 'subject', type: 'select', select: '#theme-select',
max: 8, all_button: "${_('View <%= count %> themes')}", overlay: '#all-themes-overlay',
values: [
%for course_subject in course_subjects:
{value: "${course_subject.slug}", title: "${course_subject.get_short_name()}", count: ${course_subject.public_courses_count}},
%endfor
]
},
{
title: "${_('Universities')}", name: 'university', type: 'select', select: '#university-select',
max: 10, all_button: "${_('View <%= count %> universities')}", overlay: '#all-universities-overlay',
values: [
%for university in universities:
{value: "${university.code}", title: "${university.get_short_name()}", count: ${university.public_courses_count}},
%endfor
]
},
/*
{title: '${_("Certified training")}', name: 'certificate', type: 'checkbox', values: [
{value: "certificat", title: "Certificate", count: 50 },
]},
*/
{
title: '${_("Language")}', name: 'language', type: 'select',
values: [
%for language in languages:
{value: "${language['language']}", title: "${language['title']}", image: "${language['language'] + '-flag'}", count: "${language['count']}"},
%endfor
]
},
];
var AppRouter = Backbone.Router.extend({
routes: {
"filter/:name/:slug?page=:page&rpp=:rpp&sort=:sortBy": "filterByAvailability",
"filter/:name/:slug?page=:page&rpp=:rpp": "filterByAvailability",
"filter/:name/:slug": "filterByAvailability",
"filter": "filterByAvailability",
"": "filterByAvailability",
"filter?page=:page&rpp=:rpp&sort=:sortBy": "sort",
"search?query=:query&page=:page&rpp=:rpp&sort=:sortBy": "search",
"search?query=:query&page=:page&rpp=:rpp": "search",
"search?query=:query": "search",
},
initialize: function(options) {
this.criterioncollection = options.criterioncollection;
this.filter = options.filter;
this.timeout_handle=null;
if (!window.location.hash) {
window.history.pushState('', '', '#filter/availability/starting_soon');
}
},
search: function(query, page, rpp, sortBy) {
/* the smaller the query the more we wait */
var delay = query.length > 4 ? 750 :( query.length > 1 ? 1500 : 3000 );
if (this.timeout_handle!==null) {
window.clearTimeout(this.timeout_handle);
}
var msearch = this._search;
/** JS trick this is overloaded in Timeout callbacks **/
var that= this;
this.timeout_handle = window.setTimeout(
function () { msearch(query, page, rpp, sortBy, that)
},delay);
},
_search: function(query, page, rpp, sortBy, that) {
var attributes = {
query: query,
selected: null,
};
var that;
if (rpp) {
attributes.resultsPerPage = parseInt(rpp);
}
if (page) {
attributes.currentPage = parseInt(page);
}
if (sortBy) {
attributes.sortBy = sortBy;
}
that.filter.set(attributes);
},
sort: function(page, rpp, sortBy) {
return this.filterByAvailability(null, null, page, rpp, sortBy);
},
filterByAvailability: function(name, slug, page, rpp, sortBy) {
var attributes = {
query: null,
}
if (name) {
var criterion = this.criterioncollection.findWhere({'name': name});
var value = criterion.getValue(slug);
var $select = $('select[id^="' + name + '"]');
$select.val(slug);
attributes.selected = value;
} else {
attributes.selected = null;
}
if (rpp) {
attributes.resultsPerPage = parseInt(rpp);
}
if (page) {
attributes.currentPage = parseInt(page);
}
if (sortBy) {
attributes.sortBy = sortBy;
}
this.filter.set(attributes);
},
});
// Value of a possible filter. E.g: Language=English, Availability=Ends-soon, etc.
var Value = Backbone.Model.extend({
criterionName: function() {
return this.collection.criterion.get("name");
}
});
var ValueCollection = Backbone.Collection.extend({
model: Value,
initialize: function(models, options) {
this.criterion = options.criterion;
},
});
var ValueView = Backbone.View.extend({
tagName: 'li',
className: "criterion",
template: loadTemplate("criterion-value"),
initialize: function() {},
events: {"click": "onClick"},
isCheckbox: function() {
return this.model.collection.criterion.get('type') === 'checkbox';
},
render: function() {
// if an image is provided we add some CSS classes
var image = this.model.get('image');
if (image) {
this.$el.addClass('icon ' + image);
} else if (this.isCheckbox()) {
this.$el.addClass('filter-checkbox');
}
this.$el.html(this.template(this.model.toJSON()));
return this;
},
onClick: function(event) {
if (this.isCheckbox()) {
this.$el.toggleClass('checked');
this.model.set('value', this.$el.hasClass('checked') ? 'certificate' : '');
}
this.model.collection.trigger('valueSelected', this.model);
}
});
var SelectValueView = ValueView.extend({
tagName: 'option',
className: '',
render: function() {
this.$el.attr('value', this.model.get('value'));
this.$el.text(this.model.get('title'))
return this;
}
})
// only display `max` items in filter columns (others will be displayed when `all` button clicked)
var ShortValueCollectionView = Backbone.View.extend({
tagName: 'ul',
className: "criteria",
initialize: function(options) {
this.listenTo(this.collection, 'add', this.addOne);
this.listenTo(this.collection, 'valueSelected', this.valueSelected);
this.max = options.max;
this.count = 0;
},
valueSelected: function(value) {
// ShortValueCollectionView and LongValueCollectionView use the same collection,
// so this event will be triggered for both display
this.collection.criterion.collection.trigger('criterionSelected', value)
$('.hide-on-escape-key').hide();
},
addOne: function(item) {
if (this.count < this.max) {
var view = new ValueView({model: item});
this.$el.append(view.render().el);
this.count += 1;
}
},
});
// Collection view that stores the value views listed whenever we click
// on a "all" button.
var LongValueCollectionView = Backbone.View.extend({
initialize: function() {
this.listenTo(this.collection, 'add', this.addOne);
this.sortedViews = [];
},
viewTitle: function(index) {
return this.sortedViews[index].model.get("title");
},
createView: function(model) {
var view = new ValueView({model: model});
return view;
},
addOne: function(item) {
// Insert view at the proper position, sorted by title
var view = this.createView(item);
var insertBeforeIndex;
var itemTitle = item.get('title');
for (insertBeforeIndex=0; insertBeforeIndex < this.sortedViews.length; insertBeforeIndex++) {
// We use locale-sensitive string comparison to make sure accented titles are correctly sorted
if (itemTitle.localeCompare(this.viewTitle(insertBeforeIndex)) < 0) {
break;
}
}
if (insertBeforeIndex === this.sortedViews.length) {
this.$el.append(view.render().$el);
} else {
this.sortedViews[insertBeforeIndex].$el.before(view.render().$el);
}
this.sortedViews.splice(insertBeforeIndex, 0, view);
},
});
var SelectValueCollectionView = LongValueCollectionView.extend({
events: {"change": "onChange"},
initialize: function(options) {
SelectValueCollectionView.__super__.initialize.apply(this);
this.$el.append($('<option>').text(options.title));
},
createView: function(model) {
var view = new SelectValueView({model: model});
return view;
},
onChange: function(event) {
var model = this.collection.findWhere({value: event.target.value});
this.collection.criterion.collection.trigger('criterionSelected', model);
// reset other selects
$('.small-device-filter select').not('#' + this.$el.attr('id')).prop('selectedIndex', 0);
},
});
// A criterion is a collection of possible filters from one kind. E.g: Language, Availability, etc.
var Criterion = Backbone.Model.extend({
defaults: {
// if no max value, we use a big one to display all items (and save JS LOC)
"max": 100
},
initialize: function() {
this.valuecollection = new ValueCollection([], {criterion: this});
},
getValue: function(value) {
return this.valuecollection.findWhere({value: value});
}
});
var CriterionView = Backbone.View.extend({
tagName: 'div',
initialize: function() {
// Top elements from value collection
this.valuecollectionview = new ShortValueCollectionView({
collection: this.model.valuecollection,
max: this.model.get('max')});
// 'select' DOM element for mobile view
this.selectvaluecollectionview = new SelectValueCollectionView({
el: this.model.get('select'),
collection: this.model.valuecollection,
title: this.model.get('title')});
if (this.model.get('all_button')) {
// Complete list of possible values
this.longvaluecollectionview = new LongValueCollectionView({
el: this.model.get('overlay') + ' ul',
collection: this.model.valuecollection});
// Hide overlay on click outside of overlay
var overlay = $(this.model.get("overlay"));
var that = this;
var onBodyClick = function(e) {
if (!overlay.is(e.target) && !that.$(".all-items").is(e.target)) {
overlay.hide();
}
}
$('body').on("click", onBodyClick);
};
this.model.valuecollection.add(this.model.get("values"))
},
render: function() {
// We don't use a template here
this.$el.html($('<h2>').text(this.model.get('title')));
this.$el.append(this.valuecollectionview.render().el);
if (this.model.get('all_button')) {
var allButtonContent = "<i class='glyphicon glyphicon-plus'></i> ";
allButtonContent += _.template(this.model.get("all_button"))({
count: this.model.valuecollection.length
});
var allButton = $('<span>').attr({'class': 'btn btn-transparent btn-dark-bg btn-block btn-padded all-items'});
allButton.html(allButtonContent);
this.$el.append(allButton);
}
return this;
},
events: {
'click span.all-items': 'toggleAllItemsOverlay'
},
toggleAllItemsOverlay: function(event) {
var overlay = $(this.model.get('overlay'));
overlay.toggle();
if (overlay.is(':visible')) {
/* Calculate vertical overlay's position regarding clicked button */
var top = $(event.currentTarget).prev().prev().offset().top - 68;
overlay.css('top', top + 'px');
/* Calculate vertical UL's height regarding the number of items and columns as Chrome seems to fail to do it right */
var height = Math.ceil(overlay.find('li').length / overlay.find('ul').css('column-count'));
var li_height = $('li.criterion').outerHeight(true);
li_height +=3; // quick fix to compensate university names which use 2 lines... We have to use shorter name
overlay.find('ul').css('height', (height * li_height) + 'px');
}
},
})
var CriterionCollection = Backbone.Collection.extend({
model: Criterion,
});
var CriterionCollectionView = Backbone.View.extend({
initialize: function() {
this.listenTo(this.collection, 'add', this.addOne);
},
addOne: function(criterion) {
var view = new CriterionView({model: criterion});
this.$el.append(view.render().el);
},
});
var Filter = Backbone.Model.extend({
defaults: {
selected: null,
resultsPerPage: 50,
resultsPerPageChoices: [20, 50, 100, -1],
currentPage: 1,
query: null,
sortBy: "",
},
startNewSearch: function(value) {
this.set({
selected: value,
currentPage: 1,
query: null,
sortBy: "",
});
}
});
var ResultStats = Backbone.Model.extend({
defaults: {
totalCount: null,
}
});
var FilterView = Backbone.View.extend({
initialize: function(options) {
this.resultstats = options.resultstats;
this.listenTo(this.model, 'change', this.render);
this.listenTo(this.model, 'change', this.updateUrl);
this.listenTo(this.resultstats, 'change', this.render);
},
updateUrl: function() {
var url = "";
var selected = this.model.get("selected");
var query = this.model.get("query");
var page = this.model.get("currentPage");
var rpp = this.model.get("resultsPerPage");
var sortBy = this.model.get("sortBy");
var parameters = [];
// Action
if (selected) {
url = 'filter/' + selected.criterionName() + '/' + selected.get('value');
} else if (query) {
url = 'search';
parameters.push({name: "query", value: query});
} else {
url = 'filter';
}
if (page || rpp) {
parameters.push({name: "page", value: page || 1});
parameters.push({name: "rpp", value: rpp || 50});
}
if (sortBy) {
parameters.push({name: "sort", value: sortBy});
}
url += '?' + $.param(parameters);
Backbone.history.navigate(url, {trigger: false});
// Trigger Google analytics event
var _gaq = (_gaq || []);
_gaq.push('_trackPageview', "${reverse('fun-courses:index')}" + "/" + url);
},
events: {
"click .glyphicon-remove": "clearSelectedValue",
"click .filter-dropdown": "toggleDropdown",
"click .filter-dropdown.results-per-page li": "setResultsPerPage",
"click .filter-dropdown.sort-by li": "setOrderBy",
"click .result-page": "changeCurrentPage",
},
template: loadTemplate("filter"),
clearSelectedValue: function() {
this.model.startNewSearch(null);
$('.small-device-filter select').prop('selectedIndex', 0); // reset selects
},
toggleDropdown: function(e) {
var dropdown = $(e.target);
if(!dropdown.hasClass("filter-dropdown")) {
// Clicks on inside elements should trigger events, too
dropdown = dropdown.parent(".filter-dropdown");
}
var dropdownChoices = dropdown.find("ul");
dropdownChoices.css("width", dropdown.outerWidth());
dropdownChoices.toggle();
e.stopPropagation();
},
setResultsPerPage: function(e) {
var value = parseInt($(e.target).attr("value"));
this.model.set("resultsPerPage", value);
e.stopPropagation();
},
setOrderBy: function(e) {
var value = $(e.target).attr("value");
this.model.set("sortBy", value);
e.stopPropagation();
},
changeCurrentPage: function(e) {
this.model.set("currentPage", parseInt($(e.target).attr("data-page")));
},
render: function() {
var context = this.model.toJSON();
context.totalCount = this.resultstats.get("totalCount");
context.totalPageCount = Math.ceil(context.totalCount / context.resultsPerPage);
context.sortByValues = {
"": "${_('Default')}",
//"title": "${_('Title')}",
"start_date": "${_('Start date')}",
//"-start_date": "${_('Start date (decreasing)')}",
//"start_enrollment_date": "${_('Enrollment date')}",
}
this.$el.html(this.template(context));
return this;
}
});
var Course = Backbone.Model.extend({
defaults: {
"thumbnails": {}
}
});
var CourseView = Backbone.View.extend({
tagName: 'li',
template: loadTemplate("small-course-block"),
initialize: function() {
this.listenTo(this.model, "destroy", this.remove);
this.listenTo(this.model, "remove", this.remove);
},
render: function() {
var context = this.model.toJSON();
context.course_about = '/courses/'+ context.key + '/about';
this.$el.html(this.template(context));
return this;
},
});
var CourseCollection = Backbone.Collection.extend({
model: Course,
initialize: function(models, options) {
this.filter = options.filter;
this.resultstats = options.resultstats;
},
url: function() {
// build query parameters
var parameters = [];
var selectedValue = this.filter.get("selected");
if (selectedValue) {
parameters.push({
name: selectedValue.criterionName(),
value: selectedValue.get("value")
});
}
if (this.filter.get("resultsPerPage") >= 0) {
parameters.push({
name: "rpp",
value: this.filter.get("resultsPerPage"),
})
}
parameters.push({
name: "page",
value: this.filter.get("currentPage"),
})
if (this.filter.get("query")) {
parameters.push({
name: "query",
value: this.filter.get("query"),
})
}
if (this.filter.get("sortBy")) {
parameters.push({
name: "sort",
value: this.filter.get("sortBy"),
})
}
return "${reverse('fun-courses-api:courses-list')}" + '?' + $.param(parameters);
},
parse: function (response) {
this.resultstats.set("totalCount", response.count);
return response.results;
},
});
var CourseCollectionView = Backbone.View.extend({
initialize: function(data) {
this.listenTo(this.collection, 'add', this.addOne);
this.listenTo(this.collection, 'request', this.syncing);
},
addOne: function(item) {
var view = new CourseView({model: item});
this.$el.append(view.render().el);
},
syncing: function() {
this.$el.html("<div class='spinner'></div>");
},
render: function() {
// Remove spinner
this.$el.find('.spinner').remove();
// clear previously added fake blocks
this.$el.find('.fake').remove();
// add some fake blocks to get correct alignment on last line
for (var i = 5; i >= 0; i--) {
this.$el.append($('<li>').attr('class', 'small-course fake'));
};
return this;
}
});
var AppView = Backbone.View.extend({
initialize: function(data) {
// filtering criteria
var criterioncollection = new CriterionCollection;
var criterioncollectionview = new CriterionCollectionView({
el: this.$(".criterions"),
collection: criterioncollection
});
this.listenTo(criterioncollection, 'criterionSelected', this.criterionSelected)
criterioncollection.add(data.filters)
// result stats
var resultstats = new ResultStats;
// selected filter
this.filter = new Filter;
this.filterview = new FilterView({
model: this.filter,
el: this.$('.result-navigation'),
resultstats: resultstats,
}).render();
this.listenTo(this.filter, 'change', this.refreshCourses);
this.listenTo(this.filter, 'initialized', this.refreshCourses);
var approuter = new AppRouter({
criterioncollection: criterioncollection,
filter: this.filter
});
// course list
this.coursecollection = new CourseCollection([], {filter: this.filter, resultstats: resultstats});
this.coursecollectionview = new CourseCollectionView({
el: this.$('#course-filtering-results'),
collection: this.coursecollection
});
this.listenTo(this.coursecollection, 'sync', this.courseCollectionSynced)
this.refreshCourseRequest = null;
},
criterionSelected: function(value) {
this.filter.startNewSearch(value);
},
refreshCourses: function(){
// Empty collection
while (this.coursecollection.length > 0) {
this.coursecollection.pop();
}
// Fill collection
if (this.refreshCourseRequest) {
// Abort previous request
this.refreshCourseRequest.abort();
}
this.refreshCourseRequest = this.coursecollection.fetch();
window.scrollTo(0, 0);
},
courseCollectionSynced: function(){
this.coursecollectionview.render()
}
});
var App = new AppView({
el: $('#course-search'),
filters: FILTERS
});
Backbone.history.start();
App.refreshCourses();
})($, _, Backbone);
</script>
</%block>
<%block name="content">
<div class="breadcrumbs-wrapper">
${breadcrumbs.breadcrumbs(_("Courses"))}
</div>
</%block>
<%block name="content_fullwidth2">
<div class="courses-page">
<div class="row-fluid">
<div class="col-lg-36 text-center">
<h1 class="big-title">${_("Courses")}</h1>
</div>
</div>
<div class="row-fluid">
<div id="course-search" class="course-list clearfix">
<div class="course-list-table">
<div class="left hidden-xs">
<div class="filters criteria-block">
<div class="title">${_("Refine search results")}</div>
<div class="criterions"></div>
</div>
</div>
<div class="right row">
<div class="small-device-filter row visible-xs">
<div class="title">${_("Refine search results")}</div>
<select id="status-select">
</select>
<select id="availability-select">
</select>
<select id="university-select">
</select>
<select id="theme-select">
</select>
</div>
<div class="result-navigation result-navigation-top">
</div>
<div class="list">
<ul id="course-filtering-results"></ul>
</div>
<div class="result-navigation result-navigation-bottom"></div>
<div id="all-themes-overlay" class="criteria-overlay criteria-block hide-on-escape-key overlay">
<div class="pull-right">
<span class="close-overlay glyphicon glyphicon-remove"></span>
</div>
<h2>${_("All themes")}</h2>
<ul class="criteria"></ul>
</div>
<div id="all-universities-overlay" class="criteria-overlay criteria-block hide-on-escape-key overlay">
<div class="pull-right">
<span class="close-overlay glyphicon glyphicon-remove"></span>
</div>
<h2>${_("All universities")}</h2>
<ul class="criteria"></ul>
</div>
</div>
</div>
</div>
</div>
</div>
</%block>