Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions common/djangoapps/student/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import uuid

import analytics

from config_models.models import ConfigurationModel
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
Expand All @@ -45,6 +46,7 @@
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
Expand Down Expand Up @@ -1212,6 +1214,10 @@ def enroll(cls, user, course_key, mode=None, check_access=False):
# 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
Expand Down
42 changes: 22 additions & 20 deletions common/static/common/js/components/collections/paging_collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand Down
20 changes: 14 additions & 6 deletions common/static/common/js/components/views/list.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
});
Expand Down
19 changes: 16 additions & 3 deletions common/static/common/js/components/views/paginated_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Add a comment explaining why we're referencing this.listViewClass instead of ListView

tagName: 'div',
className: this.type + '-container',
itemViewClass: this.itemViewClass
Expand All @@ -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');
Expand All @@ -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();
}
Expand Down
4 changes: 3 additions & 1 deletion common/static/common/js/components/views/paging_footer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
_,
Expand Down Expand Up @@ -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
Expand All @@ -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});
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions common/static/common/js/spec/main_requirejs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
5 changes: 5 additions & 0 deletions common/static/common/js/spec_helpers/ajax_helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
<nav class="pagination pagination-full bottom" aria-label="Pagination">
<nav class="pagination pagination-full bottom" aria-label="<%= paginationLabel %>">
<div class="nav-item previous"><button class="nav-link previous-page-link"><i class="icon fa fa-angle-left" aria-hidden="true"></i> <span class="nav-label"><%= gettext("Previous") %></span></button></div>
<div class="nav-item page">
<div class="pagination-form">
<label class="page-number-label" for="page-number-input"><%= gettext("Page number") %></label>
<label class="page-number-label" for="page-number-input"><%= interpolate(
gettext("Page number out of %(total_pages)s"),
{total_pages: total_pages},
true
)%></label>
<input id="page-number-input" class="page-number-input" name="page-number" type="text" size="4" autocomplete="off" aria-describedby="page-number-input-helper"/>
<span class="sr field-helper" id="page-number-input-helper"><%= gettext("Enter the page number you'd like to quickly navigate to.") %></span>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<nav class="page-content-nav" aria-label="Teams"></nav>
<nav class="page-content-nav" aria-label="<%- viewLabel %>"></nav>
<div class="page-content-main">
<div class="tabs"></div>
</div>
66 changes: 66 additions & 0 deletions common/test/acceptance/pages/lms/learner_profile.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""
Bok-Choy PageObject class for learner profile page.
"""
from bok_choy.query import BrowserQuery

from . import BASE_URL
from bok_choy.page_object import PageObject
from .fields import FieldsMixin
Expand All @@ -16,6 +18,49 @@
}


class Badge(PageObject):
"""
Represents a single badge displayed on the learner profile page.
"""
url = None

def __init__(self, element, browser):
self.full_view = browser
# Element API is similar to browser API, should allow subqueries.
super(Badge, self).__init__(element)

def is_browser_on_page(self):
return self.q(css=".badge-details").visible

def modal_displayed(self):
"""
Verifies that the share modal is diplayed.
"""
# The modal is on the page at large, and not a subelement of the badge div.
return BrowserQuery(self.full_view, css=".badges-modal").visible

def display_modal(self):
"""
Click the share button to display the sharing modal for the badge.
"""
self.q(css=".share-button").click()
EmptyPromise(self.modal_displayed, "Share modal displayed").fulfill()
EmptyPromise(self.modal_focused, "Focus handed to modal").fulfill()

def modal_focused(self):
"""
Return True if the badges model has focus, False otherwise.
"""
return BrowserQuery(self.full_view, css=".badges-modal").is_focused()

def close_modal(self):
"""
Close the badges modal and check that it is no longer displayed.
"""
BrowserQuery(self.full_view, css=".badges-modal .close").click()
EmptyPromise(lambda: not self.modal_displayed(), "Share modal dismissed").fulfill()


class LearnerProfilePage(FieldsMixin, PageObject):
"""
PageObject methods for Learning Profile Page.
Expand Down Expand Up @@ -58,6 +103,27 @@ def privacy(self):
"""
return 'all_users' if self.q(css=PROFILE_VISIBILITY_SELECTOR.format('all_users')).selected else 'private'

def accomplishments_available(self):
"""
Verify that the accomplishments tab is available.
"""
return self.q(css="button[data-url='accomplishments']").visible

def display_accomplishments(self):
"""
Click the accomplishments tab and wait for the accomplishments to load.
"""
EmptyPromise(self.accomplishments_available, "Accomplishments tab is displayed").fulfill()
self.q(css="button[data-url='accomplishments']").click()
self.wait_for_element_visibility(".badge-list", "Badge list displayed")

@property
def badges(self):
"""
Get all currently listed badges.
"""
return [Badge(element, self.browser) for element in self.q(css=".badge-display:not(.badge-placeholder)")]

@privacy.setter
def privacy(self, privacy):
"""
Expand Down
Loading