Skip to content
Snippets Groups Projects
Commit a04a635e authored by Jonathan Piacenti's avatar Jonathan Piacenti
Browse files

Add accomplishments to user profile

parent a2104634
No related merge requests found
Showing
with 128 additions and 66 deletions
......@@ -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;
......
......@@ -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;
}
});
......
......@@ -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');
......
......@@ -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);
......
......@@ -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,
......
......@@ -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
......
......@@ -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',
......
......@@ -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);
};
......
<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>
......
<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>
"""
Serializers for Badges
Serializers for badging
"""
from rest_framework import serializers
......@@ -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')
......@@ -6,8 +6,12 @@ from opaque_keys.edx.keys import CourseKey
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
......@@ -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 **
......@@ -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):
"""
......
......@@ -5,6 +5,8 @@ from django.db import migrations, models
import jsonfield.fields
import badges.models
from django.conf import settings
import django.utils.timezone
from model_utils import fields
import xmodule_django.models
......@@ -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()),
......
......@@ -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
......@@ -47,14 +49,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.
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())
......
......@@ -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')),
],
),
......
......@@ -9,6 +9,7 @@ from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import ugettext_lazy as _
from lazy import lazy
from model_utils.models import TimeStampedModel
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
......@@ -122,7 +123,7 @@ class BadgeClass(models.Model):
verbose_name_plural = "Badge Classes"
class BadgeAssertion(models.Model):
class BadgeAssertion(TimeStampedModel):
"""
Tracks badges on our side of the badge baking transaction
"""
......@@ -152,6 +153,10 @@ class BadgeAssertion(models.Model):
app_label = "badges"
# Abstract model doesn't index this, so we have to.
BadgeAssertion._meta.get_field('created').db_index = True # pylint: disable=protected-access
class CourseCompleteImageConfiguration(models.Model):
"""
Contains the icon configuration for badges for a specific course mode.
......@@ -216,7 +221,7 @@ class CourseEventBadgesConfiguration(ConfigurationModel):
help_text=_(
u"On each line, put the number of completed courses to award a badge for, a comma, and the slug of a "
u"badge class you have created with the issuing component 'edx__course'. "
u"For example: 3,course-v1:edx/Demo/DemoX"
u"For example: 3,enrolled_3_courses"
)
)
courses_enrolled = models.TextField(
......@@ -224,7 +229,7 @@ class CourseEventBadgesConfiguration(ConfigurationModel):
help_text=_(
u"On each line, put the number of enrolled courses to award a badge for, a comma, and the slug of a "
u"badge class you have created with the issuing component 'edx__course'. "
u"For example: 3,course-v1:edx/Demo/DemoX"
u"For example: 3,enrolled_3_courses"
)
)
course_groups = models.TextField(
......@@ -232,8 +237,8 @@ class CourseEventBadgesConfiguration(ConfigurationModel):
help_text=_(
u"Each line is a comma-separated list. The first item in each line is the slug of a badge class to award, "
u"with an issuing component of 'edx__course'. The remaining items in each line are the course keys the "
u"user will need to complete to get the badge. For example: slug_for_compsci_courses_group_badge,course-v1"
u":CompSci+Course+First,course-v1:CompsSci+Course+Second"
u"user will need to complete to be awarded the badge. For example: "
u"slug_for_compsci_courses_group_badge,course-v1:CompSci+Course+First,course-v1:CompsSci+Course+Second"
)
)
......
""" Views for a student's profile information. """
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ObjectDoesNotExist
from django_countries import countries
from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required
from django.http import Http404
from django.views.decorators.http import require_http_methods
from django_countries import countries
from edxmako.shortcuts import render_to_response
from edxmako.shortcuts import render_to_response, marketing_link
from microsite_configuration import microsite
from openedx.core.djangoapps.user_api.accounts.api import get_account_settings
from openedx.core.djangoapps.user_api.accounts.serializers import PROFILE_IMAGE_KEY_PREFIX
from openedx.core.djangoapps.user_api.errors import UserNotFound, UserNotAuthorized
from openedx.core.djangoapps.user_api.preferences.api import get_user_preferences
from student.models import User
from microsite_configuration import microsite
@login_required
......@@ -87,9 +85,14 @@ def learner_profile_context(request, profile_username, user_is_staff):
'has_preferences_access': (logged_in_user.username == profile_username or user_is_staff),
'own_profile': own_profile,
'country_options': list(countries),
'find_courses_url': marketing_link('COURSES'),
'language_options': settings.ALL_LANGUAGES,
'platform_name': microsite.get_value('platform_name', settings.PLATFORM_NAME),
},
'disable_courseware_js': True,
}
if settings.FEATURES.get("ENABLE_OPENBADGES"):
context['data']['badges_api_url'] = reverse("badges_api:user_assertions", kwargs={'username': profile_username})
return context
......@@ -10,13 +10,13 @@
BaseCollection.prototype.initialize.call(this, options);
this.server_api = _.extend(
this.server_api,
{
topic_id: this.topic_id = options.topic_id,
expand: 'user',
course_id: function () { return encodeURIComponent(self.course_id); },
order_by: function () { return self.searchString ? '' : this.sortField; }
},
BaseCollection.prototype.server_api
}
);
delete this.server_api.sort_order; // Sort order is not specified for the Team API
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment