diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 8ff43b4456..cf5fa7bac0 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -22,6 +22,7 @@ from urllib import urlencode import uuid import analytics + from config_models.models import ConfigurationModel from django.utils.translation import ugettext_lazy as _ from django.conf import settings @@ -45,6 +46,7 @@ from simple_history.models import HistoricalRecords from track import contexts from xmodule_django.models import CourseKeyField, NoneToEmptyManager +from lms.djangoapps.badges.utils import badges_enabled from certificates.models import GeneratedCertificate from course_modes.models import CourseMode from enrollment.api import _default_course_mode @@ -1212,6 +1214,10 @@ class CourseEnrollment(models.Model): # User is allowed to enroll if they've reached this point. enrollment = cls.get_or_create_enrollment(user, course_key) enrollment.update_enrollment(is_active=True, mode=mode) + if badges_enabled(): + from lms.djangoapps.badges.events.course_meta import award_enrollment_badge + award_enrollment_badge(user) + return enrollment @classmethod diff --git a/common/static/common/js/components/collections/paging_collection.js b/common/static/common/js/components/collections/paging_collection.js index 0259b56c2c..3acb1e033d 100644 --- a/common/static/common/js/components/collections/paging_collection.js +++ b/common/static/common/js/components/collections/paging_collection.js @@ -21,10 +21,32 @@ define(['backbone.paginator'], function (BackbonePaginator) { var PagingCollection = BackbonePaginator.requestPager.extend({ initialize: function () { + var self = this; // These must be initialized in the constructor because otherwise all PagingCollections would point // to the same object references for sortableFields and filterableFields. this.sortableFields = {}; this.filterableFields = {}; + + this.paginator_core = { + type: 'GET', + dataType: 'json', + url: function () { return this.url; } + }; + this.paginator_ui = { + firstPage: function () { return self.isZeroIndexed ? 0 : 1; }, + // Specifies the initial page during collection initialization + currentPage: self.isZeroIndexed ? 0 : 1, + perPage: function () { return self.perPage; } + }; + + this.currentPage = this.paginator_ui.currentPage; + + this.server_api = { + page: function () { return self.currentPage; }, + page_size: function () { return self.perPage; }, + text_search: function () { return self.searchString ? self.searchString : ''; }, + sort_order: function () { return self.sortField; } + }; }, isZeroIndexed: false, @@ -41,26 +63,6 @@ searchString: null, - paginator_core: { - type: 'GET', - dataType: 'json', - url: function () { return this.url; } - }, - - paginator_ui: { - firstPage: function () { return this.isZeroIndexed ? 0 : 1; }, - // Specifies the initial page during collection initialization - currentPage: function () { return this.isZeroIndexed ? 0 : 1; }, - perPage: function () { return this.perPage; } - }, - - server_api: { - page: function () { return this.currentPage; }, - page_size: function () { return this.perPage; }, - text_search: function () { return this.searchString ? this.searchString : ''; }, - sort_order: function () { return this.sortField; } - }, - parse: function (response) { this.totalCount = response.count; this.currentPage = response.current_page; diff --git a/common/static/common/js/components/views/list.js b/common/static/common/js/components/views/list.js index b8c319afaa..ed3a64f4af 100644 --- a/common/static/common/js/components/views/list.js +++ b/common/static/common/js/components/views/list.js @@ -24,18 +24,26 @@ this.itemViews = []; }, + renderCollection: function() { + /** + * Render every item in the collection. + * This should push each rendered item to this.itemViews + * to ensure garbage collection works. + */ + this.collection.each(function (model) { + var itemView = new this.itemViewClass({model: model}); + this.$el.append(itemView.render().el); + this.itemViews.push(itemView); + }, this); + }, + render: function () { // Remove old children views _.each(this.itemViews, function (childView) { childView.remove(); }); this.itemViews = []; - // Render the collection - this.collection.each(function (model) { - var itemView = new this.itemViewClass({model: model}); - this.$el.append(itemView.render().el); - this.itemViews.push(itemView); - }, this); + this.renderCollection(); return this; } }); diff --git a/common/static/common/js/components/views/paginated_view.js b/common/static/common/js/components/views/paginated_view.js index 7755a93f1d..947ce83088 100644 --- a/common/static/common/js/components/views/paginated_view.js +++ b/common/static/common/js/components/views/paginated_view.js @@ -26,7 +26,7 @@ ], function (Backbone, _, PagingHeader, PagingFooter, ListView, paginatedViewTemplate) { var PaginatedView = Backbone.View.extend({ initialize: function () { - var ItemListView = ListView.extend({ + var ItemListView = this.listViewClass.extend({ tagName: 'div', className: this.type + '-container', itemViewClass: this.itemViewClass @@ -39,18 +39,25 @@ }, this); }, + listViewClass: ListView, + + viewTemplate: paginatedViewTemplate, + + paginationLabel: gettext("Pagination"), + createHeaderView: function() { return new PagingHeader({collection: this.options.collection, srInfo: this.srInfo}); }, createFooterView: function() { return new PagingFooter({ - collection: this.options.collection, hideWhenOnePage: true + collection: this.options.collection, hideWhenOnePage: true, + paginationLabel: this.paginationLabel }); }, render: function () { - this.$el.html(_.template(paginatedViewTemplate)({type: this.type})); + this.$el.html(_.template(this.viewTemplate)({type: this.type})); this.assign(this.listView, '.' + this.type + '-list'); if (this.headerView) { this.assign(this.headerView, '.' + this.type + '-paging-header'); @@ -61,6 +68,12 @@ return this; }, + renderError: function () { + this.$el.text( + gettext('Your request could not be completed. Reload the page and try again. If the issue persists, click the Help tab to report the problem.') // jshint ignore: line + ); + }, + assign: function (view, selector) { view.setElement(this.$(selector)).render(); } diff --git a/common/static/common/js/components/views/paging_footer.js b/common/static/common/js/components/views/paging_footer.js index 8b796b52b9..b68a0e9133 100644 --- a/common/static/common/js/components/views/paging_footer.js +++ b/common/static/common/js/components/views/paging_footer.js @@ -13,6 +13,7 @@ initialize: function(options) { this.collection = options.collection; this.hideWhenOnePage = options.hideWhenOnePage || false; + this.paginationLabel = options.paginationLabel || gettext("Pagination"); this.collection.bind('add', _.bind(this.render, this)); this.collection.bind('remove', _.bind(this.render, this)); this.collection.bind('reset', _.bind(this.render, this)); @@ -32,7 +33,8 @@ } this.$el.html(_.template(paging_footer_template)({ current_page: this.collection.getPage(), - total_pages: this.collection.totalPages + total_pages: this.collection.totalPages, + paginationLabel: this.paginationLabel })); this.$(".previous-page-link").toggleClass("is-disabled", onFirstPage).attr('aria-disabled', onFirstPage); this.$(".next-page-link").toggleClass("is-disabled", onLastPage).attr('aria-disabled', onLastPage); diff --git a/lms/static/js/components/tabbed/views/tabbed_view.js b/common/static/common/js/components/views/tabbed_view.js similarity index 93% rename from lms/static/js/components/tabbed/views/tabbed_view.js rename to common/static/common/js/components/views/tabbed_view.js index 367a803ef3..375d9f36f4 100644 --- a/lms/static/js/components/tabbed/views/tabbed_view.js +++ b/common/static/common/js/components/views/tabbed_view.js @@ -3,9 +3,9 @@ define(['backbone', 'underscore', 'jquery', - 'text!templates/components/tabbed/tabbed_view.underscore', - 'text!templates/components/tabbed/tab.underscore', - 'text!templates/components/tabbed/tabpanel.underscore', + 'text!common/templates/components/tabbed_view.underscore', + 'text!common/templates/components/tab.underscore', + 'text!common/templates/components/tabpanel.underscore', ], function ( Backbone, _, @@ -37,8 +37,6 @@ 'click .nav-item.tab': 'switchTab' }, - template: _.template(tabbedViewTemplate), - /** * View for a tabbed interface. Expects a list of tabs * in its options object, each of which should contain the @@ -51,12 +49,13 @@ * If a router is passed in (via options.router), * use that router to keep track of history between * tabs. Backbone.history.start() must be called - * by the router's instatiator after this view is + * by the router's instantiator after this view is * initialized. */ initialize: function (options) { this.router = options.router || null; this.tabs = options.tabs; + this.template = _.template(tabbedViewTemplate)({viewLabel: options.viewLabel}); // Convert each view into a TabPanelView _.each(this.tabs, function (tabInfo) { tabInfo.view = new TabPanelView({url: tabInfo.url, view: tabInfo.view}); @@ -69,7 +68,7 @@ render: function () { var self = this; - this.$el.html(this.template({})); + this.$el.html(this.template); _.each(this.tabs, function(tabInfo, index) { var tabEl = $(_.template(tabTemplate)({ index: index, diff --git a/lms/static/js/spec/components/tabbed/tabbed_view_spec.js b/common/static/common/js/spec/components/tabbed_view_spec.js similarity index 97% rename from lms/static/js/spec/components/tabbed/tabbed_view_spec.js rename to common/static/common/js/spec/components/tabbed_view_spec.js index cebe44f6d6..edf9319d0d 100644 --- a/lms/static/js/spec/components/tabbed/tabbed_view_spec.js +++ b/common/static/common/js/spec/components/tabbed_view_spec.js @@ -4,7 +4,7 @@ define(['jquery', 'underscore', 'backbone', - 'js/components/tabbed/views/tabbed_view' + 'common/js/components/views/tabbed_view' ], function($, _, Backbone, TabbedView) { var view, @@ -36,7 +36,8 @@ title: 'Test 2', view: new TestSubview({text: 'other text'}), url: 'test-2' - }] + }], + viewLabel: 'Tabs', }).render(); // _.defer() is used to make calls to diff --git a/common/static/common/js/spec/main_requirejs.js b/common/static/common/js/spec/main_requirejs.js index 23d8adfcb1..94a40ca2de 100644 --- a/common/static/common/js/spec/main_requirejs.js +++ b/common/static/common/js/spec/main_requirejs.js @@ -155,6 +155,7 @@ define([ // Run the common tests that use RequireJS. + 'common-requirejs/include/common/js/spec/components/tabbed_view_spec.js', 'common-requirejs/include/common/js/spec/components/feedback_spec.js', 'common-requirejs/include/common/js/spec/components/list_spec.js', 'common-requirejs/include/common/js/spec/components/paginated_view_spec.js', diff --git a/common/static/common/js/spec_helpers/ajax_helpers.js b/common/static/common/js/spec_helpers/ajax_helpers.js index 665da718fe..056328e07e 100644 --- a/common/static/common/js/spec_helpers/ajax_helpers.js +++ b/common/static/common/js/spec_helpers/ajax_helpers.js @@ -72,6 +72,11 @@ define(['sinon', 'underscore', 'URI'], function(sinon, _, URI) { expect(request.readyState).toEqual(XML_HTTP_READY_STATES.OPENED); expect(request.url).toEqual(url); expect(request.method).toEqual(method); + if (typeof body === 'undefined') { + // The body of the request may not be germane to the current test-- like some call by a library, + // so allow it to be ignored. + return; + } expect(request.requestBody).toEqual(body); }; diff --git a/common/static/common/templates/components/paging-footer.underscore b/common/static/common/templates/components/paging-footer.underscore index d04bbc0d2d..28c34b6178 100644 --- a/common/static/common/templates/components/paging-footer.underscore +++ b/common/static/common/templates/components/paging-footer.underscore @@ -1,8 +1,12 @@ -