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
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
13 changes: 10 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({
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 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;
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: this.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
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
4 changes: 4 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,10 @@ 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 contents if this call may not be germane to the current test.
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="Teams 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>
4 changes: 2 additions & 2 deletions lms/djangoapps/badges/api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Serializers for Badges
Serializers for badging
"""
from rest_framework import serializers

Expand All @@ -25,4 +25,4 @@ class BadgeAssertionSerializer(serializers.ModelSerializer):

class Meta(object):
model = BadgeAssertion
fields = ('badge_class', 'image_url', 'assertion_url')
fields = ('badge_class', 'image_url', 'assertion_url', 'created')
4 changes: 2 additions & 2 deletions lms/djangoapps/badges/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
from openedx.core.djangoapps.user_api.urls import USERNAME_PATTERN

urlpatterns = patterns(
'badges.views',
url('^assertions/user/' + USERNAME_PATTERN + '/$', UserBadgeAssertions.as_view(), name='user-assertions'),
'badges.api',
url('^assertions/user/' + USERNAME_PATTERN + '/$', UserBadgeAssertions.as_view(), name='user_assertions'),
)
18 changes: 16 additions & 2 deletions lms/djangoapps/badges/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@
from rest_framework import generics
from rest_framework.exceptions import APIException

from openedx.core.djangoapps.user_api.permissions import is_field_shared_factory
from openedx.core.lib.api.authentication import (
OAuth2AuthenticationAllowInactiveUser,
SessionAuthenticationAllowInactiveUser
)
from badges.models import BadgeAssertion
from openedx.core.lib.api.view_utils import view_auth_classes
from .serializers import BadgeAssertionSerializer
from xmodule_django.models import CourseKeyField

Expand All @@ -20,7 +24,6 @@ class CourseKeyError(APIException):
default_detail = "The course key provided could not be parsed."


@view_auth_classes(is_user=True)
class UserBadgeAssertions(generics.ListAPIView):
"""
** Use cases **
Expand Down Expand Up @@ -89,6 +92,17 @@ class UserBadgeAssertions(generics.ListAPIView):
}
"""
serializer_class = BadgeAssertionSerializer
authentication_classes = (
OAuth2AuthenticationAllowInactiveUser,
SessionAuthenticationAllowInactiveUser
)
permission_classes = (is_field_shared_factory("accomplishments_shared"),)

def filter_queryset(self, queryset):
"""
Return most recent to least recent badge.
"""
return queryset.order_by('-created')

def get_queryset(self):
"""
Expand Down
8 changes: 6 additions & 2 deletions lms/djangoapps/badges/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import jsonfield.fields
import badges.models
from django.conf import settings
import django.utils.timezone
from model_utils import fields
import xmodule_django.models


Expand All @@ -23,14 +25,16 @@ class Migration(migrations.Migration):
('backend', models.CharField(max_length=50)),
('image_url', models.URLField()),
('assertion_url', models.URLField()),
('modified', fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)),
('created', fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False, db_index=True)),
],
),
migrations.CreateModel(
name='BadgeClass',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('slug', models.SlugField(max_length=255)),
('issuing_component', models.SlugField(default=b'', blank=True)),
('slug', models.SlugField(max_length=255, validators=[badges.models.validate_lowercase])),
('issuing_component', models.SlugField(default=b'', blank=True, validators=[badges.models.validate_lowercase])),
('display_name', models.CharField(max_length=255)),
('course_id', xmodule_django.models.CourseKeyField(default=None, max_length=255, blank=True)),
('description', models.TextField()),
Expand Down
12 changes: 10 additions & 2 deletions lms/djangoapps/badges/migrations/0002_data__migrate_assertions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
from __future__ import unicode_literals

import json
from datetime import datetime

import os
import time
from django.db import migrations, models


Expand Down Expand Up @@ -43,14 +45,20 @@ def forwards(apps, schema_editor):
data = badge.data
else:
data = json.dumps(badge.data)
BadgeAssertion(
assertion = BadgeAssertion(
user_id=badge.user_id,
badge_class=classes[(badge.course_id, badge.mode)],
data=data,
backend='BadgrBackend',
image_url=badge.data['image'],
assertion_url=badge.data['json']['id'],
).save()
)
assertion.save()
# Would be overwritten by the first save.

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.

FYI, per Django docs, another option "If you want to be able to modify this field, set the following instead of auto_now_add=True: default=timezone.now - from django.utils.timezone.now()"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@bradenmacdonald Yes, but aside from this initial use, it shouldn't need manual setting again.

assertion.created = datetime.fromtimestamp(
time.mktime(time.strptime(badge.data['created_at'], "%Y-%m-%dT%H:%M:%S"))
)
assertion.save()

for configuration in BadgeImageConfiguration.objects.all():
file_content = ContentFile(configuration.icon.read())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ class Migration(migrations.Migration):
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
('courses_completed', models.TextField(default=b'', help_text="On each line, put the number of completed courses to award a badge for, a comma, and the slug of a badge class you have created with the issuing component 'edx__course'. For example: 3,course-v1:edx/Demo/DemoX", blank=True)),
('courses_enrolled', models.TextField(default=b'', help_text="On each line, put the number of enrolled courses to award a badge for, a comma, and the slug of a badge class you have created with the issuing component 'edx__course'. For example: 3,course-v1:edx/Demo/DemoX", blank=True)),
('course_groups', models.TextField(default=b'', help_text="On each line, put the slug of a badge class you have created with the issuing component 'edx__course' to award, a comma, and a comma separated list of course keys that the user will need to complete to get this badge. For example: slug_for_compsci_courses_group_badge,course-v1:CompSci+Course+First,course-v1:CompsSci+Course+Second", blank=True)),
('courses_completed', models.TextField(default=b'', help_text="On each line, put the number of completed courses to award a badge for, a comma, and the slug of a badge class you have created with the issuing component 'edx__course'. For example: 3,enrolled_3_courses", blank=True)),
('courses_enrolled', models.TextField(default=b'', help_text="On each line, put the number of enrolled courses to award a badge for, a comma, and the slug of a badge class you have created with the issuing component 'edx__course'. For example: 3,enrolled_3_courses", blank=True)),
('course_groups', models.TextField(default=b'', help_text="Each line is a comma-separated list. The first item in each line is the slug of a badge class to award, with an issuing component of 'edx__course'. The remaining items in each line are the course keys the user will need to complete to be awarded the badge. For example: slug_for_compsci_courses_group_badge,course-v1:CompSci+Course+First,course-v1:CompsSci+Course+Second", blank=True)),
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to=settings.AUTH_USER_MODEL, null=True, verbose_name='Changed by')),
],
),
Expand Down
Loading