Merge pull request #10556 from edx/feature/badges-v2
Badges V2 Feature Branch
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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');
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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,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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -16,6 +18,49 @@ FIELD_ICONS = {
|
||||
}
|
||||
|
||||
|
||||
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.
|
||||
@@ -58,6 +103,27 @@ class LearnerProfilePage(FieldsMixin, PageObject):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
|
||||
@@ -750,6 +750,15 @@ class DifferentUserLearnerProfilePageTest(LearnerProfileTestMixin, WebAppTest):
|
||||
self.verify_profile_page_is_public(profile_page, is_editable=False)
|
||||
self.verify_profile_page_view_event(username, different_user_id, visibility=self.PRIVACY_PUBLIC)
|
||||
|
||||
def test_badge_share_modal(self):
|
||||
username = 'testcert'
|
||||
AutoAuthPage(self.browser, username=username).visit()
|
||||
profile_page = self.visit_profile_page(username)
|
||||
profile_page.display_accomplishments()
|
||||
badge = profile_page.badges[0]
|
||||
badge.display_modal()
|
||||
badge.close_modal()
|
||||
|
||||
|
||||
@attr('a11y')
|
||||
class LearnerProfileA11yTest(LearnerProfileTestMixin, WebAppTest):
|
||||
@@ -800,3 +809,22 @@ class LearnerProfileA11yTest(LearnerProfileTestMixin, WebAppTest):
|
||||
})
|
||||
|
||||
profile_page.a11y_audit.check_for_accessibility_errors()
|
||||
|
||||
def test_badges_accessibility(self):
|
||||
"""
|
||||
Test the accessibility of the badge listings and sharing modal.
|
||||
"""
|
||||
username = 'testcert'
|
||||
AutoAuthPage(self.browser, username=username).visit()
|
||||
profile_page = self.visit_profile_page(username)
|
||||
|
||||
profile_page.a11y_audit.config.set_rules({
|
||||
"ignore": [
|
||||
'skip-link', # TODO: AC-179
|
||||
'link-href', # TODO: AC-231
|
||||
],
|
||||
})
|
||||
profile_page.display_accomplishments()
|
||||
profile_page.a11y_audit.check_for_accessibility_errors()
|
||||
profile_page.badges[0].display_modal()
|
||||
profile_page.a11y_audit.check_for_accessibility_errors()
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -9,6 +9,45 @@
|
||||
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
||||
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
||||
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
|
||||
DROP TABLE IF EXISTS `api_admin_apiaccessrequest`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `api_admin_apiaccessrequest` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`created` datetime(6) NOT NULL,
|
||||
`modified` datetime(6) NOT NULL,
|
||||
`status` varchar(255) NOT NULL,
|
||||
`website` varchar(200) NOT NULL,
|
||||
`reason` longtext NOT NULL,
|
||||
`user_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `api_admin_apiaccessrequ_user_id_6753e50e296cabc7_fk_auth_user_id` (`user_id`),
|
||||
KEY `api_admin_apiaccessrequest_9acb4454` (`status`),
|
||||
CONSTRAINT `api_admin_apiaccessrequ_user_id_6753e50e296cabc7_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `api_admin_historicalapiaccessrequest`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `api_admin_historicalapiaccessrequest` (
|
||||
`id` int(11) NOT NULL,
|
||||
`created` datetime(6) NOT NULL,
|
||||
`modified` datetime(6) NOT NULL,
|
||||
`status` varchar(255) NOT NULL,
|
||||
`website` varchar(200) NOT NULL,
|
||||
`reason` longtext NOT NULL,
|
||||
`history_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`history_date` datetime(6) NOT NULL,
|
||||
`history_type` varchar(1) NOT NULL,
|
||||
`history_user_id` int(11) DEFAULT NULL,
|
||||
`user_id` int(11) DEFAULT NULL,
|
||||
PRIMARY KEY (`history_id`),
|
||||
KEY `api_admin_histo_history_user_id_73c59297a81bcd02_fk_auth_user_id` (`history_user_id`),
|
||||
KEY `api_admin_historicalapiaccessrequest_b80bb774` (`id`),
|
||||
KEY `api_admin_historicalapiaccessrequest_9acb4454` (`status`),
|
||||
CONSTRAINT `api_admin_histo_history_user_id_73c59297a81bcd02_fk_auth_user_id` FOREIGN KEY (`history_user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `assessment_aiclassifier`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
@@ -20,8 +59,8 @@ CREATE TABLE `assessment_aiclassifier` (
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `assessment_aiclassifier_962f069f` (`classifier_set_id`),
|
||||
KEY `assessment_aiclassifier_385b00a3` (`criterion_id`),
|
||||
CONSTRAINT `assessm_criterion_id_275db29f2a0e1711_fk_assessment_criterion_id` FOREIGN KEY (`criterion_id`) REFERENCES `assessment_criterion` (`id`),
|
||||
CONSTRAINT `D3bd45d5e3c9cfdc4f3b442119adebe8` FOREIGN KEY (`classifier_set_id`) REFERENCES `assessment_aiclassifierset` (`id`)
|
||||
CONSTRAINT `D3bd45d5e3c9cfdc4f3b442119adebe8` FOREIGN KEY (`classifier_set_id`) REFERENCES `assessment_aiclassifierset` (`id`),
|
||||
CONSTRAINT `assessm_criterion_id_275db29f2a0e1711_fk_assessment_criterion_id` FOREIGN KEY (`criterion_id`) REFERENCES `assessment_criterion` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `assessment_aiclassifierset`;
|
||||
@@ -72,9 +111,9 @@ CREATE TABLE `assessment_aigradingworkflow` (
|
||||
KEY `assessment_aigradingworkflow_a4079fcf` (`assessment_id`),
|
||||
KEY `assessment_aigradingworkflow_962f069f` (`classifier_set_id`),
|
||||
KEY `assessment_aigradingworkflow_8980b7ae` (`rubric_id`),
|
||||
CONSTRAINT `assessment_ai_rubric_id_3fc938e9e3ae7b2d_fk_assessment_rubric_id` FOREIGN KEY (`rubric_id`) REFERENCES `assessment_rubric` (`id`),
|
||||
CONSTRAINT `D4d9bca115376aeb07fd970155499db3` FOREIGN KEY (`classifier_set_id`) REFERENCES `assessment_aiclassifierset` (`id`),
|
||||
CONSTRAINT `asses_assessment_id_68b86880a7f62f1c_fk_assessment_assessment_id` FOREIGN KEY (`assessment_id`) REFERENCES `assessment_assessment` (`id`),
|
||||
CONSTRAINT `D4d9bca115376aeb07fd970155499db3` FOREIGN KEY (`classifier_set_id`) REFERENCES `assessment_aiclassifierset` (`id`)
|
||||
CONSTRAINT `assessment_ai_rubric_id_3fc938e9e3ae7b2d_fk_assessment_rubric_id` FOREIGN KEY (`rubric_id`) REFERENCES `assessment_rubric` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `assessment_aitrainingworkflow`;
|
||||
@@ -110,8 +149,8 @@ CREATE TABLE `assessment_aitrainingworkflow_training_examples` (
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `aitrainingworkflow_id` (`aitrainingworkflow_id`,`trainingexample_id`),
|
||||
KEY `ff4ddecc43bd06c0d85785a61e955133` (`trainingexample_id`),
|
||||
CONSTRAINT `ff4ddecc43bd06c0d85785a61e955133` FOREIGN KEY (`trainingexample_id`) REFERENCES `assessment_trainingexample` (`id`),
|
||||
CONSTRAINT `da55be90caee21d95136e40c53e5c754` FOREIGN KEY (`aitrainingworkflow_id`) REFERENCES `assessment_aitrainingworkflow` (`id`)
|
||||
CONSTRAINT `da55be90caee21d95136e40c53e5c754` FOREIGN KEY (`aitrainingworkflow_id`) REFERENCES `assessment_aitrainingworkflow` (`id`),
|
||||
CONSTRAINT `ff4ddecc43bd06c0d85785a61e955133` FOREIGN KEY (`trainingexample_id`) REFERENCES `assessment_trainingexample` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `assessment_assessment`;
|
||||
@@ -154,8 +193,8 @@ CREATE TABLE `assessment_assessmentfeedback_assessments` (
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `assessmentfeedback_id` (`assessmentfeedback_id`,`assessment_id`),
|
||||
KEY `asses_assessment_id_392d354eca2e0c87_fk_assessment_assessment_id` (`assessment_id`),
|
||||
CONSTRAINT `asses_assessment_id_392d354eca2e0c87_fk_assessment_assessment_id` FOREIGN KEY (`assessment_id`) REFERENCES `assessment_assessment` (`id`),
|
||||
CONSTRAINT `D1fc3fa7cd7be79d20561668a95a9fc1` FOREIGN KEY (`assessmentfeedback_id`) REFERENCES `assessment_assessmentfeedback` (`id`)
|
||||
CONSTRAINT `D1fc3fa7cd7be79d20561668a95a9fc1` FOREIGN KEY (`assessmentfeedback_id`) REFERENCES `assessment_assessmentfeedback` (`id`),
|
||||
CONSTRAINT `asses_assessment_id_392d354eca2e0c87_fk_assessment_assessment_id` FOREIGN KEY (`assessment_id`) REFERENCES `assessment_assessment` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `assessment_assessmentfeedback_options`;
|
||||
@@ -168,8 +207,8 @@ CREATE TABLE `assessment_assessmentfeedback_options` (
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `assessmentfeedback_id` (`assessmentfeedback_id`,`assessmentfeedbackoption_id`),
|
||||
KEY `cc7028abc88c431df3172c9b2d6422e4` (`assessmentfeedbackoption_id`),
|
||||
CONSTRAINT `cc7028abc88c431df3172c9b2d6422e4` FOREIGN KEY (`assessmentfeedbackoption_id`) REFERENCES `assessment_assessmentfeedbackoption` (`id`),
|
||||
CONSTRAINT `cba12ac98c4a04d67d5edaa2223f4fe5` FOREIGN KEY (`assessmentfeedback_id`) REFERENCES `assessment_assessmentfeedback` (`id`)
|
||||
CONSTRAINT `cba12ac98c4a04d67d5edaa2223f4fe5` FOREIGN KEY (`assessmentfeedback_id`) REFERENCES `assessment_assessmentfeedback` (`id`),
|
||||
CONSTRAINT `cc7028abc88c431df3172c9b2d6422e4` FOREIGN KEY (`assessmentfeedbackoption_id`) REFERENCES `assessment_assessmentfeedbackoption` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `assessment_assessmentfeedbackoption`;
|
||||
@@ -196,8 +235,8 @@ CREATE TABLE `assessment_assessmentpart` (
|
||||
KEY `assessment_assessmentpart_385b00a3` (`criterion_id`),
|
||||
KEY `assessment_assessmentpart_28df3725` (`option_id`),
|
||||
CONSTRAINT `asse_option_id_2508a14feeabf4ce_fk_assessment_criterionoption_id` FOREIGN KEY (`option_id`) REFERENCES `assessment_criterionoption` (`id`),
|
||||
CONSTRAINT `assessm_criterion_id_2061f2359fd292bf_fk_assessment_criterion_id` FOREIGN KEY (`criterion_id`) REFERENCES `assessment_criterion` (`id`),
|
||||
CONSTRAINT `asses_assessment_id_1d752290138ce479_fk_assessment_assessment_id` FOREIGN KEY (`assessment_id`) REFERENCES `assessment_assessment` (`id`)
|
||||
CONSTRAINT `asses_assessment_id_1d752290138ce479_fk_assessment_assessment_id` FOREIGN KEY (`assessment_id`) REFERENCES `assessment_assessment` (`id`),
|
||||
CONSTRAINT `assessm_criterion_id_2061f2359fd292bf_fk_assessment_criterion_id` FOREIGN KEY (`criterion_id`) REFERENCES `assessment_criterion` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `assessment_criterion`;
|
||||
@@ -272,9 +311,9 @@ CREATE TABLE `assessment_peerworkflowitem` (
|
||||
KEY `assessm_scorer_id_2d803ee2d52c0e2c_fk_assessment_peerworkflow_id` (`scorer_id`),
|
||||
KEY `assessment_peerworkflowitem_ab5b2b73` (`submission_uuid`),
|
||||
KEY `assessment_peerworkflowitem_ff1ae11b` (`started_at`),
|
||||
CONSTRAINT `assessm_scorer_id_2d803ee2d52c0e2c_fk_assessment_peerworkflow_id` FOREIGN KEY (`scorer_id`) REFERENCES `assessment_peerworkflow` (`id`),
|
||||
CONSTRAINT `asses_assessment_id_15cadfae90ddcc2a_fk_assessment_assessment_id` FOREIGN KEY (`assessment_id`) REFERENCES `assessment_assessment` (`id`),
|
||||
CONSTRAINT `assessm_author_id_1948f89dea6d2b5f_fk_assessment_peerworkflow_id` FOREIGN KEY (`author_id`) REFERENCES `assessment_peerworkflow` (`id`),
|
||||
CONSTRAINT `asses_assessment_id_15cadfae90ddcc2a_fk_assessment_assessment_id` FOREIGN KEY (`assessment_id`) REFERENCES `assessment_assessment` (`id`)
|
||||
CONSTRAINT `assessm_scorer_id_2d803ee2d52c0e2c_fk_assessment_peerworkflow_id` FOREIGN KEY (`scorer_id`) REFERENCES `assessment_peerworkflow` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `assessment_rubric`;
|
||||
@@ -345,8 +384,8 @@ CREATE TABLE `assessment_studenttrainingworkflowitem` (
|
||||
UNIQUE KEY `assessment_studenttrainingwork_workflow_id_484e930feb86ad74_uniq` (`workflow_id`,`order_num`),
|
||||
KEY `assessment_studenttrainingworkflowitem_9cc97abc` (`training_example_id`),
|
||||
KEY `assessment_studenttrainingworkflowitem_846c77cf` (`workflow_id`),
|
||||
CONSTRAINT `f9c080ebc7ad16394edda963ed3f280f` FOREIGN KEY (`workflow_id`) REFERENCES `assessment_studenttrainingworkflow` (`id`),
|
||||
CONSTRAINT `D74ce3e30635de397fef41ac869640c7` FOREIGN KEY (`training_example_id`) REFERENCES `assessment_trainingexample` (`id`)
|
||||
CONSTRAINT `D74ce3e30635de397fef41ac869640c7` FOREIGN KEY (`training_example_id`) REFERENCES `assessment_trainingexample` (`id`),
|
||||
CONSTRAINT `f9c080ebc7ad16394edda963ed3f280f` FOREIGN KEY (`workflow_id`) REFERENCES `assessment_studenttrainingworkflow` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `assessment_trainingexample`;
|
||||
@@ -385,7 +424,7 @@ CREATE TABLE `auth_group` (
|
||||
`name` varchar(80) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `name` (`name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `auth_group_permissions`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
@@ -412,7 +451,7 @@ CREATE TABLE `auth_permission` (
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `content_type_id` (`content_type_id`,`codename`),
|
||||
CONSTRAINT `auth__content_type_id_508cf46651277a81_fk_django_content_type_id` FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=755 DEFAULT CHARSET=utf8;
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=806 DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `auth_registration`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
@@ -506,6 +545,74 @@ CREATE TABLE `auth_userprofile` (
|
||||
CONSTRAINT `auth_userprofile_user_id_4c194f9b5650ad70_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `badges_badgeassertion`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `badges_badgeassertion` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`data` longtext NOT NULL,
|
||||
`backend` varchar(50) NOT NULL,
|
||||
`image_url` varchar(200) NOT NULL,
|
||||
`assertion_url` varchar(200) NOT NULL,
|
||||
`modified` datetime(6) NOT NULL,
|
||||
`created` datetime(6) NOT NULL,
|
||||
`badge_class_id` int(11) NOT NULL,
|
||||
`user_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `badges_badgeassertion_e2fa5388` (`created`),
|
||||
KEY `badges_badgeassertion_c389e456` (`badge_class_id`),
|
||||
KEY `badges_badgeassertion_e8701ad4` (`user_id`),
|
||||
CONSTRAINT `badges_b_badge_class_id_3a4a16cb833201e8_fk_badges_badgeclass_id` FOREIGN KEY (`badge_class_id`) REFERENCES `badges_badgeclass` (`id`),
|
||||
CONSTRAINT `badges_badgeassertion_user_id_14233cdefee1055a_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `badges_badgeclass`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `badges_badgeclass` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`slug` varchar(255) NOT NULL,
|
||||
`issuing_component` varchar(50) NOT NULL,
|
||||
`display_name` varchar(255) NOT NULL,
|
||||
`course_id` varchar(255) NOT NULL,
|
||||
`description` longtext NOT NULL,
|
||||
`criteria` longtext NOT NULL,
|
||||
`mode` varchar(100) NOT NULL,
|
||||
`image` varchar(100) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `badges_badgeclass_slug_7fe9eac3bca91f16_uniq` (`slug`,`issuing_component`,`course_id`),
|
||||
KEY `badges_badgeclass_2dbcba41` (`slug`),
|
||||
KEY `badges_badgeclass_a57403f2` (`issuing_component`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `badges_coursecompleteimageconfiguration`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `badges_coursecompleteimageconfiguration` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`mode` varchar(125) NOT NULL,
|
||||
`icon` varchar(100) NOT NULL,
|
||||
`default` tinyint(1) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `mode` (`mode`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `badges_courseeventbadgesconfiguration`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `badges_courseeventbadgesconfiguration` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`change_date` datetime(6) NOT NULL,
|
||||
`enabled` tinyint(1) NOT NULL,
|
||||
`courses_completed` longtext NOT NULL,
|
||||
`courses_enrolled` longtext NOT NULL,
|
||||
`course_groups` longtext NOT NULL,
|
||||
`changed_by_id` int(11) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `badges_courseeven_changed_by_id_50986a94d73238b9_fk_auth_user_id` (`changed_by_id`),
|
||||
CONSTRAINT `badges_courseeven_changed_by_id_50986a94d73238b9_fk_auth_user_id` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `bookmarks_bookmark`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
@@ -660,33 +767,6 @@ CREATE TABLE `celery_tasksetmeta` (
|
||||
KEY `celery_tasksetmeta_662f707d` (`hidden`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `certificates_badgeassertion`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `certificates_badgeassertion` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`course_id` varchar(255) NOT NULL,
|
||||
`mode` varchar(100) NOT NULL,
|
||||
`data` longtext NOT NULL,
|
||||
`user_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `certificates_badgeassertion_course_id_4dc2a23e1c0cd3ff_uniq` (`course_id`,`user_id`,`mode`),
|
||||
KEY `certificates_badgeassert_user_id_65bbbec450cd344_fk_auth_user_id` (`user_id`),
|
||||
CONSTRAINT `certificates_badgeassert_user_id_65bbbec450cd344_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `certificates_badgeimageconfiguration`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `certificates_badgeimageconfiguration` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`mode` varchar(125) NOT NULL,
|
||||
`icon` varchar(100) NOT NULL,
|
||||
`default` tinyint(1) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `mode` (`mode`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `certificates_certificategenerationconfiguration`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
@@ -974,8 +1054,8 @@ CREATE TABLE `course_action_state_coursererunstate` (
|
||||
KEY `course_action_state_coursererunstate_c8235886` (`course_key`),
|
||||
KEY `course_action_state_coursererunstate_418c5509` (`action`),
|
||||
KEY `course_action_state_coursererunstate_a9bd7343` (`source_course_key`),
|
||||
CONSTRAINT `course_action_s_updated_user_id_4fab18012332c9a4_fk_auth_user_id` FOREIGN KEY (`updated_user_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `course_action_s_created_user_id_7f53088ef8dccd0b_fk_auth_user_id` FOREIGN KEY (`created_user_id`) REFERENCES `auth_user` (`id`)
|
||||
CONSTRAINT `course_action_s_created_user_id_7f53088ef8dccd0b_fk_auth_user_id` FOREIGN KEY (`created_user_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `course_action_s_updated_user_id_4fab18012332c9a4_fk_auth_user_id` FOREIGN KEY (`updated_user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `course_creators_coursecreator`;
|
||||
@@ -1004,8 +1084,8 @@ CREATE TABLE `course_groups_cohortmembership` (
|
||||
UNIQUE KEY `course_groups_cohortmembership_user_id_395bddd0389ed7da_uniq` (`user_id`,`course_id`),
|
||||
KEY `course_groups_cohortmembership_6e438ee3` (`course_user_group_id`),
|
||||
KEY `course_groups_cohortmembership_e8701ad4` (`user_id`),
|
||||
CONSTRAINT `course_groups_cohortmem_user_id_15d408bf736398bf_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `D004e77c965054d46217a8bd48bcaec8` FOREIGN KEY (`course_user_group_id`) REFERENCES `course_groups_courseusergroup` (`id`)
|
||||
CONSTRAINT `D004e77c965054d46217a8bd48bcaec8` FOREIGN KEY (`course_user_group_id`) REFERENCES `course_groups_courseusergroup` (`id`),
|
||||
CONSTRAINT `course_groups_cohortmem_user_id_15d408bf736398bf_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `course_groups_coursecohort`;
|
||||
@@ -1143,7 +1223,6 @@ CREATE TABLE `course_overviews_courseoverview` (
|
||||
`end` datetime(6) DEFAULT NULL,
|
||||
`advertised_start` longtext,
|
||||
`course_image_url` longtext NOT NULL,
|
||||
`facebook_url` longtext,
|
||||
`social_sharing_url` longtext,
|
||||
`end_of_course_survey_url` longtext,
|
||||
`certificates_display_behavior` longtext,
|
||||
@@ -1168,6 +1247,7 @@ CREATE TABLE `course_overviews_courseoverview` (
|
||||
`effort` longtext,
|
||||
`short_description` longtext,
|
||||
`org` longtext NOT NULL,
|
||||
`self_paced` tinyint(1) NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
@@ -1229,6 +1309,20 @@ CREATE TABLE `course_structures_coursestructure` (
|
||||
UNIQUE KEY `course_id` (`course_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `coursetalk_coursetalkwidgetconfiguration`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `coursetalk_coursetalkwidgetconfiguration` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`change_date` datetime(6) NOT NULL,
|
||||
`enabled` tinyint(1) NOT NULL,
|
||||
`platform_key` varchar(50) NOT NULL,
|
||||
`changed_by_id` int(11) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `coursetalk_course_changed_by_id_18bd24020c1b37d5_fk_auth_user_id` (`changed_by_id`),
|
||||
CONSTRAINT `coursetalk_course_changed_by_id_18bd24020c1b37d5_fk_auth_user_id` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `courseware_offlinecomputedgrade`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
@@ -1400,6 +1494,20 @@ CREATE TABLE `credentials_credentialsapiconfig` (
|
||||
CONSTRAINT `credentials_crede_changed_by_id_273a2e6b0649c861_fk_auth_user_id` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `credit_creditconfig`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `credit_creditconfig` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`change_date` datetime(6) NOT NULL,
|
||||
`enabled` tinyint(1) NOT NULL,
|
||||
`cache_ttl` int(10) unsigned NOT NULL,
|
||||
`changed_by_id` int(11) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `credit_creditconf_changed_by_id_6270a800475f6694_fk_auth_user_id` (`changed_by_id`),
|
||||
CONSTRAINT `credit_creditconf_changed_by_id_6270a800475f6694_fk_auth_user_id` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `credit_creditcourse`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
@@ -1587,8 +1695,8 @@ CREATE TABLE `django_admin_log` (
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `djang_content_type_id_697914295151027a_fk_django_content_type_id` (`content_type_id`),
|
||||
KEY `django_admin_log_user_id_52fdd58701c5f563_fk_auth_user_id` (`user_id`),
|
||||
CONSTRAINT `django_admin_log_user_id_52fdd58701c5f563_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `djang_content_type_id_697914295151027a_fk_django_content_type_id` FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`)
|
||||
CONSTRAINT `djang_content_type_id_697914295151027a_fk_django_content_type_id` FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`),
|
||||
CONSTRAINT `django_admin_log_user_id_52fdd58701c5f563_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `django_comment_client_permission`;
|
||||
@@ -1609,8 +1717,8 @@ CREATE TABLE `django_comment_client_permission_roles` (
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `permission_id` (`permission_id`,`role_id`),
|
||||
KEY `django_role_id_558412c96ef7ba87_fk_django_comment_client_role_id` (`role_id`),
|
||||
CONSTRAINT `django_role_id_558412c96ef7ba87_fk_django_comment_client_role_id` FOREIGN KEY (`role_id`) REFERENCES `django_comment_client_role` (`id`),
|
||||
CONSTRAINT `D4e9a4067c1db9041491363f5e032121` FOREIGN KEY (`permission_id`) REFERENCES `django_comment_client_permission` (`name`)
|
||||
CONSTRAINT `D4e9a4067c1db9041491363f5e032121` FOREIGN KEY (`permission_id`) REFERENCES `django_comment_client_permission` (`name`),
|
||||
CONSTRAINT `django_role_id_558412c96ef7ba87_fk_django_comment_client_role_id` FOREIGN KEY (`role_id`) REFERENCES `django_comment_client_role` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `django_comment_client_role`;
|
||||
@@ -1647,7 +1755,7 @@ CREATE TABLE `django_content_type` (
|
||||
`model` varchar(100) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `django_content_type_app_label_45f3b1d93ec8c61c_uniq` (`app_label`,`model`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=251 DEFAULT CHARSET=utf8;
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=268 DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `django_migrations`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
@@ -1658,7 +1766,7 @@ CREATE TABLE `django_migrations` (
|
||||
`name` varchar(255) NOT NULL,
|
||||
`applied` datetime(6) NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=120 DEFAULT CHARSET=utf8;
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=139 DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `django_openid_auth_association`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
@@ -1766,8 +1874,8 @@ CREATE TABLE `djcelery_periodictask` (
|
||||
UNIQUE KEY `name` (`name`),
|
||||
KEY `djc_interval_id_20cfc1cad060dfad_fk_djcelery_intervalschedule_id` (`interval_id`),
|
||||
KEY `djcel_crontab_id_1d8228f5b44b680a_fk_djcelery_crontabschedule_id` (`crontab_id`),
|
||||
CONSTRAINT `djcel_crontab_id_1d8228f5b44b680a_fk_djcelery_crontabschedule_id` FOREIGN KEY (`crontab_id`) REFERENCES `djcelery_crontabschedule` (`id`),
|
||||
CONSTRAINT `djc_interval_id_20cfc1cad060dfad_fk_djcelery_intervalschedule_id` FOREIGN KEY (`interval_id`) REFERENCES `djcelery_intervalschedule` (`id`)
|
||||
CONSTRAINT `djc_interval_id_20cfc1cad060dfad_fk_djcelery_intervalschedule_id` FOREIGN KEY (`interval_id`) REFERENCES `djcelery_intervalschedule` (`id`),
|
||||
CONSTRAINT `djcel_crontab_id_1d8228f5b44b680a_fk_djcelery_crontabschedule_id` FOREIGN KEY (`crontab_id`) REFERENCES `djcelery_crontabschedule` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `djcelery_periodictasks`;
|
||||
@@ -1848,8 +1956,8 @@ CREATE TABLE `edxval_encodedvideo` (
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `edxval_encodedvideo_83a0eb3f` (`profile_id`),
|
||||
KEY `edxval_encodedvideo_b58b747e` (`video_id`),
|
||||
CONSTRAINT `edxval_encodedvideo_video_id_56934bca09fc3b13_fk_edxval_video_id` FOREIGN KEY (`video_id`) REFERENCES `edxval_video` (`id`),
|
||||
CONSTRAINT `edxval_encodedv_profile_id_484a111092acafb3_fk_edxval_profile_id` FOREIGN KEY (`profile_id`) REFERENCES `edxval_profile` (`id`)
|
||||
CONSTRAINT `edxval_encodedv_profile_id_484a111092acafb3_fk_edxval_profile_id` FOREIGN KEY (`profile_id`) REFERENCES `edxval_profile` (`id`),
|
||||
CONSTRAINT `edxval_encodedvideo_video_id_56934bca09fc3b13_fk_edxval_video_id` FOREIGN KEY (`video_id`) REFERENCES `edxval_video` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `edxval_profile`;
|
||||
@@ -2052,6 +2160,24 @@ CREATE TABLE `lms_xblock_xblockasidesconfig` (
|
||||
CONSTRAINT `lms_xblock_xblocka_changed_by_id_eabf5ef3e34dfb8_fk_auth_user_id` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `mentoring_answer`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `mentoring_answer` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(50) NOT NULL,
|
||||
`student_id` varchar(32) NOT NULL,
|
||||
`course_id` varchar(50) NOT NULL,
|
||||
`student_input` longtext NOT NULL,
|
||||
`created_on` datetime(6) NOT NULL,
|
||||
`modified_on` datetime(6) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `student_id` (`student_id`,`course_id`,`name`),
|
||||
KEY `mentoring_answer_b068931c` (`name`),
|
||||
KEY `mentoring_answer_30a811f6` (`student_id`),
|
||||
KEY `mentoring_answer_ea134da7` (`course_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `microsite_configuration_historicalmicrositeorganizationmapping`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
@@ -2337,8 +2463,8 @@ CREATE TABLE `notify_subscription` (
|
||||
PRIMARY KEY (`subscription_id`),
|
||||
KEY `a2462650bbefc26547210b80dec61069` (`notification_type_id`),
|
||||
KEY `notify_subscr_settings_id_64d594d127e8ca95_fk_notify_settings_id` (`settings_id`),
|
||||
CONSTRAINT `notify_subscr_settings_id_64d594d127e8ca95_fk_notify_settings_id` FOREIGN KEY (`settings_id`) REFERENCES `notify_settings` (`id`),
|
||||
CONSTRAINT `a2462650bbefc26547210b80dec61069` FOREIGN KEY (`notification_type_id`) REFERENCES `notify_notificationtype` (`key`)
|
||||
CONSTRAINT `a2462650bbefc26547210b80dec61069` FOREIGN KEY (`notification_type_id`) REFERENCES `notify_notificationtype` (`key`),
|
||||
CONSTRAINT `notify_subscr_settings_id_64d594d127e8ca95_fk_notify_settings_id` FOREIGN KEY (`settings_id`) REFERENCES `notify_settings` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `oauth2_accesstoken`;
|
||||
@@ -2355,8 +2481,8 @@ CREATE TABLE `oauth2_accesstoken` (
|
||||
KEY `oauth2_accesstoken_94a08da1` (`token`),
|
||||
KEY `oauth2_accesstoken_2bfe9d72` (`client_id`),
|
||||
KEY `oauth2_accesstoken_e8701ad4` (`user_id`),
|
||||
CONSTRAINT `oauth2_accesstoken_user_id_7a865c7085722378_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `oauth2_accesstoke_client_id_20c73b03a7c139a2_fk_oauth2_client_id` FOREIGN KEY (`client_id`) REFERENCES `oauth2_client` (`id`)
|
||||
CONSTRAINT `oauth2_accesstoke_client_id_20c73b03a7c139a2_fk_oauth2_client_id` FOREIGN KEY (`client_id`) REFERENCES `oauth2_client` (`id`),
|
||||
CONSTRAINT `oauth2_accesstoken_user_id_7a865c7085722378_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `oauth2_client`;
|
||||
@@ -2390,8 +2516,84 @@ CREATE TABLE `oauth2_grant` (
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `oauth2_grant_client_id_fbfc174fbc856af_fk_oauth2_client_id` (`client_id`),
|
||||
KEY `oauth2_grant_user_id_3de96a461bb76819_fk_auth_user_id` (`user_id`),
|
||||
CONSTRAINT `oauth2_grant_user_id_3de96a461bb76819_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `oauth2_grant_client_id_fbfc174fbc856af_fk_oauth2_client_id` FOREIGN KEY (`client_id`) REFERENCES `oauth2_client` (`id`)
|
||||
CONSTRAINT `oauth2_grant_client_id_fbfc174fbc856af_fk_oauth2_client_id` FOREIGN KEY (`client_id`) REFERENCES `oauth2_client` (`id`),
|
||||
CONSTRAINT `oauth2_grant_user_id_3de96a461bb76819_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `oauth2_provider_accesstoken`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `oauth2_provider_accesstoken` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`token` varchar(255) NOT NULL,
|
||||
`expires` datetime(6) NOT NULL,
|
||||
`scope` longtext NOT NULL,
|
||||
`application_id` int(11) NOT NULL,
|
||||
`user_id` int(11) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `D5ac3019ee1c474fd85718b015e3d3a1` (`application_id`),
|
||||
KEY `oauth2_provider_accesstoken_94a08da1` (`token`),
|
||||
KEY `oauth2_provider_accesst_user_id_5e2f004fdebea22d_fk_auth_user_id` (`user_id`),
|
||||
CONSTRAINT `D5ac3019ee1c474fd85718b015e3d3a1` FOREIGN KEY (`application_id`) REFERENCES `oauth2_provider_application` (`id`),
|
||||
CONSTRAINT `oauth2_provider_accesst_user_id_5e2f004fdebea22d_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `oauth2_provider_application`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `oauth2_provider_application` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`client_id` varchar(100) NOT NULL,
|
||||
`redirect_uris` longtext NOT NULL,
|
||||
`client_type` varchar(32) NOT NULL,
|
||||
`authorization_grant_type` varchar(32) NOT NULL,
|
||||
`client_secret` varchar(255) NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`user_id` int(11) NOT NULL,
|
||||
`skip_authorization` tinyint(1) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `client_id` (`client_id`),
|
||||
KEY `oauth2_provider_application_9d667c2b` (`client_secret`),
|
||||
KEY `oauth2_provider_applica_user_id_7fa13387c260b798_fk_auth_user_id` (`user_id`),
|
||||
CONSTRAINT `oauth2_provider_applica_user_id_7fa13387c260b798_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `oauth2_provider_grant`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `oauth2_provider_grant` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`code` varchar(255) NOT NULL,
|
||||
`expires` datetime(6) NOT NULL,
|
||||
`redirect_uri` varchar(255) NOT NULL,
|
||||
`scope` longtext NOT NULL,
|
||||
`application_id` int(11) NOT NULL,
|
||||
`user_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `D6b2a4f1402d4f338b690c38b795830a` (`application_id`),
|
||||
KEY `oauth2_provider_grant_user_id_3111344894d452da_fk_auth_user_id` (`user_id`),
|
||||
KEY `oauth2_provider_grant_c1336794` (`code`),
|
||||
CONSTRAINT `D6b2a4f1402d4f338b690c38b795830a` FOREIGN KEY (`application_id`) REFERENCES `oauth2_provider_application` (`id`),
|
||||
CONSTRAINT `oauth2_provider_grant_user_id_3111344894d452da_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `oauth2_provider_refreshtoken`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `oauth2_provider_refreshtoken` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`token` varchar(255) NOT NULL,
|
||||
`access_token_id` int(11) NOT NULL,
|
||||
`application_id` int(11) NOT NULL,
|
||||
`user_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `access_token_id` (`access_token_id`),
|
||||
KEY `d3e264ceec355cabed6ff9976fc42a06` (`application_id`),
|
||||
KEY `oauth2_provider_refresh_user_id_3f695b639cfbc9a3_fk_auth_user_id` (`user_id`),
|
||||
KEY `oauth2_provider_refreshtoken_94a08da1` (`token`),
|
||||
CONSTRAINT `b58d9cb3b93afb36b11b7741bf1bcc1a` FOREIGN KEY (`access_token_id`) REFERENCES `oauth2_provider_accesstoken` (`id`),
|
||||
CONSTRAINT `d3e264ceec355cabed6ff9976fc42a06` FOREIGN KEY (`application_id`) REFERENCES `oauth2_provider_application` (`id`),
|
||||
CONSTRAINT `oauth2_provider_refresh_user_id_3f695b639cfbc9a3_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `oauth2_provider_trustedclient`;
|
||||
@@ -2419,9 +2621,9 @@ CREATE TABLE `oauth2_refreshtoken` (
|
||||
UNIQUE KEY `access_token_id` (`access_token_id`),
|
||||
KEY `oauth2_refreshtok_client_id_2f55036ac9aa614e_fk_oauth2_client_id` (`client_id`),
|
||||
KEY `oauth2_refreshtoken_user_id_acecf94460b787c_fk_auth_user_id` (`user_id`),
|
||||
CONSTRAINT `oauth2_refreshtoken_user_id_acecf94460b787c_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `oauth2__access_token_id_f99377d503a000b_fk_oauth2_accesstoken_id` FOREIGN KEY (`access_token_id`) REFERENCES `oauth2_accesstoken` (`id`),
|
||||
CONSTRAINT `oauth2_refreshtok_client_id_2f55036ac9aa614e_fk_oauth2_client_id` FOREIGN KEY (`client_id`) REFERENCES `oauth2_client` (`id`),
|
||||
CONSTRAINT `oauth2__access_token_id_f99377d503a000b_fk_oauth2_accesstoken_id` FOREIGN KEY (`access_token_id`) REFERENCES `oauth2_accesstoken` (`id`)
|
||||
CONSTRAINT `oauth2_refreshtoken_user_id_acecf94460b787c_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `oauth_provider_consumer`;
|
||||
@@ -2485,9 +2687,9 @@ CREATE TABLE `oauth_provider_token` (
|
||||
KEY `oauth_consumer_id_1b9915b5bcf1ee5b_fk_oauth_provider_consumer_id` (`consumer_id`),
|
||||
KEY `oauth_provi_scope_id_459821b6fecbc02a_fk_oauth_provider_scope_id` (`scope_id`),
|
||||
KEY `oauth_provider_token_user_id_588adbcffc892186_fk_auth_user_id` (`user_id`),
|
||||
CONSTRAINT `oauth_provider_token_user_id_588adbcffc892186_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `oauth_consumer_id_1b9915b5bcf1ee5b_fk_oauth_provider_consumer_id` FOREIGN KEY (`consumer_id`) REFERENCES `oauth_provider_consumer` (`id`),
|
||||
CONSTRAINT `oauth_provi_scope_id_459821b6fecbc02a_fk_oauth_provider_scope_id` FOREIGN KEY (`scope_id`) REFERENCES `oauth_provider_scope` (`id`)
|
||||
CONSTRAINT `oauth_provi_scope_id_459821b6fecbc02a_fk_oauth_provider_scope_id` FOREIGN KEY (`scope_id`) REFERENCES `oauth_provider_scope` (`id`),
|
||||
CONSTRAINT `oauth_provider_token_user_id_588adbcffc892186_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `organizations_organization`;
|
||||
@@ -2524,6 +2726,43 @@ CREATE TABLE `organizations_organizationcourse` (
|
||||
CONSTRAINT `a7b04b16eba98e518fbe21d390bd8e3e` FOREIGN KEY (`organization_id`) REFERENCES `organizations_organization` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `problem_builder_answer`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `problem_builder_answer` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(50) NOT NULL,
|
||||
`student_id` varchar(32) NOT NULL,
|
||||
`course_id` varchar(50) NOT NULL,
|
||||
`student_input` longtext NOT NULL,
|
||||
`created_on` datetime(6) NOT NULL,
|
||||
`modified_on` datetime(6) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `problem_builder_answer_student_id_2f6847a9fb3e9385_uniq` (`student_id`,`course_id`,`name`),
|
||||
KEY `problem_builder_answer_b068931c` (`name`),
|
||||
KEY `problem_builder_answer_30a811f6` (`student_id`),
|
||||
KEY `problem_builder_answer_ea134da7` (`course_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `problem_builder_share`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `problem_builder_share` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`submission_uid` varchar(32) NOT NULL,
|
||||
`block_id` varchar(255) NOT NULL,
|
||||
`notified` tinyint(1) NOT NULL,
|
||||
`shared_by_id` int(11) NOT NULL,
|
||||
`shared_with_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `problem_builder_share_shared_by_id_4e845ea266d66e1_uniq` (`shared_by_id`,`shared_with_id`,`block_id`),
|
||||
KEY `problem_builder__shared_with_id_573844d7dca07647_fk_auth_user_id` (`shared_with_id`),
|
||||
KEY `problem_builder_share_7e53bca2` (`block_id`),
|
||||
KEY `problem_builder_share_e559ad34` (`notified`),
|
||||
CONSTRAINT `problem_builder__shared_with_id_573844d7dca07647_fk_auth_user_id` FOREIGN KEY (`shared_with_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `problem_builder_sh_shared_by_id_35201b15adc664ce_fk_auth_user_id` FOREIGN KEY (`shared_by_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `proctoring_proctoredexam`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
@@ -2560,8 +2799,8 @@ CREATE TABLE `proctoring_proctoredexamreviewpolicy` (
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `D32bab97500954b362d3f768dd89b6da` (`proctored_exam_id`),
|
||||
KEY `proctoring_proct_set_by_user_id_75a66580aa44cd84_fk_auth_user_id` (`set_by_user_id`),
|
||||
CONSTRAINT `proctoring_proct_set_by_user_id_75a66580aa44cd84_fk_auth_user_id` FOREIGN KEY (`set_by_user_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `D32bab97500954b362d3f768dd89b6da` FOREIGN KEY (`proctored_exam_id`) REFERENCES `proctoring_proctoredexam` (`id`)
|
||||
CONSTRAINT `D32bab97500954b362d3f768dd89b6da` FOREIGN KEY (`proctored_exam_id`) REFERENCES `proctoring_proctoredexam` (`id`),
|
||||
CONSTRAINT `proctoring_proct_set_by_user_id_75a66580aa44cd84_fk_auth_user_id` FOREIGN KEY (`set_by_user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `proctoring_proctoredexamreviewpolicyhistory`;
|
||||
@@ -2579,8 +2818,8 @@ CREATE TABLE `proctoring_proctoredexamreviewpolicyhistory` (
|
||||
KEY `d9965d8af87bebd0587414ca1ba4826f` (`proctored_exam_id`),
|
||||
KEY `proctoring_procto_set_by_user_id_31fae610848d90f_fk_auth_user_id` (`set_by_user_id`),
|
||||
KEY `proctoring_proctoredexamreviewpolicyhistory_524b09d0` (`original_id`),
|
||||
CONSTRAINT `proctoring_procto_set_by_user_id_31fae610848d90f_fk_auth_user_id` FOREIGN KEY (`set_by_user_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `d9965d8af87bebd0587414ca1ba4826f` FOREIGN KEY (`proctored_exam_id`) REFERENCES `proctoring_proctoredexam` (`id`)
|
||||
CONSTRAINT `d9965d8af87bebd0587414ca1ba4826f` FOREIGN KEY (`proctored_exam_id`) REFERENCES `proctoring_proctoredexam` (`id`),
|
||||
CONSTRAINT `proctoring_procto_set_by_user_id_31fae610848d90f_fk_auth_user_id` FOREIGN KEY (`set_by_user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `proctoring_proctoredexamsoftwaresecurereview`;
|
||||
@@ -2603,9 +2842,9 @@ CREATE TABLE `proctoring_proctoredexamsoftwaresecurereview` (
|
||||
KEY `proctoring_proct_reviewed_by_id_4cff67b7de094f65_fk_auth_user_id` (`reviewed_by_id`),
|
||||
KEY `proctoring_proctored_student_id_14c182517b0cbb5b_fk_auth_user_id` (`student_id`),
|
||||
KEY `proctoring_proctoredexamsoftwaresecurereview_b38e5b0e` (`attempt_code`),
|
||||
CONSTRAINT `proctoring_proctored_student_id_14c182517b0cbb5b_fk_auth_user_id` FOREIGN KEY (`student_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `proctori_exam_id_635059f5fe2cc392_fk_proctoring_proctoredexam_id` FOREIGN KEY (`exam_id`) REFERENCES `proctoring_proctoredexam` (`id`),
|
||||
CONSTRAINT `proctoring_proct_reviewed_by_id_4cff67b7de094f65_fk_auth_user_id` FOREIGN KEY (`reviewed_by_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `proctori_exam_id_635059f5fe2cc392_fk_proctoring_proctoredexam_id` FOREIGN KEY (`exam_id`) REFERENCES `proctoring_proctoredexam` (`id`)
|
||||
CONSTRAINT `proctoring_proctored_student_id_14c182517b0cbb5b_fk_auth_user_id` FOREIGN KEY (`student_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `proctoring_proctoredexamsoftwaresecurereviewhistory`;
|
||||
@@ -2627,9 +2866,9 @@ CREATE TABLE `proctoring_proctoredexamsoftwaresecurereviewhistory` (
|
||||
KEY `proctoring_proct_reviewed_by_id_139568d0bf423998_fk_auth_user_id` (`reviewed_by_id`),
|
||||
KEY `proctoring_proctored_student_id_6922ba3b791462d8_fk_auth_user_id` (`student_id`),
|
||||
KEY `proctoring_proctoredexamsoftwaresecurereviewhistory_b38e5b0e` (`attempt_code`),
|
||||
CONSTRAINT `proctoring_proctored_student_id_6922ba3b791462d8_fk_auth_user_id` FOREIGN KEY (`student_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `proctori_exam_id_73969ae423813477_fk_proctoring_proctoredexam_id` FOREIGN KEY (`exam_id`) REFERENCES `proctoring_proctoredexam` (`id`),
|
||||
CONSTRAINT `proctoring_proct_reviewed_by_id_139568d0bf423998_fk_auth_user_id` FOREIGN KEY (`reviewed_by_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `proctori_exam_id_73969ae423813477_fk_proctoring_proctoredexam_id` FOREIGN KEY (`exam_id`) REFERENCES `proctoring_proctoredexam` (`id`)
|
||||
CONSTRAINT `proctoring_proctored_student_id_6922ba3b791462d8_fk_auth_user_id` FOREIGN KEY (`student_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `proctoring_proctoredexamstudentallowance`;
|
||||
@@ -2646,8 +2885,8 @@ CREATE TABLE `proctoring_proctoredexamstudentallowance` (
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `proctoring_proctoredexamstudentall_user_id_665ed945152c2f60_uniq` (`user_id`,`proctored_exam_id`,`key`),
|
||||
KEY `db55b83a7875e70b3a0ebd1f81a898d8` (`proctored_exam_id`),
|
||||
CONSTRAINT `proctoring_proctoredexam_user_id_a0a0681d4a01661_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `db55b83a7875e70b3a0ebd1f81a898d8` FOREIGN KEY (`proctored_exam_id`) REFERENCES `proctoring_proctoredexam` (`id`)
|
||||
CONSTRAINT `db55b83a7875e70b3a0ebd1f81a898d8` FOREIGN KEY (`proctored_exam_id`) REFERENCES `proctoring_proctoredexam` (`id`),
|
||||
CONSTRAINT `proctoring_proctoredexam_user_id_a0a0681d4a01661_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `proctoring_proctoredexamstudentallowancehistory`;
|
||||
@@ -2665,8 +2904,8 @@ CREATE TABLE `proctoring_proctoredexamstudentallowancehistory` (
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `D169ec97a7fca1dbf6b0bb2929d41ccc` (`proctored_exam_id`),
|
||||
KEY `proctoring_proctoredexa_user_id_68e25e3abb187580_fk_auth_user_id` (`user_id`),
|
||||
CONSTRAINT `proctoring_proctoredexa_user_id_68e25e3abb187580_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `D169ec97a7fca1dbf6b0bb2929d41ccc` FOREIGN KEY (`proctored_exam_id`) REFERENCES `proctoring_proctoredexam` (`id`)
|
||||
CONSTRAINT `D169ec97a7fca1dbf6b0bb2929d41ccc` FOREIGN KEY (`proctored_exam_id`) REFERENCES `proctoring_proctoredexam` (`id`),
|
||||
CONSTRAINT `proctoring_proctoredexa_user_id_68e25e3abb187580_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `proctoring_proctoredexamstudentattempt`;
|
||||
@@ -2745,8 +2984,8 @@ CREATE TABLE `proctoring_proctoredexamstudentattempthistory` (
|
||||
KEY `proctoring_proctoredexa_user_id_59ce75db7c4fc769_fk_auth_user_id` (`user_id`),
|
||||
KEY `proctoring_proctoredexamstudentattempthistory_b38e5b0e` (`attempt_code`),
|
||||
KEY `proctoring_proctoredexamstudentattempthistory_0e684294` (`external_id`),
|
||||
CONSTRAINT `proctoring_proctoredexa_user_id_59ce75db7c4fc769_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `cbccbfd5c4c427541fdce96e77e6bf6c` FOREIGN KEY (`proctored_exam_id`) REFERENCES `proctoring_proctoredexam` (`id`)
|
||||
CONSTRAINT `cbccbfd5c4c427541fdce96e77e6bf6c` FOREIGN KEY (`proctored_exam_id`) REFERENCES `proctoring_proctoredexam` (`id`),
|
||||
CONSTRAINT `proctoring_proctoredexa_user_id_59ce75db7c4fc769_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `programs_programsapiconfig`;
|
||||
@@ -2767,6 +3006,7 @@ CREATE TABLE `programs_programsapiconfig` (
|
||||
`enable_studio_tab` tinyint(1) NOT NULL,
|
||||
`enable_certification` tinyint(1) NOT NULL,
|
||||
`max_retries` int(10) unsigned NOT NULL,
|
||||
`xseries_ad_enabled` tinyint(1) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `programs_programsa_changed_by_id_b7c3b49d5c0dcd3_fk_auth_user_id` (`changed_by_id`),
|
||||
CONSTRAINT `programs_programsa_changed_by_id_b7c3b49d5c0dcd3_fk_auth_user_id` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`)
|
||||
@@ -2845,9 +3085,9 @@ CREATE TABLE `shoppingcart_couponredemption` (
|
||||
KEY `shoppingcar_coupon_id_1afa016627ac44bb_fk_shoppingcart_coupon_id` (`coupon_id`),
|
||||
KEY `shoppingcart_couponredemption_69dfcb07` (`order_id`),
|
||||
KEY `shoppingcart_couponredemption_e8701ad4` (`user_id`),
|
||||
CONSTRAINT `shoppingcart_couponredemp_user_id_f5b814b7d92666_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `shoppingcar_coupon_id_1afa016627ac44bb_fk_shoppingcart_coupon_id` FOREIGN KEY (`coupon_id`) REFERENCES `shoppingcart_coupon` (`id`),
|
||||
CONSTRAINT `shoppingcart__order_id_5ba031c3bfaf643a_fk_shoppingcart_order_id` FOREIGN KEY (`order_id`) REFERENCES `shoppingcart_order` (`id`),
|
||||
CONSTRAINT `shoppingcar_coupon_id_1afa016627ac44bb_fk_shoppingcart_coupon_id` FOREIGN KEY (`coupon_id`) REFERENCES `shoppingcart_coupon` (`id`)
|
||||
CONSTRAINT `shoppingcart_couponredemp_user_id_f5b814b7d92666_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `shoppingcart_courseregcodeitem`;
|
||||
@@ -2896,9 +3136,9 @@ CREATE TABLE `shoppingcart_courseregistrationcode` (
|
||||
KEY `shoppingcart_courseregistrationcode_69dfcb07` (`order_id`),
|
||||
KEY `shoppingcart_courseregistrationcode_7a471658` (`invoice_item_id`),
|
||||
CONSTRAINT `f040030b6361304bd87eb40c09a82094` FOREIGN KEY (`invoice_item_id`) REFERENCES `shoppingcart_courseregistrationcodeinvoiceitem` (`invoiceitem_ptr_id`),
|
||||
CONSTRAINT `shoppingcart_cour_created_by_id_11125a9667aa01c9_fk_auth_user_id` FOREIGN KEY (`created_by_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `shoppingc_invoice_id_422f26bdc7c5cb99_fk_shoppingcart_invoice_id` FOREIGN KEY (`invoice_id`) REFERENCES `shoppingcart_invoice` (`id`),
|
||||
CONSTRAINT `shoppingcart__order_id_279d7e2df3fe6b6a_fk_shoppingcart_order_id` FOREIGN KEY (`order_id`) REFERENCES `shoppingcart_order` (`id`),
|
||||
CONSTRAINT `shoppingc_invoice_id_422f26bdc7c5cb99_fk_shoppingcart_invoice_id` FOREIGN KEY (`invoice_id`) REFERENCES `shoppingcart_invoice` (`id`)
|
||||
CONSTRAINT `shoppingcart_cour_created_by_id_11125a9667aa01c9_fk_auth_user_id` FOREIGN KEY (`created_by_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `shoppingcart_courseregistrationcodeinvoiceitem`;
|
||||
@@ -3014,9 +3254,9 @@ CREATE TABLE `shoppingcart_invoicetransaction` (
|
||||
KEY `shoppingcart_invoi_created_by_id_f5f3d90ce55a145_fk_auth_user_id` (`created_by_id`),
|
||||
KEY `shoppingc_invoice_id_66bdbfa6f029288b_fk_shoppingcart_invoice_id` (`invoice_id`),
|
||||
KEY `shoppingcar_last_modified_by_id_5e10e433f9576d91_fk_auth_user_id` (`last_modified_by_id`),
|
||||
CONSTRAINT `shoppingc_invoice_id_66bdbfa6f029288b_fk_shoppingcart_invoice_id` FOREIGN KEY (`invoice_id`) REFERENCES `shoppingcart_invoice` (`id`),
|
||||
CONSTRAINT `shoppingcar_last_modified_by_id_5e10e433f9576d91_fk_auth_user_id` FOREIGN KEY (`last_modified_by_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `shoppingcart_invoi_created_by_id_f5f3d90ce55a145_fk_auth_user_id` FOREIGN KEY (`created_by_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `shoppingc_invoice_id_66bdbfa6f029288b_fk_shoppingcart_invoice_id` FOREIGN KEY (`invoice_id`) REFERENCES `shoppingcart_invoice` (`id`)
|
||||
CONSTRAINT `shoppingcart_invoi_created_by_id_f5f3d90ce55a145_fk_auth_user_id` FOREIGN KEY (`created_by_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `shoppingcart_order`;
|
||||
@@ -3077,8 +3317,8 @@ CREATE TABLE `shoppingcart_orderitem` (
|
||||
KEY `shoppingcart_orderitem_76ed2946` (`refund_requested_time`),
|
||||
KEY `shoppingcart_orderitem_69dfcb07` (`order_id`),
|
||||
KEY `shoppingcart_orderitem_e8701ad4` (`user_id`),
|
||||
CONSTRAINT `shoppingcart_orderitem_user_id_5708ec7aabe24a31_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `shoppingcart__order_id_325e5347f18743e3_fk_shoppingcart_order_id` FOREIGN KEY (`order_id`) REFERENCES `shoppingcart_order` (`id`)
|
||||
CONSTRAINT `shoppingcart__order_id_325e5347f18743e3_fk_shoppingcart_order_id` FOREIGN KEY (`order_id`) REFERENCES `shoppingcart_order` (`id`),
|
||||
CONSTRAINT `shoppingcart_orderitem_user_id_5708ec7aabe24a31_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `shoppingcart_paidcourseregistration`;
|
||||
@@ -3125,8 +3365,34 @@ CREATE TABLE `shoppingcart_registrationcoderedemption` (
|
||||
KEY `D1ed44c4be114e424571929bce972f54` (`registration_code_id`),
|
||||
CONSTRAINT `D1ed44c4be114e424571929bce972f54` FOREIGN KEY (`registration_code_id`) REFERENCES `shoppingcart_courseregistrationcode` (`id`),
|
||||
CONSTRAINT `D6654a8efe686d45804b6116dfc6bee1` FOREIGN KEY (`course_enrollment_id`) REFERENCES `student_courseenrollment` (`id`),
|
||||
CONSTRAINT `shoppingcart_reg_redeemed_by_id_455df2dd74004fff_fk_auth_user_id` FOREIGN KEY (`redeemed_by_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `shoppingcart_r_order_id_752ddc3003afe96_fk_shoppingcart_order_id` FOREIGN KEY (`order_id`) REFERENCES `shoppingcart_order` (`id`)
|
||||
CONSTRAINT `shoppingcart_r_order_id_752ddc3003afe96_fk_shoppingcart_order_id` FOREIGN KEY (`order_id`) REFERENCES `shoppingcart_order` (`id`),
|
||||
CONSTRAINT `shoppingcart_reg_redeemed_by_id_455df2dd74004fff_fk_auth_user_id` FOREIGN KEY (`redeemed_by_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `site_configuration_siteconfiguration`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `site_configuration_siteconfiguration` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`values` longtext NOT NULL,
|
||||
`site_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `site_id` (`site_id`),
|
||||
CONSTRAINT `site_configuration_si_site_id_51c4aa24ab9238cb_fk_django_site_id` FOREIGN KEY (`site_id`) REFERENCES `django_site` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `site_configuration_siteconfigurationhistory`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `site_configuration_siteconfigurationhistory` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`created` datetime(6) NOT NULL,
|
||||
`modified` datetime(6) NOT NULL,
|
||||
`values` longtext NOT NULL,
|
||||
`site_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `site_configuration_si_site_id_20c9c1a5f8c3358e_fk_django_site_id` (`site_id`),
|
||||
CONSTRAINT `site_configuration_si_site_id_20c9c1a5f8c3358e_fk_django_site_id` FOREIGN KEY (`site_id`) REFERENCES `django_site` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `social_auth_association`;
|
||||
@@ -3531,8 +3797,8 @@ CREATE TABLE `student_userstanding` (
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `user_id` (`user_id`),
|
||||
KEY `student_userstand_changed_by_id_23784b83f2849aff_fk_auth_user_id` (`changed_by_id`),
|
||||
CONSTRAINT `student_userstanding_user_id_6bb90abaaa05d42e_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `student_userstand_changed_by_id_23784b83f2849aff_fk_auth_user_id` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`)
|
||||
CONSTRAINT `student_userstand_changed_by_id_23784b83f2849aff_fk_auth_user_id` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `student_userstanding_user_id_6bb90abaaa05d42e_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `student_usertestgroup`;
|
||||
@@ -3556,8 +3822,8 @@ CREATE TABLE `student_usertestgroup_users` (
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `usertestgroup_id` (`usertestgroup_id`,`user_id`),
|
||||
KEY `student_usertestgroup_u_user_id_26c886de60cceacb_fk_auth_user_id` (`user_id`),
|
||||
CONSTRAINT `student_usertestgroup_u_user_id_26c886de60cceacb_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `st_usertestgroup_id_3d634741f1dd4e4f_fk_student_usertestgroup_id` FOREIGN KEY (`usertestgroup_id`) REFERENCES `student_usertestgroup` (`id`)
|
||||
CONSTRAINT `st_usertestgroup_id_3d634741f1dd4e4f_fk_student_usertestgroup_id` FOREIGN KEY (`usertestgroup_id`) REFERENCES `student_usertestgroup` (`id`),
|
||||
CONSTRAINT `student_usertestgroup_u_user_id_26c886de60cceacb_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `submissions_score`;
|
||||
@@ -3575,8 +3841,8 @@ CREATE TABLE `submissions_score` (
|
||||
KEY `submissions_score_fde81f11` (`created_at`),
|
||||
KEY `submissions_score_02d5e83e` (`student_item_id`),
|
||||
KEY `submissions_score_1dd9cfcc` (`submission_id`),
|
||||
CONSTRAINT `subm_submission_id_3fc975fe88442ff7_fk_submissions_submission_id` FOREIGN KEY (`submission_id`) REFERENCES `submissions_submission` (`id`),
|
||||
CONSTRAINT `s_student_item_id_7d4d4bb6a7dd0642_fk_submissions_studentitem_id` FOREIGN KEY (`student_item_id`) REFERENCES `submissions_studentitem` (`id`)
|
||||
CONSTRAINT `s_student_item_id_7d4d4bb6a7dd0642_fk_submissions_studentitem_id` FOREIGN KEY (`student_item_id`) REFERENCES `submissions_studentitem` (`id`),
|
||||
CONSTRAINT `subm_submission_id_3fc975fe88442ff7_fk_submissions_submission_id` FOREIGN KEY (`submission_id`) REFERENCES `submissions_submission` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `submissions_scoreannotation`;
|
||||
@@ -3608,8 +3874,8 @@ CREATE TABLE `submissions_scoresummary` (
|
||||
KEY `submissions__highest_id_7fd91b8eb312c175_fk_submissions_score_id` (`highest_id`),
|
||||
KEY `submissions_s_latest_id_2b352506a35fd569_fk_submissions_score_id` (`latest_id`),
|
||||
CONSTRAINT `s_student_item_id_32fa0a425a149b1b_fk_submissions_studentitem_id` FOREIGN KEY (`student_item_id`) REFERENCES `submissions_studentitem` (`id`),
|
||||
CONSTRAINT `submissions_s_latest_id_2b352506a35fd569_fk_submissions_score_id` FOREIGN KEY (`latest_id`) REFERENCES `submissions_score` (`id`),
|
||||
CONSTRAINT `submissions__highest_id_7fd91b8eb312c175_fk_submissions_score_id` FOREIGN KEY (`highest_id`) REFERENCES `submissions_score` (`id`)
|
||||
CONSTRAINT `submissions__highest_id_7fd91b8eb312c175_fk_submissions_score_id` FOREIGN KEY (`highest_id`) REFERENCES `submissions_score` (`id`),
|
||||
CONSTRAINT `submissions_s_latest_id_2b352506a35fd569_fk_submissions_score_id` FOREIGN KEY (`latest_id`) REFERENCES `submissions_score` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `submissions_studentitem`;
|
||||
@@ -3665,8 +3931,8 @@ CREATE TABLE `survey_surveyanswer` (
|
||||
KEY `survey_surveyanswer_c8235886` (`course_key`),
|
||||
KEY `survey_surveyanswer_d6cba1ad` (`form_id`),
|
||||
KEY `survey_surveyanswer_e8701ad4` (`user_id`),
|
||||
CONSTRAINT `survey_surveyanswer_user_id_4e77d83a82fd0b2b_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `survey_surveyan_form_id_1c835afe12a54912_fk_survey_surveyform_id` FOREIGN KEY (`form_id`) REFERENCES `survey_surveyform` (`id`)
|
||||
CONSTRAINT `survey_surveyan_form_id_1c835afe12a54912_fk_survey_surveyform_id` FOREIGN KEY (`form_id`) REFERENCES `survey_surveyform` (`id`),
|
||||
CONSTRAINT `survey_surveyanswer_user_id_4e77d83a82fd0b2b_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `survey_surveyform`;
|
||||
@@ -3720,8 +3986,20 @@ CREATE TABLE `teams_courseteammembership` (
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `teams_courseteammembership_user_id_48efa8e8971947c3_uniq` (`user_id`,`team_id`),
|
||||
KEY `teams_courseteam_team_id_594700d19b04f922_fk_teams_courseteam_id` (`team_id`),
|
||||
CONSTRAINT `teams_courseteammembers_user_id_2d93b28be22c3c40_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `teams_courseteam_team_id_594700d19b04f922_fk_teams_courseteam_id` FOREIGN KEY (`team_id`) REFERENCES `teams_courseteam` (`id`)
|
||||
CONSTRAINT `teams_courseteam_team_id_594700d19b04f922_fk_teams_courseteam_id` FOREIGN KEY (`team_id`) REFERENCES `teams_courseteam` (`id`),
|
||||
CONSTRAINT `teams_courseteammembers_user_id_2d93b28be22c3c40_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `theming_sitetheme`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `theming_sitetheme` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`theme_dir_name` varchar(255) NOT NULL,
|
||||
`site_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `theming_sitetheme_site_id_4fccdacaebfeb01f_fk_django_site_id` (`site_id`),
|
||||
CONSTRAINT `theming_sitetheme_site_id_4fccdacaebfeb01f_fk_django_site_id` FOREIGN KEY (`site_id`) REFERENCES `django_site` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `third_party_auth_ltiproviderconfig`;
|
||||
@@ -3741,6 +4019,7 @@ CREATE TABLE `third_party_auth_ltiproviderconfig` (
|
||||
`lti_consumer_secret` varchar(255) NOT NULL,
|
||||
`lti_max_timestamp_age` int(11) NOT NULL,
|
||||
`changed_by_id` int(11) DEFAULT NULL,
|
||||
`icon_image` varchar(100) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `third_party_auth__changed_by_id_7749e09fd5f71ab0_fk_auth_user_id` (`changed_by_id`),
|
||||
KEY `third_party_auth_ltiproviderconfig_fe8da584` (`lti_hostname`),
|
||||
@@ -3764,6 +4043,7 @@ CREATE TABLE `third_party_auth_oauth2providerconfig` (
|
||||
`secret` longtext NOT NULL,
|
||||
`other_settings` longtext NOT NULL,
|
||||
`changed_by_id` int(11) DEFAULT NULL,
|
||||
`icon_image` varchar(100) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `third_party_auth__changed_by_id_17044d1cd96e8d57_fk_auth_user_id` (`changed_by_id`),
|
||||
KEY `third_party_auth_oauth2providerconfig_abcd61c0` (`backend_name`),
|
||||
@@ -3824,6 +4104,7 @@ CREATE TABLE `third_party_auth_samlproviderconfig` (
|
||||
`attr_email` varchar(128) NOT NULL,
|
||||
`other_settings` longtext NOT NULL,
|
||||
`changed_by_id` int(11) DEFAULT NULL,
|
||||
`icon_image` varchar(100) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `third_party_auth__changed_by_id_508190ecd0b0e845_fk_auth_user_id` (`changed_by_id`),
|
||||
KEY `third_party_auth_samlproviderconfig_098674f1` (`idp_slug`),
|
||||
@@ -3934,6 +4215,17 @@ CREATE TABLE `util_ratelimitconfiguration` (
|
||||
CONSTRAINT `util_ratelimitcon_changed_by_id_2c8891cb4854f3b5_fk_auth_user_id` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `verified_track_content_verifiedtrackcohortedcourse`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `verified_track_content_verifiedtrackcohortedcourse` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`course_key` varchar(255) NOT NULL,
|
||||
`enabled` tinyint(1) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `course_key` (`course_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `verify_student_historicalverificationdeadline`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
@@ -3995,8 +4287,8 @@ CREATE TABLE `verify_student_skippedreverification` (
|
||||
KEY `verify_student_skippedreverification_ea134da7` (`course_id`),
|
||||
KEY `verify_student_skippedreverification_bef2d98a` (`checkpoint_id`),
|
||||
KEY `verify_student_skippedreverification_e8701ad4` (`user_id`),
|
||||
CONSTRAINT `verify_student_skippedr_user_id_6752b392e3d3c501_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `D759ffa5ca66ef1a2c8c200f7a21365b` FOREIGN KEY (`checkpoint_id`) REFERENCES `verify_student_verificationcheckpoint` (`id`)
|
||||
CONSTRAINT `D759ffa5ca66ef1a2c8c200f7a21365b` FOREIGN KEY (`checkpoint_id`) REFERENCES `verify_student_verificationcheckpoint` (`id`),
|
||||
CONSTRAINT `verify_student_skippedr_user_id_6752b392e3d3c501_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `verify_student_softwaresecurephotoverification`;
|
||||
@@ -4030,9 +4322,9 @@ CREATE TABLE `verify_student_softwaresecurephotoverification` (
|
||||
KEY `verify_student_softwaresecurephotoverification_afd1a1a8` (`updated_at`),
|
||||
KEY `verify_student_softwaresecurephotoverification_ebf78b51` (`display`),
|
||||
KEY `verify_student_softwaresecurephotoverification_22bb6ff9` (`submitted_at`),
|
||||
CONSTRAINT `verify_student_software_user_id_61ffab9c12020106_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `D01dce17b91c9382bd80d4be23a3e0cf` FOREIGN KEY (`copy_id_photo_from_id`) REFERENCES `verify_student_softwaresecurephotoverification` (`id`),
|
||||
CONSTRAINT `verify_studen_reviewing_user_id_727fae1d0bcf8aaf_fk_auth_user_id` FOREIGN KEY (`reviewing_user_id`) REFERENCES `auth_user` (`id`)
|
||||
CONSTRAINT `verify_studen_reviewing_user_id_727fae1d0bcf8aaf_fk_auth_user_id` FOREIGN KEY (`reviewing_user_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `verify_student_software_user_id_61ffab9c12020106_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `verify_student_verificationcheckpoint`;
|
||||
@@ -4090,8 +4382,8 @@ CREATE TABLE `verify_student_verificationstatus` (
|
||||
KEY `D4cefb6d3d71c9b26af2a5ece4c37277` (`checkpoint_id`),
|
||||
KEY `verify_student_verifica_user_id_5c19fcd6dc05f211_fk_auth_user_id` (`user_id`),
|
||||
KEY `verify_student_verificationstatus_9acb4454` (`status`),
|
||||
CONSTRAINT `verify_student_verifica_user_id_5c19fcd6dc05f211_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `D4cefb6d3d71c9b26af2a5ece4c37277` FOREIGN KEY (`checkpoint_id`) REFERENCES `verify_student_verificationcheckpoint` (`id`)
|
||||
CONSTRAINT `D4cefb6d3d71c9b26af2a5ece4c37277` FOREIGN KEY (`checkpoint_id`) REFERENCES `verify_student_verificationcheckpoint` (`id`),
|
||||
CONSTRAINT `verify_student_verifica_user_id_5c19fcd6dc05f211_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `wiki_article`;
|
||||
@@ -4112,9 +4404,9 @@ CREATE TABLE `wiki_article` (
|
||||
UNIQUE KEY `current_revision_id` (`current_revision_id`),
|
||||
KEY `wiki_article_0e939a4f` (`group_id`),
|
||||
KEY `wiki_article_5e7b1936` (`owner_id`),
|
||||
CONSTRAINT `wiki_article_owner_id_b1c1e44609a378f_fk_auth_user_id` FOREIGN KEY (`owner_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `current_revision_id_42a9dbec1e0dd15c_fk_wiki_articlerevision_id` FOREIGN KEY (`current_revision_id`) REFERENCES `wiki_articlerevision` (`id`),
|
||||
CONSTRAINT `wiki_article_group_id_2b38601b6aa39f3d_fk_auth_group_id` FOREIGN KEY (`group_id`) REFERENCES `auth_group` (`id`)
|
||||
CONSTRAINT `wiki_article_group_id_2b38601b6aa39f3d_fk_auth_group_id` FOREIGN KEY (`group_id`) REFERENCES `auth_group` (`id`),
|
||||
CONSTRAINT `wiki_article_owner_id_b1c1e44609a378f_fk_auth_user_id` FOREIGN KEY (`owner_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `wiki_articleforobject`;
|
||||
@@ -4168,9 +4460,9 @@ CREATE TABLE `wiki_articlerevision` (
|
||||
UNIQUE KEY `wiki_articlerevision_article_id_4b4e7910c8e7b2d0_uniq` (`article_id`,`revision_number`),
|
||||
KEY `fae2b1c6e892c699844d5dda69aeb89e` (`previous_revision_id`),
|
||||
KEY `wiki_articlerevision_user_id_183520686b6ead55_fk_auth_user_id` (`user_id`),
|
||||
CONSTRAINT `wiki_articlerevision_user_id_183520686b6ead55_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `fae2b1c6e892c699844d5dda69aeb89e` FOREIGN KEY (`previous_revision_id`) REFERENCES `wiki_articlerevision` (`id`),
|
||||
CONSTRAINT `wiki_articlerevis_article_id_1f2c587981af1463_fk_wiki_article_id` FOREIGN KEY (`article_id`) REFERENCES `wiki_article` (`id`)
|
||||
CONSTRAINT `wiki_articlerevis_article_id_1f2c587981af1463_fk_wiki_article_id` FOREIGN KEY (`article_id`) REFERENCES `wiki_article` (`id`),
|
||||
CONSTRAINT `wiki_articlerevision_user_id_183520686b6ead55_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `wiki_attachment`;
|
||||
@@ -4208,9 +4500,9 @@ CREATE TABLE `wiki_attachmentrevision` (
|
||||
KEY `wiki_attachmentrevision_07ba63f5` (`attachment_id`),
|
||||
KEY `wiki_attachmentrevision_e8680b8a` (`previous_revision_id`),
|
||||
KEY `wiki_attachmentrevision_e8701ad4` (`user_id`),
|
||||
CONSTRAINT `wiki_attachmentrevision_user_id_427e3f452b4bfdcd_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `D68d5cd540b66f536228137e518081f8` FOREIGN KEY (`attachment_id`) REFERENCES `wiki_attachment` (`reusableplugin_ptr_id`),
|
||||
CONSTRAINT `D8c1f0a8f0ddceb9c3ebc94379fe22c9` FOREIGN KEY (`previous_revision_id`) REFERENCES `wiki_attachmentrevision` (`id`)
|
||||
CONSTRAINT `D8c1f0a8f0ddceb9c3ebc94379fe22c9` FOREIGN KEY (`previous_revision_id`) REFERENCES `wiki_attachmentrevision` (`id`),
|
||||
CONSTRAINT `wiki_attachmentrevision_user_id_427e3f452b4bfdcd_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `wiki_image`;
|
||||
@@ -4253,8 +4545,8 @@ CREATE TABLE `wiki_reusableplugin_articles` (
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `reusableplugin_id` (`reusableplugin_id`,`article_id`),
|
||||
KEY `wiki_reusableplug_article_id_5e893d3b3fb4f7fa_fk_wiki_article_id` (`article_id`),
|
||||
CONSTRAINT `wiki_reusableplug_article_id_5e893d3b3fb4f7fa_fk_wiki_article_id` FOREIGN KEY (`article_id`) REFERENCES `wiki_article` (`id`),
|
||||
CONSTRAINT `a9f9f50fd4e8fdafe7ffc0c1a145fee3` FOREIGN KEY (`reusableplugin_id`) REFERENCES `wiki_reusableplugin` (`articleplugin_ptr_id`)
|
||||
CONSTRAINT `a9f9f50fd4e8fdafe7ffc0c1a145fee3` FOREIGN KEY (`reusableplugin_id`) REFERENCES `wiki_reusableplugin` (`articleplugin_ptr_id`),
|
||||
CONSTRAINT `wiki_reusableplug_article_id_5e893d3b3fb4f7fa_fk_wiki_article_id` FOREIGN KEY (`article_id`) REFERENCES `wiki_article` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `wiki_revisionplugin`;
|
||||
@@ -4289,9 +4581,9 @@ CREATE TABLE `wiki_revisionpluginrevision` (
|
||||
KEY `wiki_revisionpluginrevision_b25eaab4` (`plugin_id`),
|
||||
KEY `wiki_revisionpluginrevision_e8680b8a` (`previous_revision_id`),
|
||||
KEY `wiki_revisionpluginrevision_e8701ad4` (`user_id`),
|
||||
CONSTRAINT `wiki_revisionpluginrevi_user_id_55a00bd0e2532762_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `D9574e2f57b828a85a24838761473871` FOREIGN KEY (`plugin_id`) REFERENCES `wiki_revisionplugin` (`articleplugin_ptr_id`),
|
||||
CONSTRAINT `e524c4f887e857f93c39356f7cf7d4df` FOREIGN KEY (`previous_revision_id`) REFERENCES `wiki_revisionpluginrevision` (`id`)
|
||||
CONSTRAINT `e524c4f887e857f93c39356f7cf7d4df` FOREIGN KEY (`previous_revision_id`) REFERENCES `wiki_revisionpluginrevision` (`id`),
|
||||
CONSTRAINT `wiki_revisionpluginrevi_user_id_55a00bd0e2532762_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `wiki_simpleplugin`;
|
||||
@@ -4328,9 +4620,9 @@ CREATE TABLE `wiki_urlpath` (
|
||||
KEY `wiki_urlpath_656442a0` (`tree_id`),
|
||||
KEY `wiki_urlpath_c9e9a848` (`level`),
|
||||
KEY `wiki_urlpath_6be37982` (`parent_id`),
|
||||
CONSTRAINT `wiki_urlpath_site_id_4f30e731b0464e80_fk_django_site_id` FOREIGN KEY (`site_id`) REFERENCES `django_site` (`id`),
|
||||
CONSTRAINT `wiki_urlpath_article_id_1d1c5eb9a64e1390_fk_wiki_article_id` FOREIGN KEY (`article_id`) REFERENCES `wiki_article` (`id`),
|
||||
CONSTRAINT `wiki_urlpath_parent_id_24eab80cd168595f_fk_wiki_urlpath_id` FOREIGN KEY (`parent_id`) REFERENCES `wiki_urlpath` (`id`)
|
||||
CONSTRAINT `wiki_urlpath_parent_id_24eab80cd168595f_fk_wiki_urlpath_id` FOREIGN KEY (`parent_id`) REFERENCES `wiki_urlpath` (`id`),
|
||||
CONSTRAINT `wiki_urlpath_site_id_4f30e731b0464e80_fk_django_site_id` FOREIGN KEY (`site_id`) REFERENCES `django_site` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `workflow_assessmentworkflow`;
|
||||
|
||||
@@ -35,7 +35,7 @@ CREATE TABLE `django_migrations` (
|
||||
`name` varchar(255) NOT NULL,
|
||||
`applied` datetime(6) NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=120 DEFAULT CHARSET=utf8;
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=139 DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -110,5 +110,504 @@
|
||||
"company_identifier": "7nTFLiuDkkQkdELSpruCwD4F6jzqtTFsx3PfJUIT2qHqXRLG1",
|
||||
"trk_partner_name": "edx"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"display_name": "Test Badge",
|
||||
"description": "Yay! It's a test badge.",
|
||||
"image": "badge_classes/test_Nln8nhu.png",
|
||||
"issuing_component": "test_component",
|
||||
"mode": "honor",
|
||||
"criteria": "https://example.com/syllabus",
|
||||
"course_id": null,
|
||||
"slug": "test_slug_0_0768205214811"
|
||||
},
|
||||
"model": "badges.badgeclass",
|
||||
"pk": 1
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created": "2016-03-22T15:35:21.401Z",
|
||||
"modified": "2016-03-22T15:35:21.403Z",
|
||||
"image_url": "http://example.com/image.png",
|
||||
"user": 99,
|
||||
"badge_class": 1,
|
||||
"data": "{}",
|
||||
"backend": ""
|
||||
},
|
||||
"model": "badges.badgeassertion",
|
||||
"pk": 1
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created": "2016-03-22T15:35:21.412Z",
|
||||
"modified": "2016-03-22T15:35:21.413Z",
|
||||
"image_url": "http://example.com/image.png",
|
||||
"user": 99,
|
||||
"badge_class": 1,
|
||||
"data": "{}",
|
||||
"backend": ""
|
||||
},
|
||||
"model": "badges.badgeassertion",
|
||||
"pk": 2
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created": "2016-03-22T15:35:21.422Z",
|
||||
"modified": "2016-03-22T15:35:21.423Z",
|
||||
"image_url": "http://example.com/image.png",
|
||||
"user": 99,
|
||||
"badge_class": 1,
|
||||
"data": "{}",
|
||||
"backend": ""
|
||||
},
|
||||
"model": "badges.badgeassertion",
|
||||
"pk": 3
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created": "2016-03-22T15:35:21.437Z",
|
||||
"modified": "2016-03-22T15:35:21.437Z",
|
||||
"image_url": "http://example.com/image.png",
|
||||
"user": 99,
|
||||
"badge_class": 1,
|
||||
"data": "{}",
|
||||
"backend": ""
|
||||
},
|
||||
"model": "badges.badgeassertion",
|
||||
"pk": 4
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created": "2016-03-22T15:35:21.444Z",
|
||||
"modified": "2016-03-22T15:35:21.445Z",
|
||||
"image_url": "http://example.com/image.png",
|
||||
"user": 99,
|
||||
"badge_class": 1,
|
||||
"data": "{}",
|
||||
"backend": ""
|
||||
},
|
||||
"model": "badges.badgeassertion",
|
||||
"pk": 5
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created": "2016-03-22T15:35:21.451Z",
|
||||
"modified": "2016-03-22T15:35:21.451Z",
|
||||
"image_url": "http://example.com/image.png",
|
||||
"user": 99,
|
||||
"badge_class": 1,
|
||||
"data": "{}",
|
||||
"backend": ""
|
||||
},
|
||||
"model": "badges.badgeassertion",
|
||||
"pk": 6
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created": "2016-03-22T15:35:21.459Z",
|
||||
"modified": "2016-03-22T15:35:21.459Z",
|
||||
"image_url": "http://example.com/image.png",
|
||||
"user": 99,
|
||||
"badge_class": 1,
|
||||
"data": "{}",
|
||||
"backend": ""
|
||||
},
|
||||
"model": "badges.badgeassertion",
|
||||
"pk": 7
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created": "2016-03-22T15:35:21.467Z",
|
||||
"modified": "2016-03-22T15:35:21.467Z",
|
||||
"image_url": "http://example.com/image.png",
|
||||
"user": 99,
|
||||
"badge_class": 1,
|
||||
"data": "{}",
|
||||
"backend": ""
|
||||
},
|
||||
"model": "badges.badgeassertion",
|
||||
"pk": 8
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created": "2016-03-22T15:35:21.475Z",
|
||||
"modified": "2016-03-22T15:35:21.476Z",
|
||||
"image_url": "http://example.com/image.png",
|
||||
"user": 99,
|
||||
"badge_class": 1,
|
||||
"data": "{}",
|
||||
"backend": ""
|
||||
},
|
||||
"model": "badges.badgeassertion",
|
||||
"pk": 9
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created": "2016-03-22T15:35:21.488Z",
|
||||
"modified": "2016-03-22T15:35:21.489Z",
|
||||
"image_url": "http://example.com/image.png",
|
||||
"user": 99,
|
||||
"badge_class": 1,
|
||||
"data": "{}",
|
||||
"backend": ""
|
||||
},
|
||||
"model": "badges.badgeassertion",
|
||||
"pk": 10
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created": "2016-03-22T15:35:21.495Z",
|
||||
"modified": "2016-03-22T15:35:21.496Z",
|
||||
"image_url": "http://example.com/image.png",
|
||||
"user": 99,
|
||||
"badge_class": 1,
|
||||
"data": "{}",
|
||||
"backend": ""
|
||||
},
|
||||
"model": "badges.badgeassertion",
|
||||
"pk": 11
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created": "2016-03-22T15:35:21.503Z",
|
||||
"modified": "2016-03-22T15:35:21.504Z",
|
||||
"image_url": "http://example.com/image.png",
|
||||
"user": 99,
|
||||
"badge_class": 1,
|
||||
"data": "{}",
|
||||
"backend": ""
|
||||
},
|
||||
"model": "badges.badgeassertion",
|
||||
"pk": 12
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created": "2016-03-22T15:35:21.511Z",
|
||||
"modified": "2016-03-22T15:35:21.512Z",
|
||||
"image_url": "http://example.com/image.png",
|
||||
"user": 99,
|
||||
"badge_class": 1,
|
||||
"data": "{}",
|
||||
"backend": ""
|
||||
},
|
||||
"model": "badges.badgeassertion",
|
||||
"pk": 13
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created": "2016-03-22T15:35:21.519Z",
|
||||
"modified": "2016-03-22T15:35:21.519Z",
|
||||
"image_url": "http://example.com/image.png",
|
||||
"user": 99,
|
||||
"badge_class": 1,
|
||||
"data": "{}",
|
||||
"backend": ""
|
||||
},
|
||||
"model": "badges.badgeassertion",
|
||||
"pk": 14
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created": "2016-03-22T15:35:21.532Z",
|
||||
"modified": "2016-03-22T15:35:21.534Z",
|
||||
"image_url": "http://example.com/image.png",
|
||||
"user": 99,
|
||||
"badge_class": 1,
|
||||
"data": "{}",
|
||||
"backend": ""
|
||||
},
|
||||
"model": "badges.badgeassertion",
|
||||
"pk": 15
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created": "2016-03-22T15:35:21.542Z",
|
||||
"modified": "2016-03-22T15:35:21.544Z",
|
||||
"image_url": "http://example.com/image.png",
|
||||
"user": 99,
|
||||
"badge_class": 1,
|
||||
"data": "{}",
|
||||
"backend": ""
|
||||
},
|
||||
"model": "badges.badgeassertion",
|
||||
"pk": 16
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created": "2016-03-22T15:35:21.551Z",
|
||||
"modified": "2016-03-22T15:35:21.552Z",
|
||||
"image_url": "http://example.com/image.png",
|
||||
"user": 99,
|
||||
"badge_class": 1,
|
||||
"data": "{}",
|
||||
"backend": ""
|
||||
},
|
||||
"model": "badges.badgeassertion",
|
||||
"pk": 17
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created": "2016-03-22T15:35:21.559Z",
|
||||
"modified": "2016-03-22T15:35:21.560Z",
|
||||
"image_url": "http://example.com/image.png",
|
||||
"user": 99,
|
||||
"badge_class": 1,
|
||||
"data": "{}",
|
||||
"backend": ""
|
||||
},
|
||||
"model": "badges.badgeassertion",
|
||||
"pk": 18
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created": "2016-03-22T15:35:21.566Z",
|
||||
"modified": "2016-03-22T15:35:21.566Z",
|
||||
"image_url": "http://example.com/image.png",
|
||||
"user": 99,
|
||||
"badge_class": 1,
|
||||
"data": "{}",
|
||||
"backend": ""
|
||||
},
|
||||
"model": "badges.badgeassertion",
|
||||
"pk": 19
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created": "2016-03-22T15:35:21.573Z",
|
||||
"modified": "2016-03-22T15:35:21.574Z",
|
||||
"image_url": "http://example.com/image.png",
|
||||
"user": 99,
|
||||
"badge_class": 1,
|
||||
"data": "{}",
|
||||
"backend": ""
|
||||
},
|
||||
"model": "badges.badgeassertion",
|
||||
"pk": 20
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created": "2016-03-22T15:35:21.592Z",
|
||||
"modified": "2016-03-22T15:35:21.592Z",
|
||||
"image_url": "http://example.com/image.png",
|
||||
"user": 99,
|
||||
"badge_class": 1,
|
||||
"data": "{}",
|
||||
"backend": ""
|
||||
},
|
||||
"model": "badges.badgeassertion",
|
||||
"pk": 21
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created": "2016-03-22T15:35:21.600Z",
|
||||
"modified": "2016-03-22T15:35:21.600Z",
|
||||
"image_url": "http://example.com/image.png",
|
||||
"user": 99,
|
||||
"badge_class": 1,
|
||||
"data": "{}",
|
||||
"backend": ""
|
||||
},
|
||||
"model": "badges.badgeassertion",
|
||||
"pk": 22
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created": "2016-03-22T15:35:21.607Z",
|
||||
"modified": "2016-03-22T15:35:21.608Z",
|
||||
"image_url": "http://example.com/image.png",
|
||||
"user": 99,
|
||||
"badge_class": 1,
|
||||
"data": "{}",
|
||||
"backend": ""
|
||||
},
|
||||
"model": "badges.badgeassertion",
|
||||
"pk": 23
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created": "2016-03-22T15:35:21.617Z",
|
||||
"modified": "2016-03-22T15:35:21.618Z",
|
||||
"image_url": "http://example.com/image.png",
|
||||
"user": 99,
|
||||
"badge_class": 1,
|
||||
"data": "{}",
|
||||
"backend": ""
|
||||
},
|
||||
"model": "badges.badgeassertion",
|
||||
"pk": 24
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created": "2016-03-22T15:35:21.624Z",
|
||||
"modified": "2016-03-22T15:35:21.625Z",
|
||||
"image_url": "http://example.com/image.png",
|
||||
"user": 99,
|
||||
"badge_class": 1,
|
||||
"data": "{}",
|
||||
"backend": ""
|
||||
},
|
||||
"model": "badges.badgeassertion",
|
||||
"pk": 25
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created": "2016-03-22T15:35:21.632Z",
|
||||
"modified": "2016-03-22T15:35:21.632Z",
|
||||
"image_url": "http://example.com/image.png",
|
||||
"user": 99,
|
||||
"badge_class": 1,
|
||||
"data": "{}",
|
||||
"backend": ""
|
||||
},
|
||||
"model": "badges.badgeassertion",
|
||||
"pk": 26
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created": "2016-03-22T15:35:21.645Z",
|
||||
"modified": "2016-03-22T15:35:21.646Z",
|
||||
"image_url": "http://example.com/image.png",
|
||||
"user": 99,
|
||||
"badge_class": 1,
|
||||
"data": "{}",
|
||||
"backend": ""
|
||||
},
|
||||
"model": "badges.badgeassertion",
|
||||
"pk": 27
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created": "2016-03-22T15:35:21.653Z",
|
||||
"modified": "2016-03-22T15:35:21.653Z",
|
||||
"image_url": "http://example.com/image.png",
|
||||
"user": 99,
|
||||
"badge_class": 1,
|
||||
"data": "{}",
|
||||
"backend": ""
|
||||
},
|
||||
"model": "badges.badgeassertion",
|
||||
"pk": 28
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created": "2016-03-22T15:35:21.659Z",
|
||||
"modified": "2016-03-22T15:35:21.659Z",
|
||||
"image_url": "http://example.com/image.png",
|
||||
"user": 99,
|
||||
"badge_class": 1,
|
||||
"data": "{}",
|
||||
"backend": ""
|
||||
},
|
||||
"model": "badges.badgeassertion",
|
||||
"pk": 29
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created": "2016-03-22T15:35:21.667Z",
|
||||
"modified": "2016-03-22T15:35:21.667Z",
|
||||
"image_url": "http://example.com/image.png",
|
||||
"user": 99,
|
||||
"badge_class": 1,
|
||||
"data": "{}",
|
||||
"backend": ""
|
||||
},
|
||||
"model": "badges.badgeassertion",
|
||||
"pk": 30
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created": "2016-03-22T15:35:21.675Z",
|
||||
"modified": "2016-03-22T15:35:21.676Z",
|
||||
"image_url": "http://example.com/image.png",
|
||||
"user": 99,
|
||||
"badge_class": 1,
|
||||
"data": "{}",
|
||||
"backend": ""
|
||||
},
|
||||
"model": "badges.badgeassertion",
|
||||
"pk": 31
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created": "2016-03-22T15:35:21.693Z",
|
||||
"modified": "2016-03-22T15:35:21.695Z",
|
||||
"image_url": "http://example.com/image.png",
|
||||
"user": 99,
|
||||
"badge_class": 1,
|
||||
"data": "{}",
|
||||
"backend": ""
|
||||
},
|
||||
"model": "badges.badgeassertion",
|
||||
"pk": 32
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created": "2016-03-22T15:35:21.704Z",
|
||||
"modified": "2016-03-22T15:35:21.705Z",
|
||||
"image_url": "http://example.com/image.png",
|
||||
"user": 99,
|
||||
"badge_class": 1,
|
||||
"data": "{}",
|
||||
"backend": ""
|
||||
},
|
||||
"model": "badges.badgeassertion",
|
||||
"pk": 33
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created": "2016-03-22T15:35:21.715Z",
|
||||
"modified": "2016-03-22T15:35:21.716Z",
|
||||
"image_url": "http://example.com/image.png",
|
||||
"user": 99,
|
||||
"badge_class": 1,
|
||||
"data": "{}",
|
||||
"backend": ""
|
||||
},
|
||||
"model": "badges.badgeassertion",
|
||||
"pk": 34
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"default": true,
|
||||
"mode": "honor",
|
||||
"icon": "course_complete_badges/honor.png"
|
||||
},
|
||||
"model": "badges.coursecompleteimageconfiguration",
|
||||
"pk": 1
|
||||
}
|
||||
]
|
||||
|
||||
0
lms/djangoapps/badges/__init__.py
Normal file
0
lms/djangoapps/badges/__init__.py
Normal file
11
lms/djangoapps/badges/admin.py
Normal file
11
lms/djangoapps/badges/admin.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""
|
||||
Admin registration for Badge Models
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from badges.models import CourseCompleteImageConfiguration, CourseEventBadgesConfiguration, BadgeClass
|
||||
from config_models.admin import ConfigurationModelAdmin
|
||||
|
||||
admin.site.register(CourseCompleteImageConfiguration)
|
||||
admin.site.register(BadgeClass)
|
||||
# Use the standard Configuration Model Admin handler for this model.
|
||||
admin.site.register(CourseEventBadgesConfiguration, ConfigurationModelAdmin)
|
||||
0
lms/djangoapps/badges/api/__init__.py
Normal file
0
lms/djangoapps/badges/api/__init__.py
Normal file
28
lms/djangoapps/badges/api/serializers.py
Normal file
28
lms/djangoapps/badges/api/serializers.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
Serializers for Badges
|
||||
"""
|
||||
from rest_framework import serializers
|
||||
|
||||
from badges.models import BadgeClass, BadgeAssertion
|
||||
|
||||
|
||||
class BadgeClassSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializer for BadgeClass model.
|
||||
"""
|
||||
image_url = serializers.ImageField(source='image')
|
||||
|
||||
class Meta(object):
|
||||
model = BadgeClass
|
||||
fields = ('slug', 'issuing_component', 'display_name', 'course_id', 'description', 'criteria', 'image_url')
|
||||
|
||||
|
||||
class BadgeAssertionSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializer for the BadgeAssertion model.
|
||||
"""
|
||||
badge_class = BadgeClassSerializer(read_only=True)
|
||||
|
||||
class Meta(object):
|
||||
model = BadgeAssertion
|
||||
fields = ('badge_class', 'image_url', 'assertion_url', 'created')
|
||||
224
lms/djangoapps/badges/api/tests.py
Normal file
224
lms/djangoapps/badges/api/tests.py
Normal file
@@ -0,0 +1,224 @@
|
||||
"""
|
||||
Tests for the badges API views.
|
||||
"""
|
||||
from ddt import ddt, data, unpack
|
||||
from django.conf import settings
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from badges.tests.factories import BadgeAssertionFactory, BadgeClassFactory, RandomBadgeClassFactory
|
||||
from openedx.core.lib.api.test_utils import ApiTestCase
|
||||
from student.tests.factories import UserFactory
|
||||
from util.testing import UrlResetMixin
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
FEATURES_WITH_BADGES_ENABLED = settings.FEATURES.copy()
|
||||
FEATURES_WITH_BADGES_ENABLED['ENABLE_OPENBADGES'] = True
|
||||
|
||||
|
||||
@override_settings(FEATURES=FEATURES_WITH_BADGES_ENABLED)
|
||||
class UserAssertionTestCase(UrlResetMixin, ModuleStoreTestCase, ApiTestCase):
|
||||
"""
|
||||
Mixin for badge API tests.
|
||||
"""
|
||||
|
||||
def setUp(self, *args, **kwargs):
|
||||
super(UserAssertionTestCase, self).setUp(*args, **kwargs)
|
||||
self.course = CourseFactory.create()
|
||||
self.user = UserFactory.create()
|
||||
# Password defined by factory.
|
||||
self.client.login(username=self.user.username, password='test')
|
||||
|
||||
def url(self):
|
||||
"""
|
||||
Return the URL to look up the current user's assertions.
|
||||
"""
|
||||
return '/api/badges/v1/assertions/user/{}/'.format(self.user.username)
|
||||
|
||||
def check_class_structure(self, badge_class, json_class):
|
||||
"""
|
||||
Check a JSON response against a known badge class.
|
||||
"""
|
||||
self.assertEqual(badge_class.issuing_component, json_class['issuing_component'])
|
||||
self.assertEqual(badge_class.slug, json_class['slug'])
|
||||
self.assertIn(badge_class.image.url, json_class['image_url'])
|
||||
self.assertEqual(badge_class.description, json_class['description'])
|
||||
self.assertEqual(badge_class.criteria, json_class['criteria'])
|
||||
self.assertEqual(badge_class.course_id and unicode(badge_class.course_id), json_class['course_id'])
|
||||
|
||||
def check_assertion_structure(self, assertion, json_assertion):
|
||||
"""
|
||||
Check a JSON response against a known assertion object.
|
||||
"""
|
||||
self.assertEqual(assertion.image_url, json_assertion['image_url'])
|
||||
self.assertEqual(assertion.assertion_url, json_assertion['assertion_url'])
|
||||
self.check_class_structure(assertion.badge_class, json_assertion['badge_class'])
|
||||
|
||||
def get_course_id(self, wildcard, badge_class):
|
||||
"""
|
||||
Used for tests which may need to test for a course_id or a wildcard.
|
||||
"""
|
||||
if wildcard:
|
||||
return '*'
|
||||
else:
|
||||
return unicode(badge_class.course_id)
|
||||
|
||||
def create_badge_class(self, check_course, **kwargs):
|
||||
"""
|
||||
Create a badge class, using a course id if it's relevant to the URL pattern.
|
||||
"""
|
||||
if check_course:
|
||||
return RandomBadgeClassFactory.create(course_id=self.course.location.course_key, **kwargs)
|
||||
return RandomBadgeClassFactory.create(**kwargs)
|
||||
|
||||
def get_qs_args(self, check_course, wildcard, badge_class):
|
||||
"""
|
||||
Get a dictionary to be serialized into querystring params based on class settings.
|
||||
"""
|
||||
qs_args = {
|
||||
'issuing_component': badge_class.issuing_component,
|
||||
'slug': badge_class.slug,
|
||||
}
|
||||
if check_course:
|
||||
qs_args['course_id'] = self.get_course_id(wildcard, badge_class)
|
||||
return qs_args
|
||||
|
||||
|
||||
class TestUserBadgeAssertions(UserAssertionTestCase):
|
||||
"""
|
||||
Test the general badge assertions retrieval view.
|
||||
"""
|
||||
|
||||
def test_get_assertions(self):
|
||||
"""
|
||||
Verify we can get all of a user's badge assertions.
|
||||
"""
|
||||
for dummy in range(3):
|
||||
BadgeAssertionFactory(user=self.user)
|
||||
# Add in a course scoped badge-- these should not be excluded from the full listing.
|
||||
BadgeAssertionFactory(user=self.user, badge_class=BadgeClassFactory(course_id=self.course.location.course_key))
|
||||
# Should not be included.
|
||||
for dummy in range(3):
|
||||
self.create_badge_class(False)
|
||||
response = self.get_json(self.url())
|
||||
# pylint: disable=no-member
|
||||
self.assertEqual(len(response['results']), 4)
|
||||
|
||||
def test_assertion_structure(self):
|
||||
badge_class = self.create_badge_class(False)
|
||||
assertion = BadgeAssertionFactory.create(user=self.user, badge_class=badge_class)
|
||||
response = self.get_json(self.url())
|
||||
# pylint: disable=no-member
|
||||
self.check_assertion_structure(assertion, response['results'][0])
|
||||
|
||||
|
||||
class TestUserCourseBadgeAssertions(UserAssertionTestCase):
|
||||
"""
|
||||
Test the Badge Assertions view with the course_id filter.
|
||||
"""
|
||||
|
||||
def test_get_assertions(self):
|
||||
"""
|
||||
Verify we can get assertions via the course_id and username.
|
||||
"""
|
||||
course_key = self.course.location.course_key
|
||||
badge_class = BadgeClassFactory.create(course_id=course_key)
|
||||
for dummy in range(3):
|
||||
BadgeAssertionFactory.create(user=self.user, badge_class=badge_class)
|
||||
# Should not be included, as they don't share the target badge class.
|
||||
for dummy in range(3):
|
||||
BadgeAssertionFactory.create(user=self.user)
|
||||
# Also should not be included, as they don't share the same user.
|
||||
for dummy in range(6):
|
||||
BadgeAssertionFactory.create(badge_class=badge_class)
|
||||
response = self.get_json(self.url(), data={'course_id': course_key})
|
||||
# pylint: disable=no-member
|
||||
self.assertEqual(len(response['results']), 3)
|
||||
unused_course = CourseFactory.create()
|
||||
response = self.get_json(self.url(), data={'course_id': unused_course.location.course_key})
|
||||
# pylint: disable=no-member
|
||||
self.assertEqual(len(response['results']), 0)
|
||||
|
||||
def test_assertion_structure(self):
|
||||
"""
|
||||
Verify the badge assertion structure is as expected when a course is involved.
|
||||
"""
|
||||
course_key = self.course.location.course_key
|
||||
badge_class = BadgeClassFactory.create(course_id=course_key)
|
||||
assertion = BadgeAssertionFactory.create(badge_class=badge_class, user=self.user)
|
||||
response = self.get_json(self.url())
|
||||
# pylint: disable=no-member
|
||||
self.check_assertion_structure(assertion, response['results'][0])
|
||||
|
||||
|
||||
@ddt
|
||||
class TestUserBadgeAssertionsByClass(UserAssertionTestCase):
|
||||
"""
|
||||
Test the Badge Assertions view with the badge class filter.
|
||||
"""
|
||||
|
||||
@unpack
|
||||
@data((False, False), (True, False), (True, True))
|
||||
def test_get_assertions(self, check_course, wildcard):
|
||||
"""
|
||||
Verify we can get assertions via the badge class and username.
|
||||
"""
|
||||
badge_class = self.create_badge_class(check_course)
|
||||
for dummy in range(3):
|
||||
BadgeAssertionFactory.create(user=self.user, badge_class=badge_class)
|
||||
if badge_class.course_id:
|
||||
# Also create a version of this badge under a different course.
|
||||
alt_class = BadgeClassFactory.create(
|
||||
slug=badge_class.slug, issuing_component=badge_class.issuing_component,
|
||||
course_id=CourseFactory.create().location.course_key
|
||||
)
|
||||
BadgeAssertionFactory.create(user=self.user, badge_class=alt_class)
|
||||
# Same badge class, but different user. Should not show up in the list.
|
||||
for dummy in range(5):
|
||||
BadgeAssertionFactory.create(badge_class=badge_class)
|
||||
# Different badge class AND different user. Certainly shouldn't show up in the list!
|
||||
for dummy in range(6):
|
||||
BadgeAssertionFactory.create()
|
||||
|
||||
response = self.get_json(
|
||||
self.url(),
|
||||
data=self.get_qs_args(check_course, wildcard, badge_class),
|
||||
)
|
||||
if wildcard:
|
||||
expected_length = 4
|
||||
else:
|
||||
expected_length = 3
|
||||
# pylint: disable=no-member
|
||||
self.assertEqual(len(response['results']), expected_length)
|
||||
unused_class = self.create_badge_class(check_course, slug='unused_slug', issuing_component='unused_component')
|
||||
|
||||
response = self.get_json(
|
||||
self.url(),
|
||||
data=self.get_qs_args(check_course, wildcard, unused_class),
|
||||
)
|
||||
# pylint: disable=no-member
|
||||
self.assertEqual(len(response['results']), 0)
|
||||
|
||||
def check_badge_class_assertion(self, check_course, wildcard, badge_class):
|
||||
"""
|
||||
Given a badge class, create an assertion for the current user and fetch it, checking the structure.
|
||||
"""
|
||||
assertion = BadgeAssertionFactory.create(badge_class=badge_class, user=self.user)
|
||||
response = self.get_json(
|
||||
self.url(),
|
||||
data=self.get_qs_args(check_course, wildcard, badge_class),
|
||||
)
|
||||
# pylint: disable=no-member
|
||||
self.check_assertion_structure(assertion, response['results'][0])
|
||||
|
||||
@unpack
|
||||
@data((False, False), (True, False), (True, True))
|
||||
def test_assertion_structure(self, check_course, wildcard):
|
||||
self.check_badge_class_assertion(check_course, wildcard, self.create_badge_class(check_course))
|
||||
|
||||
@unpack
|
||||
@data((False, False), (True, False), (True, True))
|
||||
def test_empty_issuing_component(self, check_course, wildcard):
|
||||
self.check_badge_class_assertion(
|
||||
check_course, wildcard, self.create_badge_class(check_course, issuing_component='')
|
||||
)
|
||||
12
lms/djangoapps/badges/api/urls.py
Normal file
12
lms/djangoapps/badges/api/urls.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""
|
||||
URLs for badges API
|
||||
"""
|
||||
from django.conf import settings
|
||||
from django.conf.urls import patterns, url
|
||||
|
||||
from .views import UserBadgeAssertions
|
||||
|
||||
urlpatterns = patterns(
|
||||
'badges.api',
|
||||
url('^assertions/user/' + settings.USERNAME_PATTERN + '/$', UserBadgeAssertions.as_view(), name='user_assertions'),
|
||||
)
|
||||
138
lms/djangoapps/badges/api/views.py
Normal file
138
lms/djangoapps/badges/api/views.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""
|
||||
API views for badges
|
||||
"""
|
||||
from opaque_keys import InvalidKeyError
|
||||
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 xmodule_django.models import CourseKeyField
|
||||
|
||||
from badges.models import BadgeAssertion
|
||||
from .serializers import BadgeAssertionSerializer
|
||||
|
||||
|
||||
class InvalidCourseKeyError(APIException):
|
||||
"""
|
||||
Raised the course key given isn't valid.
|
||||
"""
|
||||
status_code = 400
|
||||
default_detail = "The course key provided was invalid."
|
||||
|
||||
|
||||
class UserBadgeAssertions(generics.ListAPIView):
|
||||
"""
|
||||
** Use cases **
|
||||
|
||||
Request a list of assertions for a user, optionally constrained to a course.
|
||||
|
||||
** Example Requests **
|
||||
|
||||
GET /api/badges/v1/assertions/user/{username}/
|
||||
|
||||
** Response Values **
|
||||
|
||||
Body comprised of a list of objects with the following fields:
|
||||
|
||||
* badge_class: The badge class the assertion was awarded for. Represented as an object
|
||||
with the following fields:
|
||||
* slug: The identifier for the badge class
|
||||
* issuing_component: The software component responsible for issuing this badge.
|
||||
* display_name: The display name of the badge.
|
||||
* course_id: The course key of the course this badge is scoped to, or null if it isn't scoped to a course.
|
||||
* description: A description of the award and its significance.
|
||||
* criteria: A description of what is needed to obtain this award.
|
||||
* image_url: A URL to the icon image used to represent this award.
|
||||
* image_url: The baked assertion image derived from the badge_class icon-- contains metadata about the award
|
||||
in its headers.
|
||||
* assertion_url: The URL to the OpenBadges BadgeAssertion object, for verification by compatible tools
|
||||
and software.
|
||||
|
||||
** Params **
|
||||
|
||||
* slug (optional): The identifier for a particular badge class to filter by.
|
||||
* issuing_component (optional): The issuing component for a particular badge class to filter by
|
||||
(requires slug to have been specified, or this will be ignored.) If slug is provided and this is not,
|
||||
assumes the issuing_component should be empty.
|
||||
* course_id (optional): Returns assertions that were awarded as part of a particular course. If slug is
|
||||
provided, and this field is not specified, assumes that the target badge has an empty course_id field.
|
||||
'*' may be used to get all badges with the specified slug, issuing_component combination across all courses.
|
||||
|
||||
** Returns **
|
||||
|
||||
* 200 on success, with a list of Badge Assertion objects.
|
||||
* 403 if a user who does not have permission to masquerade as
|
||||
another user specifies a username other than their own.
|
||||
* 404 if the specified user does not exist
|
||||
|
||||
{
|
||||
"count": 7,
|
||||
"previous": null,
|
||||
"num_pages": 1,
|
||||
"results": [
|
||||
{
|
||||
"badge_class": {
|
||||
"slug": "special_award",
|
||||
"issuing_component": "openedx__course",
|
||||
"display_name": "Very Special Award",
|
||||
"course_id": "course-v1:edX+DemoX+Demo_Course",
|
||||
"description": "Awarded for people who did something incredibly special",
|
||||
"criteria": "Do something incredibly special.",
|
||||
"image": "http://example.com/media/badge_classes/badges/special_xdpqpBv_9FYOZwN.png"
|
||||
},
|
||||
"image_url": "http://badges.example.com/media/issued/cd75b69fc1c979fcc1697c8403da2bdf.png",
|
||||
"assertion_url": "http://badges.example.com/public/assertions/07020647-e772-44dd-98b7-d13d34335ca6"
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Get all badges for the username specified.
|
||||
"""
|
||||
queryset = BadgeAssertion.objects.filter(user__username=self.kwargs['username'])
|
||||
provided_course_id = self.request.query_params.get('course_id')
|
||||
if provided_course_id == '*':
|
||||
# We might want to get all the matching course scoped badges to see how many courses
|
||||
# a user managed to get a specific award on.
|
||||
course_id = None
|
||||
elif provided_course_id:
|
||||
try:
|
||||
course_id = CourseKey.from_string(provided_course_id)
|
||||
except InvalidKeyError:
|
||||
raise InvalidCourseKeyError
|
||||
elif 'slug' not in self.request.query_params:
|
||||
# Need to get all badges for the user.
|
||||
course_id = None
|
||||
else:
|
||||
# Django won't let us use 'None' for querying a ForeignKey field. We have to use this special
|
||||
# 'Empty' value to indicate we're looking only for badges without a course key set.
|
||||
course_id = CourseKeyField.Empty
|
||||
|
||||
if course_id is not None:
|
||||
queryset = queryset.filter(badge_class__course_id=course_id)
|
||||
if self.request.query_params.get('slug'):
|
||||
queryset = queryset.filter(
|
||||
badge_class__slug=self.request.query_params['slug'],
|
||||
badge_class__issuing_component=self.request.query_params.get('issuing_component', '')
|
||||
)
|
||||
return queryset
|
||||
0
lms/djangoapps/badges/backends/__init__.py
Normal file
0
lms/djangoapps/badges/backends/__init__.py
Normal file
182
lms/djangoapps/badges/backends/badgr.py
Normal file
182
lms/djangoapps/badges/backends/badgr.py
Normal file
@@ -0,0 +1,182 @@
|
||||
"""
|
||||
Badge Awarding backend for Badgr-Server.
|
||||
"""
|
||||
import hashlib
|
||||
import logging
|
||||
import mimetypes
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from lazy import lazy
|
||||
from requests.packages.urllib3.exceptions import HTTPError
|
||||
|
||||
from badges.backends.base import BadgeBackend
|
||||
from eventtracking import tracker
|
||||
|
||||
from badges.models import BadgeAssertion
|
||||
|
||||
MAX_SLUG_LENGTH = 255
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BadgrBackend(BadgeBackend):
|
||||
"""
|
||||
Backend for Badgr-Server by Concentric Sky. http://info.badgr.io/
|
||||
"""
|
||||
badges = []
|
||||
|
||||
def __init__(self):
|
||||
super(BadgrBackend, self).__init__()
|
||||
if not settings.BADGR_API_TOKEN:
|
||||
raise ImproperlyConfigured("BADGR_API_TOKEN not set.")
|
||||
|
||||
@lazy
|
||||
def _base_url(self):
|
||||
"""
|
||||
Base URL for all API requests.
|
||||
"""
|
||||
return "{}/v1/issuer/issuers/{}".format(settings.BADGR_BASE_URL, settings.BADGR_ISSUER_SLUG)
|
||||
|
||||
@lazy
|
||||
def _badge_create_url(self):
|
||||
"""
|
||||
URL for generating a new Badge specification
|
||||
"""
|
||||
return "{}/badges".format(self._base_url)
|
||||
|
||||
def _badge_url(self, slug):
|
||||
"""
|
||||
Get the URL for a course's badge in a given mode.
|
||||
"""
|
||||
return "{}/{}".format(self._badge_create_url, slug)
|
||||
|
||||
def _assertion_url(self, slug):
|
||||
"""
|
||||
URL for generating a new assertion.
|
||||
"""
|
||||
return "{}/assertions".format(self._badge_url(slug))
|
||||
|
||||
def _slugify(self, badge_class):
|
||||
"""
|
||||
Get a compatible badge slug from the specification.
|
||||
"""
|
||||
slug = badge_class.issuing_component + badge_class.slug
|
||||
if badge_class.issuing_component and badge_class.course_id:
|
||||
# Make this unique to the course, and down to 64 characters.
|
||||
# We don't do this to badges without issuing_component set for backwards compatibility.
|
||||
slug = hashlib.sha256(slug + unicode(badge_class.course_id)).hexdigest()
|
||||
if len(slug) > MAX_SLUG_LENGTH:
|
||||
# Will be 64 characters.
|
||||
slug = hashlib.sha256(slug).hexdigest()
|
||||
return slug
|
||||
|
||||
def _log_if_raised(self, response, data):
|
||||
"""
|
||||
Log server response if there was an error.
|
||||
"""
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except HTTPError:
|
||||
LOGGER.error(
|
||||
u"Encountered an error when contacting the Badgr-Server. Request sent to %r with headers %r.\n"
|
||||
u"and data values %r\n"
|
||||
u"Response status was %s.\n%s",
|
||||
response.request.url, response.request.headers,
|
||||
data,
|
||||
response.status_code, response.content
|
||||
)
|
||||
raise
|
||||
|
||||
def _create_badge(self, badge_class):
|
||||
"""
|
||||
Create the badge class on Badgr.
|
||||
"""
|
||||
image = badge_class.image
|
||||
# We don't want to bother validating the file any further than making sure we can detect its MIME type,
|
||||
# for HTTP. The Badgr-Server should tell us if there's anything in particular wrong with it.
|
||||
content_type, __ = mimetypes.guess_type(image.name)
|
||||
if not content_type:
|
||||
raise ValueError(
|
||||
u"Could not determine content-type of image! Make sure it is a properly named .png file. "
|
||||
u"Filename was: {}".format(image.name)
|
||||
)
|
||||
files = {'image': (image.name, image, content_type)}
|
||||
data = {
|
||||
'name': badge_class.display_name,
|
||||
'criteria': badge_class.criteria,
|
||||
'slug': self._slugify(badge_class),
|
||||
'description': badge_class.description,
|
||||
}
|
||||
result = requests.post(
|
||||
self._badge_create_url, headers=self._get_headers(), data=data, files=files,
|
||||
timeout=settings.BADGR_TIMEOUT
|
||||
)
|
||||
self._log_if_raised(result, data)
|
||||
|
||||
def _send_assertion_created_event(self, user, assertion):
|
||||
"""
|
||||
Send an analytics event to record the creation of a badge assertion.
|
||||
"""
|
||||
tracker.emit(
|
||||
'edx.badge.assertion.created', {
|
||||
'user_id': user.id,
|
||||
'badge_slug': assertion.badge_class.slug,
|
||||
'badge_name': assertion.badge_class.display_name,
|
||||
'issuing_component': assertion.badge_class.issuing_component,
|
||||
'course_id': unicode(assertion.badge_class.course_id),
|
||||
'enrollment_mode': assertion.badge_class.mode,
|
||||
'assertion_id': assertion.id,
|
||||
'assertion_image_url': assertion.image_url,
|
||||
'assertion_json_url': assertion.assertion_url,
|
||||
'issuer': assertion.data.get('issuer'),
|
||||
}
|
||||
)
|
||||
|
||||
def _create_assertion(self, badge_class, user, evidence_url):
|
||||
"""
|
||||
Register an assertion with the Badgr server for a particular user for a specific class.
|
||||
"""
|
||||
data = {
|
||||
'email': user.email,
|
||||
'evidence': evidence_url,
|
||||
}
|
||||
response = requests.post(
|
||||
self._assertion_url(self._slugify(badge_class)), headers=self._get_headers(), data=data,
|
||||
timeout=settings.BADGR_TIMEOUT
|
||||
)
|
||||
self._log_if_raised(response, data)
|
||||
assertion, __ = BadgeAssertion.objects.get_or_create(user=user, badge_class=badge_class)
|
||||
assertion.data = response.json()
|
||||
assertion.backend = 'BadgrBackend'
|
||||
assertion.image_url = assertion.data['image']
|
||||
assertion.assertion_url = assertion.data['json']['id']
|
||||
assertion.save()
|
||||
self._send_assertion_created_event(user, assertion)
|
||||
return assertion
|
||||
|
||||
@staticmethod
|
||||
def _get_headers():
|
||||
"""
|
||||
Headers to send along with the request-- used for authentication.
|
||||
"""
|
||||
return {'Authorization': 'Token {}'.format(settings.BADGR_API_TOKEN)}
|
||||
|
||||
def _ensure_badge_created(self, badge_class):
|
||||
"""
|
||||
Verify a badge has been created for this badge class, and create it if not.
|
||||
"""
|
||||
slug = self._slugify(badge_class)
|
||||
if slug in BadgrBackend.badges:
|
||||
return
|
||||
response = requests.get(self._badge_url(slug), headers=self._get_headers(), timeout=settings.BADGR_TIMEOUT)
|
||||
if response.status_code != 200:
|
||||
self._create_badge(badge_class)
|
||||
BadgrBackend.badges.append(slug)
|
||||
|
||||
def award(self, badge_class, user, evidence_url=None):
|
||||
"""
|
||||
Make sure the badge class has been created on the backend, and then award the badge class to the user.
|
||||
"""
|
||||
self._ensure_badge_created(badge_class)
|
||||
return self._create_assertion(badge_class, user, evidence_url)
|
||||
17
lms/djangoapps/badges/backends/base.py
Normal file
17
lms/djangoapps/badges/backends/base.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""
|
||||
Base class for badge backends.
|
||||
"""
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
|
||||
class BadgeBackend(object):
|
||||
"""
|
||||
Defines the interface for badging backends.
|
||||
"""
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
@abstractmethod
|
||||
def award(self, badge_class, user, evidence_url=None):
|
||||
"""
|
||||
Create a badge assertion for the user using this backend.
|
||||
"""
|
||||
0
lms/djangoapps/badges/backends/tests/__init__.py
Normal file
0
lms/djangoapps/badges/backends/tests/__init__.py
Normal file
13
lms/djangoapps/badges/backends/tests/dummy_backend.py
Normal file
13
lms/djangoapps/badges/backends/tests/dummy_backend.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""
|
||||
Dummy backend, for use in testing.
|
||||
"""
|
||||
from lms.djangoapps.badges.backends.base import BadgeBackend
|
||||
from lms.djangoapps.badges.tests.factories import BadgeAssertionFactory
|
||||
|
||||
|
||||
class DummyBackend(BadgeBackend):
|
||||
"""
|
||||
Dummy backend that creates assertions without contacting any real-world backend.
|
||||
"""
|
||||
def award(self, badge_class, user, evidence_url=None):
|
||||
return BadgeAssertionFactory(badge_class=badge_class, user=user)
|
||||
@@ -1,19 +1,22 @@
|
||||
"""
|
||||
Tests for the BadgeHandler, which communicates with the Badgr Server.
|
||||
Tests for BadgrBackend
|
||||
"""
|
||||
from datetime import datetime
|
||||
from django.test.utils import override_settings
|
||||
|
||||
import ddt
|
||||
from django.db.models.fields.files import ImageFieldFile
|
||||
from django.test.utils import override_settings
|
||||
from lazy.lazy import lazy
|
||||
from mock import patch, Mock, call
|
||||
from certificates.models import BadgeAssertion, BadgeImageConfiguration
|
||||
|
||||
from badges.backends.badgr import BadgrBackend
|
||||
from badges.models import BadgeAssertion
|
||||
from badges.tests.factories import BadgeClassFactory
|
||||
from openedx.core.lib.tests.assertions.events import assert_event_matches
|
||||
from track.tests import EventTrackingTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from certificates.badge_handler import BadgeHandler
|
||||
from certificates.tests.factories import BadgeImageConfigurationFactory
|
||||
from student.tests.factories import UserFactory, CourseEnrollmentFactory
|
||||
from track.tests import EventTrackingTestCase
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
BADGR_SETTINGS = {
|
||||
'BADGR_API_TOKEN': '12345',
|
||||
@@ -21,9 +24,14 @@ BADGR_SETTINGS = {
|
||||
'BADGR_ISSUER_SLUG': 'test-issuer',
|
||||
}
|
||||
|
||||
# Should be the hashed result of test_slug as the slug, and test_component as the component
|
||||
EXAMPLE_SLUG = '15bb687e0c59ef2f0a49f6838f511bf4ca6c566dd45da6293cabbd9369390e1a'
|
||||
|
||||
|
||||
# pylint: disable=protected-access
|
||||
@ddt.ddt
|
||||
@override_settings(**BADGR_SETTINGS)
|
||||
class BadgeHandlerTestCase(ModuleStoreTestCase, EventTrackingTestCase):
|
||||
class BadgrBackendTestCase(ModuleStoreTestCase, EventTrackingTestCase):
|
||||
"""
|
||||
Tests the BadgeHandler object
|
||||
"""
|
||||
@@ -31,7 +39,7 @@ class BadgeHandlerTestCase(ModuleStoreTestCase, EventTrackingTestCase):
|
||||
"""
|
||||
Create a course and user to test with.
|
||||
"""
|
||||
super(BadgeHandlerTestCase, self).setUp()
|
||||
super(BadgrBackendTestCase, self).setUp()
|
||||
# Need key to be deterministic to test slugs.
|
||||
self.course = CourseFactory.create(
|
||||
org='edX', course='course_test', run='test_run', display_name='Badged',
|
||||
@@ -40,9 +48,13 @@ class BadgeHandlerTestCase(ModuleStoreTestCase, EventTrackingTestCase):
|
||||
)
|
||||
self.user = UserFactory.create(email='example@example.com')
|
||||
CourseEnrollmentFactory.create(user=self.user, course_id=self.course.location.course_key, mode='honor')
|
||||
# Need for force empty this dict on each run.
|
||||
BadgeHandler.badges = {}
|
||||
BadgeImageConfigurationFactory()
|
||||
# Need to empty this on each run.
|
||||
BadgrBackend.badges = []
|
||||
self.badge_class = BadgeClassFactory.create(course_id=self.course.location.course_key)
|
||||
self.legacy_badge_class = BadgeClassFactory.create(
|
||||
course_id=self.course.location.course_key, issuing_component=''
|
||||
)
|
||||
self.no_course_badge_class = BadgeClassFactory.create()
|
||||
|
||||
@lazy
|
||||
def handler(self):
|
||||
@@ -50,21 +62,21 @@ class BadgeHandlerTestCase(ModuleStoreTestCase, EventTrackingTestCase):
|
||||
Lazily loads a BadgeHandler object for the current course. Can't do this on setUp because the settings
|
||||
overrides aren't in place.
|
||||
"""
|
||||
return BadgeHandler(self.course.location.course_key)
|
||||
return BadgrBackend()
|
||||
|
||||
def test_urls(self):
|
||||
"""
|
||||
Make sure the handler generates the correct URLs for different API tasks.
|
||||
"""
|
||||
self.assertEqual(self.handler.base_url, 'https://example.com/v1/issuer/issuers/test-issuer')
|
||||
self.assertEqual(self.handler.badge_create_url, 'https://example.com/v1/issuer/issuers/test-issuer/badges')
|
||||
self.assertEqual(self.handler._base_url, 'https://example.com/v1/issuer/issuers/test-issuer')
|
||||
self.assertEqual(self.handler._badge_create_url, 'https://example.com/v1/issuer/issuers/test-issuer/badges')
|
||||
self.assertEqual(
|
||||
self.handler.badge_url('honor'),
|
||||
'https://example.com/v1/issuer/issuers/test-issuer/badges/edxcourse_testtest_run_honor_fc5519b'
|
||||
self.handler._badge_url('test_slug_here'),
|
||||
'https://example.com/v1/issuer/issuers/test-issuer/badges/test_slug_here'
|
||||
)
|
||||
self.assertEqual(
|
||||
self.handler.assertion_url('honor'),
|
||||
'https://example.com/v1/issuer/issuers/test-issuer/badges/edxcourse_testtest_run_honor_fc5519b/assertions'
|
||||
self.handler._assertion_url('another_test_slug'),
|
||||
'https://example.com/v1/issuer/issuers/test-issuer/badges/another_test_slug/assertions'
|
||||
)
|
||||
|
||||
def check_headers(self, headers):
|
||||
@@ -73,121 +85,111 @@ class BadgeHandlerTestCase(ModuleStoreTestCase, EventTrackingTestCase):
|
||||
"""
|
||||
self.assertEqual(headers, {'Authorization': 'Token 12345'})
|
||||
|
||||
def test_slug(self):
|
||||
"""
|
||||
Verify slug generation is working as expected. If this test fails, the algorithm has changed, and it will cause
|
||||
the handler to lose track of all badges it made in the past.
|
||||
"""
|
||||
self.assertEqual(
|
||||
self.handler.course_slug('honor'),
|
||||
'edxcourse_testtest_run_honor_fc5519b'
|
||||
)
|
||||
self.assertEqual(
|
||||
self.handler.course_slug('verified'),
|
||||
'edxcourse_testtest_run_verified_a199ec0'
|
||||
)
|
||||
|
||||
def test_get_headers(self):
|
||||
"""
|
||||
Check to make sure the handler generates appropriate HTTP headers.
|
||||
"""
|
||||
self.check_headers(self.handler.get_headers())
|
||||
self.check_headers(self.handler._get_headers())
|
||||
|
||||
@patch('requests.post')
|
||||
def test_create_badge(self, post):
|
||||
"""
|
||||
Verify badge spec creation works.
|
||||
"""
|
||||
self.handler.create_badge('honor')
|
||||
self.handler._create_badge(self.badge_class)
|
||||
args, kwargs = post.call_args
|
||||
self.assertEqual(args[0], 'https://example.com/v1/issuer/issuers/test-issuer/badges')
|
||||
self.assertEqual(kwargs['files']['image'][0], BadgeImageConfiguration.objects.get(mode='honor').icon.name)
|
||||
self.assertEqual(kwargs['files']['image'][0], self.badge_class.image.name)
|
||||
self.assertIsInstance(kwargs['files']['image'][1], ImageFieldFile)
|
||||
self.assertEqual(kwargs['files']['image'][2], 'image/png')
|
||||
self.check_headers(kwargs['headers'])
|
||||
self.assertEqual(
|
||||
kwargs['data'],
|
||||
{
|
||||
'name': 'Badged',
|
||||
'slug': 'edxcourse_testtest_run_honor_fc5519b',
|
||||
'criteria': 'https://edx.org/courses/edX/course_test/test_run/about',
|
||||
'description': 'Completed the course "Badged" (honor, 2015-05-19 - 2015-05-20)',
|
||||
'name': 'Test Badge',
|
||||
'slug': EXAMPLE_SLUG,
|
||||
'criteria': 'https://example.com/syllabus',
|
||||
'description': "Yay! It's a test badge.",
|
||||
}
|
||||
)
|
||||
|
||||
def test_self_paced_description(self):
|
||||
"""
|
||||
Verify that a badge created for a course with no end date gets a different description.
|
||||
"""
|
||||
self.course.end = None
|
||||
self.assertEqual(BadgeHandler.badge_description(self.course, 'honor'), 'Completed the course "Badged" (honor)')
|
||||
|
||||
def test_ensure_badge_created_cache(self):
|
||||
"""
|
||||
Make sure ensure_badge_created doesn't call create_badge if we know the badge is already there.
|
||||
"""
|
||||
BadgeHandler.badges['edxcourse_testtest_run_honor_fc5519b'] = True
|
||||
self.handler.create_badge = Mock()
|
||||
self.handler.ensure_badge_created('honor')
|
||||
self.assertFalse(self.handler.create_badge.called)
|
||||
BadgrBackend.badges.append(EXAMPLE_SLUG)
|
||||
self.handler._create_badge = Mock()
|
||||
self.handler._ensure_badge_created(self.badge_class)
|
||||
self.assertFalse(self.handler._create_badge.called)
|
||||
|
||||
@ddt.unpack
|
||||
@ddt.data(
|
||||
('badge_class', EXAMPLE_SLUG),
|
||||
('legacy_badge_class', 'test_slug'),
|
||||
('no_course_badge_class', 'test_componenttest_slug')
|
||||
)
|
||||
def test_slugs(self, badge_class_type, slug):
|
||||
self.assertEqual(self.handler._slugify(getattr(self, badge_class_type)), slug)
|
||||
|
||||
@patch('requests.get')
|
||||
def test_ensure_badge_created_checks(self, get):
|
||||
response = Mock()
|
||||
response.status_code = 200
|
||||
get.return_value = response
|
||||
self.assertNotIn('edxcourse_testtest_run_honor_fc5519b', BadgeHandler.badges)
|
||||
self.handler.create_badge = Mock()
|
||||
self.handler.ensure_badge_created('honor')
|
||||
self.assertNotIn('test_componenttest_slug', BadgrBackend.badges)
|
||||
self.handler._create_badge = Mock()
|
||||
self.handler._ensure_badge_created(self.badge_class)
|
||||
self.assertTrue(get.called)
|
||||
args, kwargs = get.call_args
|
||||
self.assertEqual(
|
||||
args[0],
|
||||
'https://example.com/v1/issuer/issuers/test-issuer/badges/'
|
||||
'edxcourse_testtest_run_honor_fc5519b'
|
||||
'https://example.com/v1/issuer/issuers/test-issuer/badges/' +
|
||||
EXAMPLE_SLUG
|
||||
)
|
||||
self.check_headers(kwargs['headers'])
|
||||
self.assertTrue(BadgeHandler.badges['edxcourse_testtest_run_honor_fc5519b'])
|
||||
self.assertFalse(self.handler.create_badge.called)
|
||||
self.assertIn(EXAMPLE_SLUG, BadgrBackend.badges)
|
||||
self.assertFalse(self.handler._create_badge.called)
|
||||
|
||||
@patch('requests.get')
|
||||
def test_ensure_badge_created_creates(self, get):
|
||||
response = Mock()
|
||||
response.status_code = 404
|
||||
get.return_value = response
|
||||
self.assertNotIn('edxcourse_testtest_run_honor_fc5519b', BadgeHandler.badges)
|
||||
self.handler.create_badge = Mock()
|
||||
self.handler.ensure_badge_created('honor')
|
||||
self.assertTrue(self.handler.create_badge.called)
|
||||
self.assertEqual(self.handler.create_badge.call_args, call('honor'))
|
||||
self.assertTrue(BadgeHandler.badges['edxcourse_testtest_run_honor_fc5519b'])
|
||||
self.assertNotIn(EXAMPLE_SLUG, BadgrBackend.badges)
|
||||
self.handler._create_badge = Mock()
|
||||
self.handler._ensure_badge_created(self.badge_class)
|
||||
self.assertTrue(self.handler._create_badge.called)
|
||||
self.assertEqual(self.handler._create_badge.call_args, call(self.badge_class))
|
||||
self.assertIn(EXAMPLE_SLUG, BadgrBackend.badges)
|
||||
|
||||
@patch('requests.post')
|
||||
def test_badge_creation_event(self, post):
|
||||
result = {
|
||||
'json': {'id': 'http://www.example.com/example'},
|
||||
'image': 'http://www.example.com/example.png',
|
||||
'slug': 'test_assertion_slug',
|
||||
'badge': 'test_assertion_slug',
|
||||
'issuer': 'https://example.com/v1/issuer/issuers/test-issuer',
|
||||
}
|
||||
response = Mock()
|
||||
response.json.return_value = result
|
||||
post.return_value = response
|
||||
self.recreate_tracker()
|
||||
self.handler.create_assertion(self.user, 'honor')
|
||||
self.handler._create_assertion(self.badge_class, self.user, 'https://example.com/irrefutable_proof')
|
||||
args, kwargs = post.call_args
|
||||
self.assertEqual(
|
||||
args[0],
|
||||
'https://example.com/v1/issuer/issuers/test-issuer/badges/'
|
||||
'edxcourse_testtest_run_honor_fc5519b/assertions'
|
||||
'https://example.com/v1/issuer/issuers/test-issuer/badges/' +
|
||||
EXAMPLE_SLUG +
|
||||
'/assertions'
|
||||
)
|
||||
self.check_headers(kwargs['headers'])
|
||||
assertion = BadgeAssertion.objects.get(user=self.user, course_id=self.course.location.course_key)
|
||||
assertion = BadgeAssertion.objects.get(user=self.user, badge_class__course_id=self.course.location.course_key)
|
||||
self.assertEqual(assertion.data, result)
|
||||
self.assertEqual(assertion.image_url, 'http://www.example.com/example.png')
|
||||
self.assertEqual(assertion.assertion_url, 'http://www.example.com/example')
|
||||
self.assertEqual(kwargs['data'], {
|
||||
'email': 'example@example.com',
|
||||
'evidence': 'https://edx.org/certificates/user/2/course/edX/course_test/test_run?evidence_visit=1'
|
||||
'evidence': 'https://example.com/irrefutable_proof'
|
||||
})
|
||||
assert_event_matches({
|
||||
'name': 'edx.badge.assertion.created',
|
||||
@@ -196,6 +198,9 @@ class BadgeHandlerTestCase(ModuleStoreTestCase, EventTrackingTestCase):
|
||||
'course_id': unicode(self.course.location.course_key),
|
||||
'enrollment_mode': 'honor',
|
||||
'assertion_id': assertion.id,
|
||||
'badge_name': 'Test Badge',
|
||||
'badge_slug': 'test_slug',
|
||||
'issuing_component': 'test_component',
|
||||
'assertion_image_url': 'http://www.example.com/example.png',
|
||||
'assertion_json_url': 'http://www.example.com/example',
|
||||
'issuer': 'https://example.com/v1/issuer/issuers/test-issuer',
|
||||
0
lms/djangoapps/badges/events/__init__.py
Normal file
0
lms/djangoapps/badges/events/__init__.py
Normal file
117
lms/djangoapps/badges/events/course_complete.py
Normal file
117
lms/djangoapps/badges/events/course_complete.py
Normal file
@@ -0,0 +1,117 @@
|
||||
"""
|
||||
Helper functions for the course complete event that was originally included with the Badging MVP.
|
||||
"""
|
||||
import hashlib
|
||||
import logging
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.template.defaultfilters import slugify
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from badges.models import CourseCompleteImageConfiguration, BadgeClass, BadgeAssertion
|
||||
from badges.utils import site_prefix, requires_badges_enabled
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# NOTE: As these functions are carry-overs from the initial badging implementation, they are used in
|
||||
# migrations. Please check the badge migrations when changing any of these functions.
|
||||
|
||||
|
||||
def course_slug(course_key, mode):
|
||||
"""
|
||||
Legacy: Not to be used as a model for constructing badge slugs. Included for compatibility with the original badge
|
||||
type, awarded on course completion.
|
||||
|
||||
Slug ought to be deterministic and limited in size so it's not too big for Badgr.
|
||||
|
||||
Badgr's max slug length is 255.
|
||||
"""
|
||||
# Seven digits should be enough to realistically avoid collisions. That's what git services use.
|
||||
digest = hashlib.sha256(u"{}{}".format(unicode(course_key), unicode(mode))).hexdigest()[:7]
|
||||
base_slug = slugify(unicode(course_key) + u'_{}_'.format(mode))[:248]
|
||||
return base_slug + digest
|
||||
|
||||
|
||||
def badge_description(course, mode):
|
||||
"""
|
||||
Returns a description for the earned badge.
|
||||
"""
|
||||
if course.end:
|
||||
return _(u'Completed the course "{course_name}" ({course_mode}, {start_date} - {end_date})').format(
|
||||
start_date=course.start.date(),
|
||||
end_date=course.end.date(),
|
||||
course_name=course.display_name,
|
||||
course_mode=mode,
|
||||
)
|
||||
else:
|
||||
return _(u'Completed the course "{course_name}" ({course_mode})').format(
|
||||
course_name=course.display_name,
|
||||
course_mode=mode,
|
||||
)
|
||||
|
||||
|
||||
def evidence_url(user_id, course_key):
|
||||
"""
|
||||
Generates a URL to the user's Certificate HTML view, along with a GET variable that will signal the evidence visit
|
||||
event.
|
||||
"""
|
||||
return site_prefix() + reverse(
|
||||
'certificates:html_view', kwargs={'user_id': user_id, 'course_id': unicode(course_key)}) + '?evidence_visit=1'
|
||||
|
||||
|
||||
def criteria(course_key):
|
||||
"""
|
||||
Constructs the 'criteria' URL from the course about page.
|
||||
"""
|
||||
about_path = reverse('about_course', kwargs={'course_id': unicode(course_key)})
|
||||
return u'{}{}'.format(site_prefix(), about_path)
|
||||
|
||||
|
||||
def get_completion_badge(course_id, user):
|
||||
"""
|
||||
Given a course key and a user, find the user's enrollment mode
|
||||
and get the Course Completion badge.
|
||||
"""
|
||||
from student.models import CourseEnrollment
|
||||
badge_classes = CourseEnrollment.objects.filter(
|
||||
user=user, course_id=course_id
|
||||
).order_by('-is_active')
|
||||
if not badge_classes:
|
||||
return None
|
||||
mode = badge_classes[0].mode
|
||||
course = modulestore().get_course(course_id)
|
||||
if not course.issue_badges:
|
||||
return None
|
||||
return BadgeClass.get_badge_class(
|
||||
slug=course_slug(course_id, mode),
|
||||
issuing_component='',
|
||||
criteria=criteria(course_id),
|
||||
description=badge_description(course, mode),
|
||||
course_id=course_id,
|
||||
mode=mode,
|
||||
display_name=course.display_name,
|
||||
image_file_handle=CourseCompleteImageConfiguration.image_for_mode(mode)
|
||||
)
|
||||
|
||||
|
||||
@requires_badges_enabled
|
||||
def course_badge_check(user, course_key):
|
||||
"""
|
||||
Takes a GeneratedCertificate instance, and checks to see if a badge exists for this course, creating
|
||||
it if not, should conditions be right.
|
||||
"""
|
||||
if not modulestore().get_course(course_key).issue_badges:
|
||||
LOGGER.info("Course is not configured to issue badges.")
|
||||
return
|
||||
badge_class = get_completion_badge(course_key, user)
|
||||
if not badge_class:
|
||||
# We're not configured to make a badge for this course mode.
|
||||
return
|
||||
if BadgeAssertion.objects.filter(user=user, badge_class=badge_class):
|
||||
LOGGER.info("Completion badge already exists for this user on this course.")
|
||||
# Badge already exists. Skip.
|
||||
return
|
||||
evidence = evidence_url(user.id, course_key)
|
||||
badge_class.award(user, evidence_url=evidence)
|
||||
81
lms/djangoapps/badges/events/course_meta.py
Normal file
81
lms/djangoapps/badges/events/course_meta.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
Events which have to do with a user doing something with more than one course, such
|
||||
as enrolling in a certain number, completing a certain number, or completing a specific set of courses.
|
||||
"""
|
||||
|
||||
from badges.models import CourseEventBadgesConfiguration, BadgeClass
|
||||
from badges.utils import requires_badges_enabled
|
||||
|
||||
|
||||
def award_badge(config, count, user):
|
||||
"""
|
||||
Given one of the configurations for enrollments or completions, award
|
||||
the appropriate badge if one is configured.
|
||||
|
||||
config is a dictionary with integer keys and course keys as values.
|
||||
count is the key to retrieve from this dictionary.
|
||||
user is the user to award the badge to.
|
||||
|
||||
Example config:
|
||||
{3: 'slug_for_badge_for_three_enrollments', 5: 'slug_for_badge_with_five_enrollments'}
|
||||
"""
|
||||
slug = config.get(count)
|
||||
if not slug:
|
||||
return
|
||||
badge_class = BadgeClass.get_badge_class(
|
||||
slug=slug, issuing_component='openedx__course', create=False,
|
||||
)
|
||||
if not badge_class:
|
||||
return
|
||||
if not badge_class.get_for_user(user):
|
||||
badge_class.award(user)
|
||||
|
||||
|
||||
def award_enrollment_badge(user):
|
||||
"""
|
||||
Awards badges based on the number of courses a user is enrolled in.
|
||||
"""
|
||||
config = CourseEventBadgesConfiguration.current().enrolled_settings
|
||||
enrollments = user.courseenrollment_set.filter(is_active=True).count()
|
||||
award_badge(config, enrollments, user)
|
||||
|
||||
|
||||
@requires_badges_enabled
|
||||
def completion_check(user):
|
||||
"""
|
||||
Awards badges based upon the number of courses a user has 'completed'.
|
||||
Courses are never truly complete, but they can be closed.
|
||||
|
||||
For this reason we use checks on certificates to find out if a user has
|
||||
completed courses. This badge will not work if certificate generation isn't
|
||||
enabled and run.
|
||||
"""
|
||||
from certificates.models import CertificateStatuses
|
||||
config = CourseEventBadgesConfiguration.current().completed_settings
|
||||
certificates = user.generatedcertificate_set.filter(status__in=CertificateStatuses.PASSED_STATUSES).count()
|
||||
award_badge(config, certificates, user)
|
||||
|
||||
|
||||
@requires_badges_enabled
|
||||
def course_group_check(user, course_key):
|
||||
"""
|
||||
Awards a badge if a user has completed every course in a defined set.
|
||||
"""
|
||||
from certificates.models import CertificateStatuses
|
||||
config = CourseEventBadgesConfiguration.current().course_group_settings
|
||||
awards = []
|
||||
for slug, keys in config.items():
|
||||
if course_key in keys:
|
||||
certs = user.generatedcertificate_set.filter(
|
||||
status__in=CertificateStatuses.PASSED_STATUSES,
|
||||
course_id__in=keys,
|
||||
)
|
||||
if len(certs) == len(keys):
|
||||
awards.append(slug)
|
||||
|
||||
for slug in awards:
|
||||
badge_class = BadgeClass.get_badge_class(
|
||||
slug=slug, issuing_component='openedx__course', create=False,
|
||||
)
|
||||
if badge_class and not badge_class.get_for_user(user):
|
||||
badge_class.award(user)
|
||||
0
lms/djangoapps/badges/events/tests/__init__.py
Normal file
0
lms/djangoapps/badges/events/tests/__init__.py
Normal file
70
lms/djangoapps/badges/events/tests/test_course_complete.py
Normal file
70
lms/djangoapps/badges/events/tests/test_course_complete.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
Tests for the course completion helper functions.
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
|
||||
from badges.events import course_complete
|
||||
|
||||
|
||||
class CourseCompleteTestCase(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for the course completion helper functions.
|
||||
"""
|
||||
def setUp(self, **kwargs):
|
||||
super(CourseCompleteTestCase, self).setUp()
|
||||
# Need key to be deterministic to test slugs.
|
||||
self.course = CourseFactory.create(
|
||||
org='edX', course='course_test', run='test_run', display_name='Badged',
|
||||
start=datetime(year=2015, month=5, day=19),
|
||||
end=datetime(year=2015, month=5, day=20)
|
||||
)
|
||||
self.course_key = self.course.location.course_key
|
||||
|
||||
def test_slug(self):
|
||||
"""
|
||||
Verify slug generation is working as expected. If this test fails, the algorithm has changed, and it will cause
|
||||
the handler to lose track of all badges it made in the past.
|
||||
"""
|
||||
self.assertEqual(
|
||||
course_complete.course_slug(self.course_key, 'honor'),
|
||||
'edxcourse_testtest_run_honor_fc5519b'
|
||||
)
|
||||
self.assertEqual(
|
||||
course_complete.course_slug(self.course_key, 'verified'),
|
||||
'edxcourse_testtest_run_verified_a199ec0'
|
||||
)
|
||||
|
||||
def test_dated_description(self):
|
||||
"""
|
||||
Verify that a course with start/end dates contains a description with them.
|
||||
"""
|
||||
self.assertEqual(
|
||||
course_complete.badge_description(self.course, 'honor'),
|
||||
'Completed the course "Badged" (honor, 2015-05-19 - 2015-05-20)'
|
||||
)
|
||||
|
||||
def test_self_paced_description(self):
|
||||
"""
|
||||
Verify that a badge created for a course with no end date gets a different description.
|
||||
"""
|
||||
self.course.end = None
|
||||
self.assertEqual(
|
||||
course_complete.badge_description(self.course, 'honor'),
|
||||
'Completed the course "Badged" (honor)'
|
||||
)
|
||||
|
||||
def test_evidence_url(self):
|
||||
"""
|
||||
Make sure the evidence URL points to the right place.
|
||||
"""
|
||||
user = UserFactory.create()
|
||||
self.assertEqual(
|
||||
'https://edx.org/certificates/user/{user_id}/course/{course_key}?evidence_visit=1'.format(
|
||||
user_id=user.id, course_key=self.course_key
|
||||
),
|
||||
course_complete.evidence_url(user.id, self.course_key)
|
||||
)
|
||||
189
lms/djangoapps/badges/events/tests/test_course_meta.py
Normal file
189
lms/djangoapps/badges/events/tests/test_course_meta.py
Normal file
@@ -0,0 +1,189 @@
|
||||
"""
|
||||
Tests the course meta badging events
|
||||
"""
|
||||
from ddt import ddt, unpack, data
|
||||
from django.test.utils import override_settings
|
||||
from mock import patch
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from badges.tests.factories import RandomBadgeClassFactory, CourseEventBadgesConfigurationFactory
|
||||
from certificates.models import GeneratedCertificate, CertificateStatuses
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
|
||||
@ddt
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_OPENBADGES': True})
|
||||
@override_settings(BADGING_BACKEND='lms.djangoapps.badges.backends.tests.dummy_backend.DummyBackend')
|
||||
class CourseEnrollmentBadgeTest(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests the event which awards badges based on number of courses a user is enrolled in.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(CourseEnrollmentBadgeTest, self).setUp()
|
||||
self.badge_classes = [
|
||||
RandomBadgeClassFactory(
|
||||
issuing_component='openedx__course'
|
||||
),
|
||||
RandomBadgeClassFactory(
|
||||
issuing_component='openedx__course'
|
||||
),
|
||||
RandomBadgeClassFactory(
|
||||
issuing_component='openedx__course'
|
||||
),
|
||||
]
|
||||
nums = ['3', '5', '8']
|
||||
entries = [','.join(pair) for pair in zip(nums, [badge.slug for badge in self.badge_classes])]
|
||||
enrollment_config = '\r'.join(entries)
|
||||
self.config = CourseEventBadgesConfigurationFactory(courses_enrolled=enrollment_config)
|
||||
|
||||
def test_no_match(self):
|
||||
"""
|
||||
Make sure a badge isn't created before a user's reached any checkpoint.
|
||||
"""
|
||||
user = UserFactory()
|
||||
course = CourseFactory()
|
||||
# pylint: disable=no-member
|
||||
CourseEnrollment.enroll(user, course_key=course.location.course_key)
|
||||
self.assertFalse(user.badgeassertion_set.all())
|
||||
|
||||
@unpack
|
||||
@data((1, 3), (2, 5), (3, 8))
|
||||
def test_checkpoint_matches(self, checkpoint, required_badges):
|
||||
"""
|
||||
Make sure the proper badges are awarded at the right checkpoints.
|
||||
"""
|
||||
user = UserFactory()
|
||||
courses = [CourseFactory() for _i in range(required_badges)]
|
||||
for course in courses:
|
||||
CourseEnrollment.enroll(user, course_key=course.location.course_key)
|
||||
# pylint: disable=no-member
|
||||
assertions = user.badgeassertion_set.all().order_by('id')
|
||||
self.assertEqual(user.badgeassertion_set.all().count(), checkpoint)
|
||||
self.assertEqual(assertions[checkpoint - 1].badge_class, self.badge_classes[checkpoint - 1])
|
||||
|
||||
|
||||
@ddt
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_OPENBADGES': True})
|
||||
@override_settings(BADGING_BACKEND='lms.djangoapps.badges.backends.tests.dummy_backend.DummyBackend')
|
||||
class CourseCompletionBadgeTest(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests the event which awards badges based on the number of courses completed.
|
||||
"""
|
||||
def setUp(self, **kwargs):
|
||||
super(CourseCompletionBadgeTest, self).setUp()
|
||||
self.badge_classes = [
|
||||
RandomBadgeClassFactory(
|
||||
issuing_component='openedx__course'
|
||||
),
|
||||
RandomBadgeClassFactory(
|
||||
issuing_component='openedx__course'
|
||||
),
|
||||
RandomBadgeClassFactory(
|
||||
issuing_component='openedx__course'
|
||||
),
|
||||
]
|
||||
nums = ['2', '6', '9']
|
||||
entries = [','.join(pair) for pair in zip(nums, [badge.slug for badge in self.badge_classes])]
|
||||
completed_config = '\r'.join(entries)
|
||||
self.config = CourseEventBadgesConfigurationFactory.create(courses_completed=completed_config)
|
||||
self.config.clean_fields()
|
||||
|
||||
def test_no_match(self):
|
||||
"""
|
||||
Make sure a badge isn't created before a user's reached any checkpoint.
|
||||
"""
|
||||
user = UserFactory()
|
||||
course = CourseFactory()
|
||||
GeneratedCertificate(
|
||||
# pylint: disable=no-member
|
||||
user=user, course_id=course.location.course_key, status=CertificateStatuses.downloadable
|
||||
).save()
|
||||
# pylint: disable=no-member
|
||||
self.assertFalse(user.badgeassertion_set.all())
|
||||
|
||||
@unpack
|
||||
@data((1, 2), (2, 6), (3, 9))
|
||||
def test_checkpoint_matches(self, checkpoint, required_badges):
|
||||
"""
|
||||
Make sure the proper badges are awarded at the right checkpoints.
|
||||
"""
|
||||
user = UserFactory()
|
||||
courses = [CourseFactory() for _i in range(required_badges)]
|
||||
for course in courses:
|
||||
GeneratedCertificate(
|
||||
# pylint: disable=no-member
|
||||
user=user, course_id=course.location.course_key, status=CertificateStatuses.downloadable
|
||||
).save()
|
||||
# pylint: disable=no-member
|
||||
assertions = user.badgeassertion_set.all().order_by('id')
|
||||
# pylint: disable=no-member
|
||||
self.assertEqual(user.badgeassertion_set.all().count(), checkpoint)
|
||||
self.assertEqual(assertions[checkpoint - 1].badge_class, self.badge_classes[checkpoint - 1])
|
||||
|
||||
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_OPENBADGES': True})
|
||||
@override_settings(BADGING_BACKEND='lms.djangoapps.badges.backends.tests.dummy_backend.DummyBackend')
|
||||
class CourseGroupBadgeTest(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests the event which awards badges when a user completes a set of courses.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(CourseGroupBadgeTest, self).setUp()
|
||||
self.badge_classes = [
|
||||
RandomBadgeClassFactory(
|
||||
issuing_component='openedx__course'
|
||||
),
|
||||
RandomBadgeClassFactory(
|
||||
issuing_component='openedx__course'
|
||||
),
|
||||
RandomBadgeClassFactory(
|
||||
issuing_component='openedx__course'
|
||||
),
|
||||
]
|
||||
self.courses = []
|
||||
for _badge_class in self.badge_classes:
|
||||
# pylint: disable=no-member
|
||||
self.courses.append([CourseFactory().location.course_key for _i in range(3)])
|
||||
lines = [badge_class.slug + ',' + ','.join([unicode(course_key) for course_key in keys])
|
||||
for badge_class, keys in zip(self.badge_classes, self.courses)]
|
||||
config = '\r'.join(lines)
|
||||
self.config = CourseEventBadgesConfigurationFactory(course_groups=config)
|
||||
self.config_map = dict(zip(self.badge_classes, self.courses))
|
||||
|
||||
def test_no_match(self):
|
||||
"""
|
||||
Make sure a badge isn't created before a user's completed any course groups.
|
||||
"""
|
||||
user = UserFactory()
|
||||
course = CourseFactory()
|
||||
GeneratedCertificate(
|
||||
# pylint: disable=no-member
|
||||
user=user, course_id=course.location.course_key, status=CertificateStatuses.downloadable
|
||||
).save()
|
||||
# pylint: disable=no-member
|
||||
self.assertFalse(user.badgeassertion_set.all())
|
||||
|
||||
def test_group_matches(self):
|
||||
"""
|
||||
Make sure the proper badges are awarded when groups are completed.
|
||||
"""
|
||||
user = UserFactory()
|
||||
items = list(self.config_map.items())
|
||||
for badge_class, course_keys in items:
|
||||
for i, key in enumerate(course_keys):
|
||||
GeneratedCertificate(
|
||||
user=user, course_id=key, status=CertificateStatuses.downloadable
|
||||
).save()
|
||||
# We don't award badges until all three are set.
|
||||
if i + 1 == len(course_keys):
|
||||
self.assertTrue(badge_class.get_for_user(user))
|
||||
else:
|
||||
self.assertFalse(badge_class.get_for_user(user))
|
||||
# pylint: disable=no-member
|
||||
classes = [badge.badge_class.id for badge in user.badgeassertion_set.all()]
|
||||
source_classes = [badge.id for badge in self.badge_classes]
|
||||
self.assertEqual(classes, source_classes)
|
||||
69
lms/djangoapps/badges/migrations/0001_initial.py
Normal file
69
lms/djangoapps/badges/migrations/0001_initial.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
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
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BadgeAssertion',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('data', jsonfield.fields.JSONField()),
|
||||
('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, 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()),
|
||||
('criteria', models.TextField()),
|
||||
('mode', models.CharField(default=b'', max_length=100, blank=True)),
|
||||
('image', models.ImageField(upload_to=b'badge_classes', validators=[badges.models.validate_badge_image])),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CourseCompleteImageConfiguration',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('mode', models.CharField(help_text='The course mode for this badge image. For example, "verified" or "honor".', unique=True, max_length=125)),
|
||||
('icon', models.ImageField(help_text='Badge images must be square PNG files. The file size should be under 250KB.', upload_to=b'course_complete_badges', validators=[badges.models.validate_badge_image])),
|
||||
('default', models.BooleanField(default=False, help_text='Set this value to True if you want this image to be the default image for any course modes that do not have a specified badge image. You can have only one default image.')),
|
||||
],
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='badgeclass',
|
||||
unique_together=set([('slug', 'issuing_component', 'course_id')]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='badgeassertion',
|
||||
name='badge_class',
|
||||
field=models.ForeignKey(to='badges.BadgeClass'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='badgeassertion',
|
||||
name='user',
|
||||
field=models.ForeignKey(to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,114 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
import os
|
||||
import time
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def forwards(apps, schema_editor):
|
||||
"""
|
||||
Migrate the initial badge classes, assertions, and course image configurations from certificates.
|
||||
"""
|
||||
from django.core.files.base import ContentFile
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from badges.events import course_complete
|
||||
db_alias = schema_editor.connection.alias
|
||||
# This will need to be changed if badges/certificates get moved out of the default db for some reason.
|
||||
if db_alias != 'default':
|
||||
return
|
||||
classes = {}
|
||||
OldBadgeAssertion = apps.get_model("certificates", "BadgeAssertion")
|
||||
BadgeImageConfiguration = apps.get_model("certificates", "BadgeImageConfiguration")
|
||||
BadgeAssertion = apps.get_model("badges", "BadgeAssertion")
|
||||
BadgeClass = apps.get_model("badges", "BadgeClass")
|
||||
CourseCompleteImageConfiguration = apps.get_model("badges", "CourseCompleteImageConfiguration")
|
||||
for badge in OldBadgeAssertion.objects.all():
|
||||
if (badge.course_id, badge.mode) not in classes:
|
||||
course = modulestore().get_course(badge.course_id)
|
||||
image_config = BadgeImageConfiguration.objects.get(mode=badge.mode)
|
||||
icon = image_config.icon
|
||||
badge_class = BadgeClass(
|
||||
display_name=course.display_name,
|
||||
criteria=course_complete.evidence_url(badge.user_id, badge.course_id),
|
||||
description=course_complete.badge_description(course, badge.mode),
|
||||
slug=course_complete.course_slug(badge.course_id, badge.mode),
|
||||
mode=image_config.mode,
|
||||
course_id=badge.course_id,
|
||||
)
|
||||
badge_class._meta.get_field('image').generate_filename = \
|
||||
lambda inst, fn: os.path.join('badge_classes', fn)
|
||||
badge_class.image.name = icon.name
|
||||
badge_class.save()
|
||||
classes[(badge.course_id, badge.mode)] = badge_class
|
||||
if isinstance(badge.data, basestring):
|
||||
data = badge.data
|
||||
else:
|
||||
data = json.dumps(badge.data)
|
||||
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'],
|
||||
)
|
||||
assertion.save()
|
||||
# Would be overwritten by the first save.
|
||||
assertion.created = datetime.fromtimestamp(
|
||||
# Later versions of badgr include microseconds, but they aren't certain to be there.
|
||||
time.mktime(time.strptime(badge.data['created_at'].split('.')[0], "%Y-%m-%dT%H:%M:%S"))
|
||||
)
|
||||
assertion.save()
|
||||
|
||||
for configuration in BadgeImageConfiguration.objects.all():
|
||||
new_conf = CourseCompleteImageConfiguration(
|
||||
default=configuration.default,
|
||||
mode=configuration.mode,
|
||||
)
|
||||
new_conf.icon.name = configuration.icon.name
|
||||
new_conf.save()
|
||||
|
||||
#
|
||||
def backwards(apps, schema_editor):
|
||||
OldBadgeAssertion = apps.get_model("certificates", "BadgeAssertion")
|
||||
BadgeAssertion = apps.get_model("badges", "BadgeAssertion")
|
||||
BadgeImageConfiguration = apps.get_model("certificates", "BadgeImageConfiguration")
|
||||
CourseCompleteImageConfiguration = apps.get_model("badges", "CourseCompleteImageConfiguration")
|
||||
for badge in BadgeAssertion.objects.all():
|
||||
if not badge.badge_class.mode:
|
||||
# Can't preserve old badges without modes.
|
||||
continue
|
||||
if isinstance(badge.data, basestring):
|
||||
data = badge.data
|
||||
else:
|
||||
data = json.dumps(badge.data)
|
||||
OldBadgeAssertion(
|
||||
user_id=badge.user_id,
|
||||
course_id=badge.badge_class.course_id,
|
||||
mode=badge.badge_class.mode,
|
||||
data=data,
|
||||
).save()
|
||||
|
||||
for configuration in CourseCompleteImageConfiguration.objects.all():
|
||||
new_conf = BadgeImageConfiguration(
|
||||
default=configuration.default,
|
||||
mode=configuration.mode,
|
||||
)
|
||||
new_conf.icon.name = configuration.icon.name
|
||||
new_conf.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('badges', '0001_initial'),
|
||||
('certificates', '0007_certificateinvalidation')
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(forwards, backwards)
|
||||
]
|
||||
@@ -0,0 +1,33 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('badges', '0002_data__migrate_assertions'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CourseEventBadgesConfiguration',
|
||||
fields=[
|
||||
('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 that has the issuing component 'openedx__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 that has the issuing component 'openedx__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 you have created that has an issuing component of 'openedx__course'. The remaining items in each line are the course keys the learner needs 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')),
|
||||
],
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='badgeclass',
|
||||
options={'verbose_name_plural': 'Badge Classes'},
|
||||
),
|
||||
]
|
||||
0
lms/djangoapps/badges/migrations/__init__.py
Normal file
0
lms/djangoapps/badges/migrations/__init__.py
Normal file
321
lms/djangoapps/badges/models.py
Normal file
321
lms/djangoapps/badges/models.py
Normal file
@@ -0,0 +1,321 @@
|
||||
"""
|
||||
Database models for the badges app
|
||||
"""
|
||||
from importlib import import_module
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from jsonfield import JSONField
|
||||
from lazy import lazy
|
||||
from model_utils.models import TimeStampedModel
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from badges.utils import deserialize_count_specs
|
||||
from config_models.models import ConfigurationModel
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule_django.models import CourseKeyField
|
||||
|
||||
|
||||
def validate_badge_image(image):
|
||||
"""
|
||||
Validates that a particular image is small enough to be a badge and square.
|
||||
"""
|
||||
if image.width != image.height:
|
||||
raise ValidationError(_(u"The badge image must be square."))
|
||||
if not image.size < (250 * 1024):
|
||||
raise ValidationError(_(u"The badge image file size must be less than 250KB."))
|
||||
|
||||
|
||||
def validate_lowercase(string):
|
||||
"""
|
||||
Validates that a string is lowercase.
|
||||
"""
|
||||
if not string.islower():
|
||||
raise ValidationError(_(u"This value must be all lowercase."))
|
||||
|
||||
|
||||
class CourseBadgesDisabledError(Exception):
|
||||
"""
|
||||
Exception raised when Course Badges aren't enabled, but an attempt to fetch one is made anyway.
|
||||
"""
|
||||
|
||||
|
||||
class BadgeClass(models.Model):
|
||||
"""
|
||||
Specifies a badge class to be registered with a backend.
|
||||
"""
|
||||
slug = models.SlugField(max_length=255, validators=[validate_lowercase])
|
||||
issuing_component = models.SlugField(max_length=50, default='', blank=True, validators=[validate_lowercase])
|
||||
display_name = models.CharField(max_length=255)
|
||||
course_id = CourseKeyField(max_length=255, blank=True, default=None)
|
||||
description = models.TextField()
|
||||
criteria = models.TextField()
|
||||
# Mode a badge was awarded for. Included for legacy/migration purposes.
|
||||
mode = models.CharField(max_length=100, default='', blank=True)
|
||||
image = models.ImageField(upload_to='badge_classes', validators=[validate_badge_image])
|
||||
|
||||
def __unicode__(self):
|
||||
return u"<Badge '{slug}' for '{issuing_component}'>".format(
|
||||
slug=self.slug, issuing_component=self.issuing_component
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_badge_class(
|
||||
cls, slug, issuing_component, display_name=None, description=None, criteria=None, image_file_handle=None,
|
||||
mode='', course_id=None, create=True
|
||||
):
|
||||
"""
|
||||
Looks up a badge class by its slug, issuing component, and course_id and returns it should it exist.
|
||||
If it does not exist, and create is True, creates it according to the arguments. Otherwise, returns None.
|
||||
|
||||
The expectation is that an XBlock or platform developer should not need to concern themselves with whether
|
||||
or not a badge class has already been created, but should just feed all requirements to this function
|
||||
and it will 'do the right thing'. It should be the exception, rather than the common case, that a badge class
|
||||
would need to be looked up without also being created were it missing.
|
||||
"""
|
||||
slug = slug.lower()
|
||||
issuing_component = issuing_component.lower()
|
||||
if course_id and not modulestore().get_course(course_id).issue_badges:
|
||||
raise CourseBadgesDisabledError("This course does not have badges enabled.")
|
||||
if not course_id:
|
||||
course_id = CourseKeyField.Empty
|
||||
try:
|
||||
return cls.objects.get(slug=slug, issuing_component=issuing_component, course_id=course_id)
|
||||
except cls.DoesNotExist:
|
||||
if not create:
|
||||
return None
|
||||
badge_class = cls(
|
||||
slug=slug,
|
||||
issuing_component=issuing_component,
|
||||
display_name=display_name,
|
||||
course_id=course_id,
|
||||
mode=mode,
|
||||
description=description,
|
||||
criteria=criteria,
|
||||
)
|
||||
badge_class.image.save(image_file_handle.name, image_file_handle)
|
||||
badge_class.full_clean()
|
||||
badge_class.save()
|
||||
return badge_class
|
||||
|
||||
@lazy
|
||||
def backend(self):
|
||||
"""
|
||||
Loads the badging backend.
|
||||
"""
|
||||
module, klass = settings.BADGING_BACKEND.rsplit('.', 1)
|
||||
module = import_module(module)
|
||||
return getattr(module, klass)()
|
||||
|
||||
def get_for_user(self, user):
|
||||
"""
|
||||
Get the assertion for this badge class for this user, if it has been awarded.
|
||||
"""
|
||||
return self.badgeassertion_set.filter(user=user)
|
||||
|
||||
def award(self, user, evidence_url=None):
|
||||
"""
|
||||
Contacts the backend to have a badge assertion created for this badge class for this user.
|
||||
"""
|
||||
return self.backend.award(self, user, evidence_url=evidence_url)
|
||||
|
||||
def save(self, **kwargs):
|
||||
"""
|
||||
Slugs must always be lowercase.
|
||||
"""
|
||||
self.slug = self.slug and self.slug.lower()
|
||||
self.issuing_component = self.issuing_component and self.issuing_component.lower()
|
||||
super(BadgeClass, self).save(**kwargs)
|
||||
|
||||
class Meta(object):
|
||||
app_label = "badges"
|
||||
unique_together = (('slug', 'issuing_component', 'course_id'),)
|
||||
verbose_name_plural = "Badge Classes"
|
||||
|
||||
|
||||
class BadgeAssertion(TimeStampedModel):
|
||||
"""
|
||||
Tracks badges on our side of the badge baking transaction
|
||||
"""
|
||||
user = models.ForeignKey(User)
|
||||
badge_class = models.ForeignKey(BadgeClass)
|
||||
data = JSONField()
|
||||
backend = models.CharField(max_length=50)
|
||||
image_url = models.URLField()
|
||||
assertion_url = models.URLField()
|
||||
|
||||
def __unicode__(self):
|
||||
return u"<{username} Badge Assertion for {slug} for {issuing_component}".format(
|
||||
username=self.user.username, slug=self.badge_class.slug,
|
||||
issuing_component=self.badge_class.issuing_component,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def assertions_for_user(cls, user, course_id=None):
|
||||
"""
|
||||
Get all assertions for a user, optionally constrained to a course.
|
||||
"""
|
||||
if course_id:
|
||||
return cls.objects.filter(user=user, badge_class__course_id=course_id)
|
||||
return cls.objects.filter(user=user)
|
||||
|
||||
class Meta(object):
|
||||
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.
|
||||
"""
|
||||
mode = models.CharField(
|
||||
max_length=125,
|
||||
help_text=_(u'The course mode for this badge image. For example, "verified" or "honor".'),
|
||||
unique=True,
|
||||
)
|
||||
icon = models.ImageField(
|
||||
# Actual max is 256KB, but need overhead for badge baking. This should be more than enough.
|
||||
help_text=_(
|
||||
u"Badge images must be square PNG files. The file size should be under 250KB."
|
||||
),
|
||||
upload_to='course_complete_badges',
|
||||
validators=[validate_badge_image]
|
||||
)
|
||||
default = models.BooleanField(
|
||||
help_text=_(
|
||||
u"Set this value to True if you want this image to be the default image for any course modes "
|
||||
u"that do not have a specified badge image. You can have only one default image."
|
||||
),
|
||||
default=False,
|
||||
)
|
||||
|
||||
def __unicode__(self):
|
||||
return u"<CourseCompleteImageConfiguration for '{mode}'{default}>".format(
|
||||
mode=self.mode,
|
||||
default=u" (default)" if self.default else u''
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
Make sure there's not more than one default.
|
||||
"""
|
||||
# pylint: disable=no-member
|
||||
if self.default and CourseCompleteImageConfiguration.objects.filter(default=True).exclude(id=self.id):
|
||||
raise ValidationError(_(u"There can be only one default image."))
|
||||
|
||||
@classmethod
|
||||
def image_for_mode(cls, mode):
|
||||
"""
|
||||
Get the image for a particular mode.
|
||||
"""
|
||||
try:
|
||||
return cls.objects.get(mode=mode).icon
|
||||
except cls.DoesNotExist:
|
||||
# Fall back to default, if there is one.
|
||||
return cls.objects.get(default=True).icon
|
||||
|
||||
class Meta(object):
|
||||
app_label = "badges"
|
||||
|
||||
|
||||
class CourseEventBadgesConfiguration(ConfigurationModel):
|
||||
"""
|
||||
Determines the settings for meta course awards-- such as completing a certain
|
||||
number of courses or enrolling in a certain number of them.
|
||||
"""
|
||||
courses_completed = models.TextField(
|
||||
blank=True, default='',
|
||||
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 that has the issuing component 'openedx__course'. "
|
||||
u"For example: 3,enrolled_3_courses"
|
||||
)
|
||||
)
|
||||
courses_enrolled = models.TextField(
|
||||
blank=True, default='',
|
||||
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 that has the issuing component 'openedx__course'. "
|
||||
u"For example: 3,enrolled_3_courses"
|
||||
)
|
||||
)
|
||||
course_groups = models.TextField(
|
||||
blank=True, default='',
|
||||
help_text=_(
|
||||
u"Each line is a comma-separated list. The first item in each line is the slug of a badge class you "
|
||||
u"have created that has an issuing component of 'openedx__course'. The remaining items in each line are "
|
||||
u"the course keys the learner needs 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"
|
||||
)
|
||||
)
|
||||
|
||||
def __unicode__(self):
|
||||
return u"<CourseEventBadgesConfiguration ({})>".format(u"Enabled" if self.enabled else u"Disabled")
|
||||
|
||||
@property
|
||||
def completed_settings(self):
|
||||
"""
|
||||
Parses the settings from the courses_completed field.
|
||||
"""
|
||||
return deserialize_count_specs(self.courses_completed)
|
||||
|
||||
@property
|
||||
def enrolled_settings(self):
|
||||
"""
|
||||
Parses the settings from the courses_completed field.
|
||||
"""
|
||||
return deserialize_count_specs(self.courses_enrolled)
|
||||
|
||||
@property
|
||||
def course_group_settings(self):
|
||||
"""
|
||||
Parses the course group settings. In example, the format is:
|
||||
|
||||
slug_for_compsci_courses_group_badge,course-v1:CompSci+Course+First,course-v1:CompsSci+Course+Second
|
||||
"""
|
||||
specs = self.course_groups.strip()
|
||||
if not specs:
|
||||
return {}
|
||||
specs = [line.split(',', 1) for line in specs.splitlines()]
|
||||
return {
|
||||
slug.strip().lower(): [CourseKey.from_string(key.strip()) for key in keys.strip().split(',')]
|
||||
for slug, keys in specs
|
||||
}
|
||||
|
||||
def clean_fields(self, exclude=tuple()):
|
||||
"""
|
||||
Verify the settings are parseable.
|
||||
"""
|
||||
errors = {}
|
||||
error_message = _(u"Please check the syntax of your entry.")
|
||||
if 'courses_completed' not in exclude:
|
||||
try:
|
||||
self.completed_settings
|
||||
except (ValueError, InvalidKeyError):
|
||||
errors['courses_completed'] = [unicode(error_message)]
|
||||
if 'courses_enrolled' not in exclude:
|
||||
try:
|
||||
self.enrolled_settings
|
||||
except (ValueError, InvalidKeyError):
|
||||
errors['courses_enrolled'] = [unicode(error_message)]
|
||||
if 'course_groups' not in exclude:
|
||||
store = modulestore()
|
||||
try:
|
||||
for key_list in self.course_group_settings.values():
|
||||
for course_key in key_list:
|
||||
if not store.get_course(course_key):
|
||||
ValueError(u"The course {course_key} does not exist.".format(course_key=course_key))
|
||||
except (ValueError, InvalidKeyError):
|
||||
errors['course_groups'] = [unicode(error_message)]
|
||||
if errors:
|
||||
raise ValidationError(errors)
|
||||
|
||||
class Meta(object):
|
||||
app_label = "badges"
|
||||
28
lms/djangoapps/badges/service.py
Normal file
28
lms/djangoapps/badges/service.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
Badging service for XBlocks
|
||||
"""
|
||||
from badges.models import BadgeClass
|
||||
|
||||
|
||||
class BadgingService(object):
|
||||
"""
|
||||
A class that provides functions for managing badges which XBlocks can use.
|
||||
|
||||
If course_enabled is True, course-level badges are permitted for this course.
|
||||
|
||||
If it is False, any badges that are awarded should be non-course specific.
|
||||
"""
|
||||
course_badges_enabled = False
|
||||
|
||||
def __init__(self, course_id=None, modulestore=None):
|
||||
"""
|
||||
Sets the 'course_badges_enabled' parameter.
|
||||
"""
|
||||
if not (course_id and modulestore):
|
||||
return
|
||||
|
||||
course = modulestore.get_course(course_id)
|
||||
if course:
|
||||
self.course_badges_enabled = course.issue_badges
|
||||
|
||||
get_badge_class = BadgeClass.get_badge_class
|
||||
0
lms/djangoapps/badges/tests/__init__.py
Normal file
0
lms/djangoapps/badges/tests/__init__.py
Normal file
81
lms/djangoapps/badges/tests/factories.py
Normal file
81
lms/djangoapps/badges/tests/factories.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
Factories for Badge tests
|
||||
"""
|
||||
from random import random
|
||||
|
||||
import factory
|
||||
from django.core.files.base import ContentFile
|
||||
from factory import DjangoModelFactory
|
||||
from factory.django import ImageField
|
||||
|
||||
from badges.models import BadgeAssertion, CourseCompleteImageConfiguration, BadgeClass, CourseEventBadgesConfiguration
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
|
||||
def generate_dummy_image(_unused):
|
||||
"""
|
||||
Used for image fields to create a sane default.
|
||||
"""
|
||||
return ContentFile(
|
||||
ImageField()._make_data( # pylint: disable=protected-access
|
||||
{'color': 'blue', 'width': 50, 'height': 50, 'format': 'PNG'}
|
||||
), 'test.png'
|
||||
)
|
||||
|
||||
|
||||
class CourseCompleteImageConfigurationFactory(DjangoModelFactory):
|
||||
"""
|
||||
Factory for BadgeImageConfigurations
|
||||
"""
|
||||
class Meta(object):
|
||||
model = CourseCompleteImageConfiguration
|
||||
|
||||
mode = 'honor'
|
||||
icon = factory.LazyAttribute(generate_dummy_image)
|
||||
|
||||
|
||||
class BadgeClassFactory(DjangoModelFactory):
|
||||
"""
|
||||
Factory for BadgeClass
|
||||
"""
|
||||
class Meta(object):
|
||||
model = BadgeClass
|
||||
|
||||
slug = 'test_slug'
|
||||
issuing_component = 'test_component'
|
||||
display_name = 'Test Badge'
|
||||
description = "Yay! It's a test badge."
|
||||
criteria = 'https://example.com/syllabus'
|
||||
mode = 'honor'
|
||||
image = factory.LazyAttribute(generate_dummy_image)
|
||||
|
||||
|
||||
class RandomBadgeClassFactory(BadgeClassFactory):
|
||||
"""
|
||||
Same as BadgeClassFactory, but randomize the slug.
|
||||
"""
|
||||
slug = factory.lazy_attribute(lambda _: 'test_slug_' + str(random()).replace('.', '_'))
|
||||
|
||||
|
||||
class BadgeAssertionFactory(DjangoModelFactory):
|
||||
"""
|
||||
Factory for BadgeAssertions
|
||||
"""
|
||||
class Meta(object):
|
||||
model = BadgeAssertion
|
||||
|
||||
user = factory.SubFactory(UserFactory)
|
||||
badge_class = factory.SubFactory(RandomBadgeClassFactory)
|
||||
data = {}
|
||||
assertion_url = 'http://example.com/example.json'
|
||||
image_url = 'http://example.com/image.png'
|
||||
|
||||
|
||||
class CourseEventBadgesConfigurationFactory(DjangoModelFactory):
|
||||
"""
|
||||
Factory for CourseEventsBadgesConfiguration
|
||||
"""
|
||||
class Meta(object):
|
||||
model = CourseEventBadgesConfiguration
|
||||
|
||||
enabled = True
|
||||
269
lms/djangoapps/badges/tests/test_models.py
Normal file
269
lms/djangoapps/badges/tests/test_models.py
Normal file
@@ -0,0 +1,269 @@
|
||||
"""
|
||||
Tests for the Badges app models.
|
||||
"""
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.images import ImageFile
|
||||
from django.db.utils import IntegrityError
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from mock import patch, Mock
|
||||
from nose.plugins.attrib import attr
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
from badges.models import (
|
||||
CourseCompleteImageConfiguration, validate_badge_image, BadgeClass, BadgeAssertion,
|
||||
CourseBadgesDisabledError
|
||||
)
|
||||
from badges.tests.factories import BadgeClassFactory, BadgeAssertionFactory, RandomBadgeClassFactory
|
||||
from certificates.tests.test_models import TEST_DATA_ROOT
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
|
||||
def get_image(name):
|
||||
"""
|
||||
Get one of the test images from the test data directory.
|
||||
"""
|
||||
return ImageFile(open(TEST_DATA_ROOT / 'badges' / name + '.png'))
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
class BadgeImageConfigurationTest(TestCase):
|
||||
"""
|
||||
Test the validation features of BadgeImageConfiguration.
|
||||
"""
|
||||
|
||||
def test_no_double_default(self):
|
||||
"""
|
||||
Verify that creating two configurations as default is not permitted.
|
||||
"""
|
||||
CourseCompleteImageConfiguration(mode='test', icon=get_image('good'), default=True).save()
|
||||
self.assertRaises(
|
||||
ValidationError,
|
||||
CourseCompleteImageConfiguration(mode='test2', icon=get_image('good'), default=True).full_clean
|
||||
)
|
||||
|
||||
def test_runs_validator(self):
|
||||
"""
|
||||
Verify that the image validator is triggered when cleaning the model.
|
||||
"""
|
||||
self.assertRaises(
|
||||
ValidationError,
|
||||
CourseCompleteImageConfiguration(mode='test2', icon=get_image('unbalanced')).full_clean
|
||||
)
|
||||
|
||||
|
||||
class DummyBackend(object):
|
||||
"""
|
||||
Dummy badge backend, used for testing.
|
||||
"""
|
||||
award = Mock()
|
||||
|
||||
|
||||
class BadgeClassTest(ModuleStoreTestCase):
|
||||
"""
|
||||
Test BadgeClass functionality
|
||||
"""
|
||||
# Need full path to make sure class names line up.
|
||||
@override_settings(BADGING_BACKEND='lms.djangoapps.badges.tests.test_models.DummyBackend')
|
||||
def test_backend(self):
|
||||
"""
|
||||
Verify the BadgeClass fetches the backend properly.
|
||||
"""
|
||||
self.assertIsInstance(BadgeClass().backend, DummyBackend)
|
||||
|
||||
def test_get_badge_class_preexisting(self):
|
||||
"""
|
||||
Verify fetching a badge first grabs existing badges.
|
||||
"""
|
||||
premade_badge_class = BadgeClassFactory.create()
|
||||
# Ignore additional parameters. This class already exists.
|
||||
badge_class = BadgeClass.get_badge_class(
|
||||
slug='test_slug', issuing_component='test_component', description='Attempted override',
|
||||
criteria='test', display_name='Testola', image_file_handle=get_image('good')
|
||||
)
|
||||
# These defaults are set on the factory.
|
||||
self.assertEqual(badge_class.criteria, 'https://example.com/syllabus')
|
||||
self.assertEqual(badge_class.display_name, 'Test Badge')
|
||||
self.assertEqual(badge_class.description, "Yay! It's a test badge.")
|
||||
# File name won't always be the same.
|
||||
self.assertEqual(badge_class.image.path, premade_badge_class.image.path)
|
||||
|
||||
def test_unique_for_course(self):
|
||||
"""
|
||||
Verify that the course_id is used in fetching existing badges or creating new ones.
|
||||
"""
|
||||
course_key = CourseFactory.create().location.course_key
|
||||
premade_badge_class = BadgeClassFactory.create(course_id=course_key)
|
||||
badge_class = BadgeClass.get_badge_class(
|
||||
slug='test_slug', issuing_component='test_component', description='Attempted override',
|
||||
criteria='test', display_name='Testola', image_file_handle=get_image('good')
|
||||
)
|
||||
course_badge_class = BadgeClass.get_badge_class(
|
||||
slug='test_slug', issuing_component='test_component', description='Attempted override',
|
||||
criteria='test', display_name='Testola', image_file_handle=get_image('good'),
|
||||
course_id=course_key,
|
||||
)
|
||||
self.assertNotEqual(badge_class.id, course_badge_class.id)
|
||||
self.assertEqual(course_badge_class.id, premade_badge_class.id)
|
||||
|
||||
def test_get_badge_class_course_disabled(self):
|
||||
"""
|
||||
Verify attempting to fetch a badge class for a course which does not issue badges raises an
|
||||
exception.
|
||||
"""
|
||||
course_key = CourseFactory.create(metadata={'issue_badges': False}).location.course_key
|
||||
with self.assertRaises(CourseBadgesDisabledError):
|
||||
BadgeClass.get_badge_class(
|
||||
slug='test_slug', issuing_component='test_component', description='Attempted override',
|
||||
criteria='test', display_name='Testola', image_file_handle=get_image('good'),
|
||||
course_id=course_key,
|
||||
)
|
||||
|
||||
def test_get_badge_class_create(self):
|
||||
"""
|
||||
Verify fetching a badge creates it if it doesn't yet exist.
|
||||
"""
|
||||
badge_class = BadgeClass.get_badge_class(
|
||||
slug='new_slug', issuing_component='new_component', description='This is a test',
|
||||
criteria='https://example.com/test_criteria', display_name='Super Badge',
|
||||
image_file_handle=get_image('good')
|
||||
)
|
||||
# This should have been saved before being passed back.
|
||||
self.assertTrue(badge_class.id)
|
||||
self.assertEqual(badge_class.slug, 'new_slug')
|
||||
self.assertEqual(badge_class.issuing_component, 'new_component')
|
||||
self.assertEqual(badge_class.description, 'This is a test')
|
||||
self.assertEqual(badge_class.criteria, 'https://example.com/test_criteria')
|
||||
self.assertEqual(badge_class.display_name, 'Super Badge')
|
||||
self.assertEqual(badge_class.image.name.rsplit('/', 1)[-1], 'good.png')
|
||||
|
||||
def test_get_badge_class_nocreate(self):
|
||||
"""
|
||||
Test returns None if the badge class does not exist.
|
||||
"""
|
||||
badge_class = BadgeClass.get_badge_class(
|
||||
slug='new_slug', issuing_component='new_component', create=False
|
||||
)
|
||||
self.assertIsNone(badge_class)
|
||||
# Run this twice to verify there wasn't a background creation of the badge.
|
||||
badge_class = BadgeClass.get_badge_class(
|
||||
slug='new_slug', issuing_component='new_component', description=None,
|
||||
criteria=None, display_name=None,
|
||||
image_file_handle=None, create=False
|
||||
)
|
||||
self.assertIsNone(badge_class)
|
||||
|
||||
def test_get_badge_class_image_validate(self):
|
||||
"""
|
||||
Verify handing a broken image to get_badge_class raises a validation error upon creation.
|
||||
"""
|
||||
self.assertRaises(
|
||||
ValidationError,
|
||||
BadgeClass.get_badge_class,
|
||||
slug='new_slug', issuing_component='new_component', description='This is a test',
|
||||
criteria='https://example.com/test_criteria', display_name='Super Badge',
|
||||
image_file_handle=get_image('unbalanced')
|
||||
)
|
||||
|
||||
def test_get_badge_class_data_validate(self):
|
||||
"""
|
||||
Verify handing incomplete data for required fields when making a badge class raises an Integrity error.
|
||||
"""
|
||||
self.assertRaises(
|
||||
IntegrityError,
|
||||
BadgeClass.get_badge_class,
|
||||
slug='new_slug', issuing_component='new_component',
|
||||
image_file_handle=get_image('good')
|
||||
)
|
||||
|
||||
def test_get_for_user(self):
|
||||
"""
|
||||
Make sure we can get an assertion for a user if there is one.
|
||||
"""
|
||||
user = UserFactory.create()
|
||||
badge_class = BadgeClassFactory.create()
|
||||
self.assertFalse(badge_class.get_for_user(user))
|
||||
assertion = BadgeAssertionFactory.create(badge_class=badge_class, user=user)
|
||||
self.assertEqual(list(badge_class.get_for_user(user)), [assertion])
|
||||
|
||||
@override_settings(BADGING_BACKEND='lms.djangoapps.badges.backends.badgr.BadgrBackend', BADGR_API_TOKEN='test')
|
||||
@patch('lms.djangoapps.badges.backends.badgr.BadgrBackend.award')
|
||||
def test_award(self, mock_award):
|
||||
"""
|
||||
Verify that the award command calls the award function on the backend with the right parameters.
|
||||
"""
|
||||
user = UserFactory.create()
|
||||
badge_class = BadgeClassFactory.create()
|
||||
badge_class.award(user, evidence_url='http://example.com/evidence')
|
||||
self.assertTrue(mock_award.called)
|
||||
mock_award.assert_called_with(badge_class, user, evidence_url='http://example.com/evidence')
|
||||
|
||||
def test_runs_validator(self):
|
||||
"""
|
||||
Verify that the image validator is triggered when cleaning the model.
|
||||
"""
|
||||
self.assertRaises(
|
||||
ValidationError,
|
||||
BadgeClass(
|
||||
slug='test', issuing_component='test2', criteria='test3',
|
||||
description='test4', image=get_image('unbalanced')
|
||||
).full_clean
|
||||
)
|
||||
|
||||
|
||||
class BadgeAssertionTest(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for the BadgeAssertion model
|
||||
"""
|
||||
def test_assertions_for_user(self):
|
||||
"""
|
||||
Verify that grabbing all assertions for a user behaves as expected.
|
||||
|
||||
This function uses object IDs because for some reason Jenkins trips up
|
||||
on its assertItemsEqual check here despite the items being equal.
|
||||
"""
|
||||
user = UserFactory()
|
||||
assertions = [BadgeAssertionFactory.create(user=user).id for _i in range(3)]
|
||||
course = CourseFactory.create()
|
||||
course_key = course.location.course_key
|
||||
course_badges = [RandomBadgeClassFactory(course_id=course_key) for _i in range(3)]
|
||||
course_assertions = [
|
||||
BadgeAssertionFactory.create(user=user, badge_class=badge_class).id for badge_class in course_badges
|
||||
]
|
||||
assertions.extend(course_assertions)
|
||||
assertions.sort()
|
||||
assertions_for_user = [badge.id for badge in BadgeAssertion.assertions_for_user(user)]
|
||||
assertions_for_user.sort()
|
||||
self.assertEqual(assertions_for_user, assertions)
|
||||
course_scoped_assertions = [
|
||||
badge.id for badge in BadgeAssertion.assertions_for_user(user, course_id=course_key)
|
||||
]
|
||||
course_scoped_assertions.sort()
|
||||
self.assertEqual(course_scoped_assertions, course_assertions)
|
||||
|
||||
|
||||
class ValidBadgeImageTest(TestCase):
|
||||
"""
|
||||
Tests the badge image field validator.
|
||||
"""
|
||||
def test_good_image(self):
|
||||
"""
|
||||
Verify that saving a valid badge image is no problem.
|
||||
"""
|
||||
validate_badge_image(get_image('good'))
|
||||
|
||||
def test_unbalanced_image(self):
|
||||
"""
|
||||
Verify that setting an image with an uneven width and height raises an error.
|
||||
"""
|
||||
unbalanced = ImageFile(get_image('unbalanced'))
|
||||
self.assertRaises(ValidationError, validate_badge_image, unbalanced)
|
||||
|
||||
def test_large_image(self):
|
||||
"""
|
||||
Verify that setting an image that is too big raises an error.
|
||||
"""
|
||||
large = get_image('large')
|
||||
self.assertRaises(ValidationError, validate_badge_image, large)
|
||||
46
lms/djangoapps/badges/utils.py
Normal file
46
lms/djangoapps/badges/utils.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""
|
||||
Utility functions used by the badging app.
|
||||
"""
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def site_prefix():
|
||||
"""
|
||||
Get the prefix for the site URL-- protocol and server name.
|
||||
"""
|
||||
scheme = u"https" if settings.HTTPS == "on" else u"http"
|
||||
return u'{}://{}'.format(scheme, settings.SITE_NAME)
|
||||
|
||||
|
||||
def requires_badges_enabled(function):
|
||||
"""
|
||||
Decorator that bails a function out early if badges aren't enabled.
|
||||
"""
|
||||
def wrapped(*args, **kwargs):
|
||||
"""
|
||||
Wrapped function which bails out early if bagdes aren't enabled.
|
||||
"""
|
||||
if not badges_enabled():
|
||||
return
|
||||
return function(*args, **kwargs)
|
||||
return wrapped
|
||||
|
||||
|
||||
def badges_enabled():
|
||||
"""
|
||||
returns a boolean indicating whether or not openbadges are enabled.
|
||||
"""
|
||||
return settings.FEATURES.get('ENABLE_OPENBADGES', False)
|
||||
|
||||
|
||||
def deserialize_count_specs(text):
|
||||
"""
|
||||
Takes a string in the format of:
|
||||
int,course_key
|
||||
int,course_key
|
||||
|
||||
And returns a dictionary with the keys as the numbers and the values as the course keys.
|
||||
"""
|
||||
specs = text.splitlines()
|
||||
specs = [line.split(',') for line in specs if line.strip()]
|
||||
return {int(num): slug.strip().lower() for num, slug in specs}
|
||||
@@ -8,7 +8,6 @@ from util.organizations_helpers import get_organizations
|
||||
from certificates.models import (
|
||||
CertificateGenerationConfiguration,
|
||||
CertificateHtmlViewConfiguration,
|
||||
BadgeImageConfiguration,
|
||||
CertificateTemplate,
|
||||
CertificateTemplateAsset,
|
||||
GeneratedCertificate,
|
||||
@@ -61,7 +60,6 @@ class GeneratedCertificateAdmin(admin.ModelAdmin):
|
||||
|
||||
admin.site.register(CertificateGenerationConfiguration)
|
||||
admin.site.register(CertificateHtmlViewConfiguration, ConfigurationModelAdmin)
|
||||
admin.site.register(BadgeImageConfiguration)
|
||||
admin.site.register(CertificateTemplate, CertificateTemplateAdmin)
|
||||
admin.site.register(CertificateTemplateAsset, CertificateTemplateAssetAdmin)
|
||||
admin.site.register(GeneratedCertificate, GeneratedCertificateAdmin)
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
"""
|
||||
BadgeHandler object-- used to award Badges to users who have completed courses.
|
||||
"""
|
||||
import hashlib
|
||||
import logging
|
||||
import mimetypes
|
||||
from eventtracking import tracker
|
||||
import requests
|
||||
from django.template.defaultfilters import slugify
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from lazy import lazy
|
||||
from requests.packages.urllib3.exceptions import HTTPError
|
||||
from certificates.models import BadgeAssertion, BadgeImageConfiguration
|
||||
from student.models import CourseEnrollment
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BadgeHandler(object):
|
||||
"""
|
||||
The only properly public method of this class is 'award'. If an alternative object is created for a different
|
||||
badging service, the other methods don't need to be reproduced.
|
||||
"""
|
||||
# Global caching dict
|
||||
badges = {}
|
||||
|
||||
def __init__(self, course_key):
|
||||
self.course_key = course_key
|
||||
assert settings.BADGR_API_TOKEN
|
||||
|
||||
@lazy
|
||||
def base_url(self):
|
||||
"""
|
||||
Base URL for all API requests.
|
||||
"""
|
||||
return "{}/v1/issuer/issuers/{}".format(settings.BADGR_BASE_URL, settings.BADGR_ISSUER_SLUG)
|
||||
|
||||
@lazy
|
||||
def badge_create_url(self):
|
||||
"""
|
||||
URL for generating a new Badge specification
|
||||
"""
|
||||
return "{}/badges".format(self.base_url)
|
||||
|
||||
def badge_url(self, mode):
|
||||
"""
|
||||
Get the URL for a course's badge in a given mode.
|
||||
"""
|
||||
return "{}/{}".format(self.badge_create_url, self.course_slug(mode))
|
||||
|
||||
def assertion_url(self, mode):
|
||||
"""
|
||||
URL for generating a new assertion.
|
||||
"""
|
||||
return "{}/assertions".format(self.badge_url(mode))
|
||||
|
||||
def course_slug(self, mode):
|
||||
"""
|
||||
Slug ought to be deterministic and limited in size so it's not too big for Badgr.
|
||||
|
||||
Badgr's max slug length is 255.
|
||||
"""
|
||||
# Seven digits should be enough to realistically avoid collisions. That's what git services use.
|
||||
digest = hashlib.sha256(u"{}{}".format(unicode(self.course_key), unicode(mode))).hexdigest()[:7]
|
||||
base_slug = slugify(unicode(self.course_key) + u'_{}_'.format(mode))[:248]
|
||||
return base_slug + digest
|
||||
|
||||
def log_if_raised(self, response, data):
|
||||
"""
|
||||
Log server response if there was an error.
|
||||
"""
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except HTTPError:
|
||||
LOGGER.error(
|
||||
u"Encountered an error when contacting the Badgr-Server. Request sent to %s with headers %s.\n"
|
||||
u"and data values %s\n"
|
||||
u"Response status was %s.\n%s",
|
||||
repr(response.request.url), repr(response.request.headers),
|
||||
repr(data),
|
||||
response.status_code, response.body
|
||||
)
|
||||
raise
|
||||
|
||||
def get_headers(self):
|
||||
"""
|
||||
Headers to send along with the request-- used for authentication.
|
||||
"""
|
||||
return {'Authorization': 'Token {}'.format(settings.BADGR_API_TOKEN)}
|
||||
|
||||
def ensure_badge_created(self, mode):
|
||||
"""
|
||||
Verify a badge has been created for this mode of the course, and, if not, create it
|
||||
"""
|
||||
if self.course_slug(mode) in BadgeHandler.badges:
|
||||
return
|
||||
response = requests.get(self.badge_url(mode), headers=self.get_headers())
|
||||
if response.status_code != 200:
|
||||
self.create_badge(mode)
|
||||
BadgeHandler.badges[self.course_slug(mode)] = True
|
||||
|
||||
@staticmethod
|
||||
def badge_description(course, mode):
|
||||
"""
|
||||
Returns a description for the earned badge.
|
||||
"""
|
||||
if course.end:
|
||||
return _(u'Completed the course "{course_name}" ({course_mode}, {start_date} - {end_date})').format(
|
||||
start_date=course.start.date(),
|
||||
end_date=course.end.date(),
|
||||
course_name=course.display_name,
|
||||
course_mode=mode,
|
||||
)
|
||||
else:
|
||||
return _(u'Completed the course "{course_name}" ({course_mode})').format(
|
||||
course_name=course.display_name,
|
||||
course_mode=mode,
|
||||
)
|
||||
|
||||
def site_prefix(self):
|
||||
"""
|
||||
Get the prefix for the site URL-- protocol and server name.
|
||||
"""
|
||||
scheme = u"https" if settings.HTTPS == "on" else u"http"
|
||||
return u'{}://{}'.format(scheme, settings.SITE_NAME)
|
||||
|
||||
def create_badge(self, mode):
|
||||
"""
|
||||
Create the badge spec for a course's mode.
|
||||
"""
|
||||
course = modulestore().get_course(self.course_key)
|
||||
image = BadgeImageConfiguration.image_for_mode(mode)
|
||||
# We don't want to bother validating the file any further than making sure we can detect its MIME type,
|
||||
# for HTTP. The Badgr-Server should tell us if there's anything in particular wrong with it.
|
||||
content_type, __ = mimetypes.guess_type(image.name)
|
||||
if not content_type:
|
||||
raise ValueError(
|
||||
"Could not determine content-type of image! Make sure it is a properly named .png file."
|
||||
)
|
||||
files = {'image': (image.name, image, content_type)}
|
||||
about_path = reverse('about_course', kwargs={'course_id': unicode(self.course_key)})
|
||||
data = {
|
||||
'name': course.display_name,
|
||||
'criteria': u'{}{}'.format(self.site_prefix(), about_path),
|
||||
'slug': self.course_slug(mode),
|
||||
'description': self.badge_description(course, mode)
|
||||
}
|
||||
result = requests.post(self.badge_create_url, headers=self.get_headers(), data=data, files=files)
|
||||
self.log_if_raised(result, data)
|
||||
|
||||
def send_assertion_created_event(self, user, assertion):
|
||||
"""
|
||||
Send an analytics event to record the creation of a badge assertion.
|
||||
"""
|
||||
tracker.emit(
|
||||
'edx.badge.assertion.created', {
|
||||
'user_id': user.id,
|
||||
'course_id': unicode(self.course_key),
|
||||
'enrollment_mode': assertion.mode,
|
||||
'assertion_id': assertion.id,
|
||||
'assertion_image_url': assertion.data['image'],
|
||||
'assertion_json_url': assertion.data['json']['id'],
|
||||
'issuer': assertion.data['issuer'],
|
||||
}
|
||||
)
|
||||
|
||||
def create_assertion(self, user, mode):
|
||||
"""
|
||||
Register an assertion with the Badgr server for a particular user in a particular course mode for
|
||||
this course.
|
||||
"""
|
||||
data = {
|
||||
'email': user.email,
|
||||
'evidence': self.site_prefix() + reverse(
|
||||
'certificates:html_view', kwargs={'user_id': user.id, 'course_id': unicode(self.course_key)}
|
||||
) + '?evidence_visit=1'
|
||||
}
|
||||
response = requests.post(self.assertion_url(mode), headers=self.get_headers(), data=data)
|
||||
self.log_if_raised(response, data)
|
||||
assertion, __ = BadgeAssertion.objects.get_or_create(course_id=self.course_key, user=user, mode=mode)
|
||||
assertion.data = response.json()
|
||||
assertion.save()
|
||||
self.send_assertion_created_event(user, assertion)
|
||||
|
||||
def award(self, user):
|
||||
"""
|
||||
Award a user a badge for their work on the course.
|
||||
"""
|
||||
mode = CourseEnrollment.objects.get(user=user, course_id=self.course_key).mode
|
||||
self.ensure_badge_created(mode)
|
||||
self.create_assertion(user, mode)
|
||||
@@ -8,8 +8,9 @@ from django.core.management.base import BaseCommand, CommandError
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
|
||||
from badges.events.course_complete import get_completion_badge
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from certificates.models import BadgeAssertion
|
||||
from certificates.api import regenerate_user_certificates
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
@@ -110,6 +111,14 @@ class Command(BaseCommand):
|
||||
course_id
|
||||
)
|
||||
|
||||
if course.issue_badges:
|
||||
badge_class = get_completion_badge(course_id, student)
|
||||
badge = badge_class.get_for_user(student)
|
||||
|
||||
if badge:
|
||||
badge.delete()
|
||||
LOGGER.info(u"Cleared badge for student %s.", student.id)
|
||||
|
||||
# Add the certificate request to the queue
|
||||
ret = regenerate_user_certificates(
|
||||
student, course_id, course=course,
|
||||
@@ -118,13 +127,6 @@ class Command(BaseCommand):
|
||||
insecure=options['insecure']
|
||||
)
|
||||
|
||||
try:
|
||||
badge = BadgeAssertion.objects.get(user=student, course_id=course_id)
|
||||
badge.delete()
|
||||
LOGGER.info(u"Cleared badge for student %s.", student.id)
|
||||
except BadgeAssertion.DoesNotExist:
|
||||
pass
|
||||
|
||||
LOGGER.info(
|
||||
(
|
||||
u"Added a certificate regeneration task to the XQueue "
|
||||
|
||||
@@ -9,6 +9,7 @@ import django_extensions.db.fields
|
||||
import django_extensions.db.fields.json
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from badges.models import validate_badge_image
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
@@ -34,7 +35,7 @@ class Migration(migrations.Migration):
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('mode', models.CharField(help_text='The course mode for this badge image. For example, "verified" or "honor".', unique=True, max_length=125)),
|
||||
('icon', models.ImageField(help_text='Badge images must be square PNG files. The file size should be under 250KB.', upload_to=b'badges', validators=[certificates.models.validate_badge_image])),
|
||||
('icon', models.ImageField(help_text='Badge images must be square PNG files. The file size should be under 250KB.', upload_to=b'badges', validators=[validate_badge_image])),
|
||||
('default', models.BooleanField(default=False, help_text='Set this value to True if you want this image to be the default image for any course modes that do not have a specified badge image. You can have only one default image.')),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -10,8 +10,11 @@ from django.core.files import File
|
||||
def forwards(apps, schema_editor):
|
||||
"""Add default modes"""
|
||||
BadgeImageConfiguration = apps.get_model("certificates", "BadgeImageConfiguration")
|
||||
|
||||
objects = BadgeImageConfiguration.objects
|
||||
db_alias = schema_editor.connection.alias
|
||||
# This will need to be changed if badges/certificates get moved out of the default db for some reason.
|
||||
if db_alias != 'default':
|
||||
return
|
||||
objects = BadgeImageConfiguration.objects.using(db_alias)
|
||||
if not objects.exists():
|
||||
for mode in ['honor', 'verified', 'professional']:
|
||||
conf = objects.create(mode=mode)
|
||||
@@ -34,5 +37,5 @@ class Migration(migrations.Migration):
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(forwards,backwards)
|
||||
migrations.RunPython(forwards, backwards)
|
||||
]
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('certificates', '0007_certificateinvalidation'),
|
||||
('badges', '0002_data__migrate_assertions'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name='badgeassertion',
|
||||
unique_together=set([]),
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='badgeassertion',
|
||||
name='user',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='BadgeImageConfiguration',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='BadgeAssertion',
|
||||
),
|
||||
]
|
||||
@@ -47,27 +47,28 @@ Eligibility:
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
|
||||
import os
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models, transaction
|
||||
from django.db.models import Count
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django_extensions.db.fields import CreationDateTimeField
|
||||
from django_extensions.db.fields.json import JSONField
|
||||
from model_utils import Choices
|
||||
from model_utils.models import TimeStampedModel
|
||||
from openedx.core.djangoapps.signals.signals import COURSE_CERT_AWARDED
|
||||
|
||||
|
||||
from badges.events.course_complete import course_badge_check
|
||||
from badges.events.course_meta import completion_check, course_group_check
|
||||
from config_models.models import ConfigurationModel
|
||||
from instructor_task.models import InstructorTask
|
||||
from util.milestones_helpers import fulfill_course_milestone, is_prerequisite_courses_enabled
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule_django.models import CourseKeyField, NoneToEmptyManager
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
@@ -96,13 +97,15 @@ class CertificateStatuses(object):
|
||||
error: "error states"
|
||||
}
|
||||
|
||||
PASSED_STATUSES = (downloadable, generating, regenerating)
|
||||
|
||||
@classmethod
|
||||
def is_passing_status(cls, status):
|
||||
"""
|
||||
Given the status of a certificate, return a boolean indicating whether
|
||||
the student passed the course.
|
||||
"""
|
||||
return status in [cls.downloadable, cls.generating]
|
||||
return status in cls.PASSED_STATUSES
|
||||
|
||||
|
||||
class CertificateSocialNetworks(object):
|
||||
@@ -893,93 +896,6 @@ class CertificateHtmlViewConfiguration(ConfigurationModel):
|
||||
return json_data
|
||||
|
||||
|
||||
class BadgeAssertion(models.Model):
|
||||
"""
|
||||
Tracks badges on our side of the badge baking transaction
|
||||
"""
|
||||
user = models.ForeignKey(User)
|
||||
course_id = CourseKeyField(max_length=255, blank=True, default=None)
|
||||
# Mode a badge was awarded for.
|
||||
mode = models.CharField(max_length=100)
|
||||
data = JSONField()
|
||||
|
||||
@property
|
||||
def image_url(self):
|
||||
"""
|
||||
Get the image for this assertion.
|
||||
"""
|
||||
|
||||
return self.data['image']
|
||||
|
||||
@property
|
||||
def assertion_url(self):
|
||||
"""
|
||||
Get the public URL for the assertion.
|
||||
"""
|
||||
return self.data['json']['id']
|
||||
|
||||
class Meta(object):
|
||||
unique_together = (('course_id', 'user', 'mode'),)
|
||||
app_label = "certificates"
|
||||
|
||||
|
||||
def validate_badge_image(image):
|
||||
"""
|
||||
Validates that a particular image is small enough, of the right type, and square to be a badge.
|
||||
"""
|
||||
if image.width != image.height:
|
||||
raise ValidationError(_(u"The badge image must be square."))
|
||||
if not image.size < (250 * 1024):
|
||||
raise ValidationError(_(u"The badge image file size must be less than 250KB."))
|
||||
|
||||
|
||||
class BadgeImageConfiguration(models.Model):
|
||||
"""
|
||||
Contains the configuration for badges for a specific mode. The mode
|
||||
"""
|
||||
class Meta(object):
|
||||
app_label = "certificates"
|
||||
|
||||
mode = models.CharField(
|
||||
max_length=125,
|
||||
help_text=_(u'The course mode for this badge image. For example, "verified" or "honor".'),
|
||||
unique=True,
|
||||
)
|
||||
icon = models.ImageField(
|
||||
# Actual max is 256KB, but need overhead for badge baking. This should be more than enough.
|
||||
help_text=_(
|
||||
u"Badge images must be square PNG files. The file size should be under 250KB."
|
||||
),
|
||||
upload_to='badges',
|
||||
validators=[validate_badge_image]
|
||||
)
|
||||
default = models.BooleanField(
|
||||
default=False,
|
||||
help_text=_(
|
||||
u"Set this value to True if you want this image to be the default image for any course modes "
|
||||
u"that do not have a specified badge image. You can have only one default image."
|
||||
)
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
Make sure there's not more than one default.
|
||||
"""
|
||||
if self.default and BadgeImageConfiguration.objects.filter(default=True).exclude(id=self.id):
|
||||
raise ValidationError(_(u"There can be only one default image."))
|
||||
|
||||
@classmethod
|
||||
def image_for_mode(cls, mode):
|
||||
"""
|
||||
Get the image for a particular mode.
|
||||
"""
|
||||
try:
|
||||
return cls.objects.get(mode=mode).icon
|
||||
except cls.DoesNotExist:
|
||||
# Fall back to default, if there is one.
|
||||
return cls.objects.get(default=True).icon
|
||||
|
||||
|
||||
class CertificateTemplate(TimeStampedModel):
|
||||
"""A set of custom web certificate templates.
|
||||
|
||||
@@ -1096,24 +1012,25 @@ class CertificateTemplateAsset(TimeStampedModel):
|
||||
|
||||
|
||||
@receiver(COURSE_CERT_AWARDED, sender=GeneratedCertificate)
|
||||
#pylint: disable=unused-argument
|
||||
def create_badge(sender, user, course_key, status, **kwargs):
|
||||
# pylint: disable=unused-argument
|
||||
def create_course_badge(sender, user, course_key, status, **kwargs):
|
||||
"""
|
||||
Standard signal hook to create badges when a certificate has been generated.
|
||||
Standard signal hook to create course badges when a certificate has been generated.
|
||||
"""
|
||||
if not settings.FEATURES.get('ENABLE_OPENBADGES', False):
|
||||
return
|
||||
if not modulestore().get_course(course_key).issue_badges:
|
||||
LOGGER.info("Course is not configured to issue badges.")
|
||||
return
|
||||
if BadgeAssertion.objects.filter(user=user, course_id=course_key):
|
||||
LOGGER.info("Badge already exists for this user on this course.")
|
||||
# Badge already exists. Skip.
|
||||
return
|
||||
# Don't bake a badge until the certificate is available. Prevents user-facing requests from being paused for this
|
||||
# by making sure it only gets run on the callback during normal workflow.
|
||||
if not status == CertificateStatuses.downloadable:
|
||||
return
|
||||
from .badge_handler import BadgeHandler
|
||||
handler = BadgeHandler(course_key)
|
||||
handler.award(user)
|
||||
course_badge_check(user, course_key)
|
||||
|
||||
|
||||
@receiver(COURSE_CERT_AWARDED, sender=GeneratedCertificate)
|
||||
def create_completion_badge(sender, user, course_key, status, **kwargs): # pylint: disable=unused-argument
|
||||
"""
|
||||
Standard signal hook to create 'x courses completed' badges when a certificate has been generated.
|
||||
"""
|
||||
completion_check(user)
|
||||
|
||||
|
||||
@receiver(COURSE_CERT_AWARDED, sender=GeneratedCertificate)
|
||||
def create_course_group_badge(sender, user, course_key, status, **kwargs): # pylint: disable=unused-argument
|
||||
"""
|
||||
Standard signal hook to create badges when a user has completed a prespecified set of courses.
|
||||
"""
|
||||
course_group_check(user, course_key)
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
# Factories are self documenting
|
||||
# pylint: disable=missing-docstring
|
||||
import factory
|
||||
from uuid import uuid4
|
||||
from django.core.files.base import ContentFile
|
||||
from factory.django import DjangoModelFactory, ImageField
|
||||
|
||||
from student.models import LinkedInAddToProfileConfiguration
|
||||
from factory.django import DjangoModelFactory
|
||||
|
||||
from certificates.models import (
|
||||
GeneratedCertificate, CertificateStatuses, CertificateHtmlViewConfiguration, CertificateWhitelist, BadgeAssertion,
|
||||
BadgeImageConfiguration, CertificateInvalidation,
|
||||
GeneratedCertificate, CertificateStatuses, CertificateHtmlViewConfiguration, CertificateWhitelist,
|
||||
CertificateInvalidation,
|
||||
)
|
||||
from student.models import LinkedInAddToProfileConfiguration
|
||||
|
||||
|
||||
class GeneratedCertificateFactory(DjangoModelFactory):
|
||||
@@ -44,33 +42,6 @@ class CertificateInvalidationFactory(DjangoModelFactory):
|
||||
active = True
|
||||
|
||||
|
||||
class BadgeAssertionFactory(DjangoModelFactory):
|
||||
class Meta(object):
|
||||
model = BadgeAssertion
|
||||
|
||||
mode = 'honor'
|
||||
data = {
|
||||
'image': 'http://www.example.com/image.png',
|
||||
'json': {'id': 'http://www.example.com/assertion.json'},
|
||||
'issuer': 'http://www.example.com/issuer.json',
|
||||
}
|
||||
|
||||
|
||||
class BadgeImageConfigurationFactory(DjangoModelFactory):
|
||||
|
||||
class Meta(object):
|
||||
model = BadgeImageConfiguration
|
||||
|
||||
mode = 'honor'
|
||||
icon = factory.LazyAttribute(
|
||||
lambda _: ContentFile(
|
||||
ImageField()._make_data( # pylint: disable=protected-access
|
||||
{'color': 'blue', 'width': 50, 'height': 50, 'format': 'PNG'}
|
||||
), 'test.png'
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class CertificateHtmlViewConfigurationFactory(DjangoModelFactory):
|
||||
|
||||
class Meta(object):
|
||||
|
||||
@@ -8,12 +8,15 @@ from mock import patch
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
from certificates.tests.factories import BadgeAssertionFactory
|
||||
|
||||
from badges.events.course_complete import get_completion_badge
|
||||
from badges.models import BadgeAssertion
|
||||
from badges.tests.factories import BadgeAssertionFactory, CourseCompleteImageConfigurationFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls
|
||||
from student.tests.factories import UserFactory, CourseEnrollmentFactory
|
||||
from certificates.management.commands import resubmit_error_certificates, regenerate_user, ungenerated_certs
|
||||
from certificates.models import GeneratedCertificate, CertificateStatuses, BadgeAssertion
|
||||
from certificates.models import GeneratedCertificate, CertificateStatuses
|
||||
|
||||
|
||||
class CertificateManagementTest(ModuleStoreTestCase):
|
||||
@@ -30,6 +33,7 @@ class CertificateManagementTest(ModuleStoreTestCase):
|
||||
CourseFactory.create()
|
||||
for __ in range(3)
|
||||
]
|
||||
CourseCompleteImageConfigurationFactory.create()
|
||||
|
||||
def _create_cert(self, course_key, user, status, mode=CourseMode.HONOR):
|
||||
"""Create a certificate entry. """
|
||||
@@ -146,6 +150,7 @@ class ResubmitErrorCertificatesTest(CertificateManagementTest):
|
||||
self._assert_cert_status(phantom_course, self.user, CertificateStatuses.error)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@attr('shard_1')
|
||||
class RegenerateCertificatesTest(CertificateManagementTest):
|
||||
"""
|
||||
@@ -160,19 +165,23 @@ class RegenerateCertificatesTest(CertificateManagementTest):
|
||||
super(RegenerateCertificatesTest, self).setUp()
|
||||
self.course = self.courses[0]
|
||||
|
||||
@ddt.data(True, False)
|
||||
@override_settings(CERT_QUEUE='test-queue')
|
||||
@patch('certificates.api.XQueueCertInterface', spec=True)
|
||||
def test_clear_badge(self, xqueue):
|
||||
def test_clear_badge(self, issue_badges, xqueue):
|
||||
"""
|
||||
Given that I have a user with a badge
|
||||
If I run regeneration for a user
|
||||
Then certificate generation will be requested
|
||||
And the badge will be deleted
|
||||
And the badge will be deleted if badge issuing is enabled
|
||||
"""
|
||||
key = self.course.location.course_key
|
||||
BadgeAssertionFactory(user=self.user, course_id=key, data={})
|
||||
self._create_cert(key, self.user, CertificateStatuses.downloadable)
|
||||
self.assertTrue(BadgeAssertion.objects.filter(user=self.user, course_id=key))
|
||||
badge_class = get_completion_badge(key, self.user)
|
||||
BadgeAssertionFactory(badge_class=badge_class, user=self.user)
|
||||
self.assertTrue(BadgeAssertion.objects.filter(user=self.user, badge_class=badge_class))
|
||||
self.course.issue_badges = issue_badges
|
||||
self.store.update_item(self.course, None)
|
||||
self._run_command(
|
||||
username=self.user.email, course=unicode(key), noop=False, insecure=False, template_file=None,
|
||||
grade_value=None
|
||||
@@ -185,7 +194,9 @@ class RegenerateCertificatesTest(CertificateManagementTest):
|
||||
template_file=None,
|
||||
generate_pdf=True
|
||||
)
|
||||
self.assertFalse(BadgeAssertion.objects.filter(user=self.user, course_id=key))
|
||||
self.assertEquals(
|
||||
bool(BadgeAssertion.objects.filter(user=self.user, badge_class=badge_class)), not issue_badges
|
||||
)
|
||||
|
||||
@override_settings(CERT_QUEUE='test-queue')
|
||||
@patch('capa.xqueue_interface.XQueueInterface.send_to_queue', spec=True)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Tests for certificate Django models. """
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.images import ImageFile
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
@@ -13,7 +12,6 @@ from certificates.models import (
|
||||
ExampleCertificateSet,
|
||||
CertificateHtmlViewConfiguration,
|
||||
CertificateTemplateAsset,
|
||||
BadgeImageConfiguration,
|
||||
EligibleCertificateManager,
|
||||
GeneratedCertificate,
|
||||
CertificateStatuses,
|
||||
@@ -167,55 +165,6 @@ class CertificateHtmlViewConfigurationTest(TestCase):
|
||||
self.assertEquals(self.config.get_config(), {})
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
class BadgeImageConfigurationTest(TestCase):
|
||||
"""
|
||||
Test the validation features of BadgeImageConfiguration.
|
||||
"""
|
||||
def get_image(self, name):
|
||||
"""
|
||||
Get one of the test images from the test data directory.
|
||||
"""
|
||||
return ImageFile(open(TEST_DATA_ROOT / 'badges' / name + '.png'))
|
||||
|
||||
def create_clean(self, file_obj):
|
||||
"""
|
||||
Shortcut to create a BadgeImageConfiguration with a specific file.
|
||||
"""
|
||||
BadgeImageConfiguration(mode='honor', icon=file_obj).full_clean()
|
||||
|
||||
def test_good_image(self):
|
||||
"""
|
||||
Verify that saving a valid badge image is no problem.
|
||||
"""
|
||||
good = self.get_image('good')
|
||||
BadgeImageConfiguration(mode='honor', icon=good).full_clean()
|
||||
|
||||
def test_unbalanced_image(self):
|
||||
"""
|
||||
Verify that setting an image with an uneven width and height raises an error.
|
||||
"""
|
||||
unbalanced = ImageFile(self.get_image('unbalanced'))
|
||||
self.assertRaises(ValidationError, self.create_clean, unbalanced)
|
||||
|
||||
def test_large_image(self):
|
||||
"""
|
||||
Verify that setting an image that is too big raises an error.
|
||||
"""
|
||||
large = self.get_image('large')
|
||||
self.assertRaises(ValidationError, self.create_clean, large)
|
||||
|
||||
def test_no_double_default(self):
|
||||
"""
|
||||
Verify that creating two configurations as default is not permitted.
|
||||
"""
|
||||
BadgeImageConfiguration(mode='test', icon=self.get_image('good'), default=True).save()
|
||||
self.assertRaises(
|
||||
ValidationError,
|
||||
BadgeImageConfiguration(mode='test2', icon=self.get_image('good'), default=True).full_clean
|
||||
)
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
class CertificateTemplateAssetTest(TestCase):
|
||||
"""
|
||||
|
||||
@@ -1,25 +1,17 @@
|
||||
"""Tests for certificates views. """
|
||||
|
||||
import json
|
||||
import ddt
|
||||
from uuid import uuid4
|
||||
from nose.plugins.attrib import attr
|
||||
from mock import patch
|
||||
|
||||
import ddt
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
from django.test.client import Client
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from nose.plugins.attrib import attr
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
from openedx.core.lib.tests.assertions.events import assert_event_matches
|
||||
from student.tests.factories import UserFactory
|
||||
from track.tests import EventTrackingTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from util.testing import UrlResetMixin
|
||||
|
||||
from certificates.api import get_certificate_url
|
||||
from certificates.models import (
|
||||
@@ -28,10 +20,9 @@ from certificates.models import (
|
||||
GeneratedCertificate,
|
||||
CertificateHtmlViewConfiguration,
|
||||
)
|
||||
|
||||
from certificates.tests.factories import (
|
||||
BadgeAssertionFactory,
|
||||
)
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
FEATURES_WITH_CERTS_ENABLED = settings.FEATURES.copy()
|
||||
FEATURES_WITH_CERTS_ENABLED['CERTIFICATES_HTML_VIEW'] = True
|
||||
@@ -342,51 +333,3 @@ class MicrositeCertificatesViewsTests(ModuleStoreTestCase):
|
||||
self.assertNotIn('platform_microsite', response.content)
|
||||
self.assertNotIn('http://www.microsite.org', response.content)
|
||||
self.assertNotIn('This should not survive being overwritten by static content', response.content)
|
||||
|
||||
|
||||
class TrackShareRedirectTest(UrlResetMixin, ModuleStoreTestCase, EventTrackingTestCase):
|
||||
"""
|
||||
Verifies the badge image share event is sent out.
|
||||
"""
|
||||
|
||||
@patch.dict(settings.FEATURES, {"ENABLE_OPENBADGES": True})
|
||||
def setUp(self):
|
||||
super(TrackShareRedirectTest, self).setUp('certificates.urls')
|
||||
self.client = Client()
|
||||
self.course = CourseFactory.create(
|
||||
org='testorg', number='run1', display_name='trackable course'
|
||||
)
|
||||
self.assertion = BadgeAssertionFactory(
|
||||
user=self.user, course_id=self.course.id, data={
|
||||
'image': 'http://www.example.com/image.png',
|
||||
'json': {'id': 'http://www.example.com/assertion.json'},
|
||||
'issuer': 'http://www.example.com/issuer.json'
|
||||
},
|
||||
)
|
||||
|
||||
def test_social_event_sent(self):
|
||||
test_url = '/certificates/badge_share_tracker/{}/social_network/{}/'.format(
|
||||
unicode(self.course.id),
|
||||
self.user.username,
|
||||
)
|
||||
self.recreate_tracker()
|
||||
response = self.client.get(test_url)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response['Location'], 'http://www.example.com/image.png')
|
||||
assert_event_matches(
|
||||
{
|
||||
'name': 'edx.badge.assertion.shared',
|
||||
'data': {
|
||||
'course_id': 'testorg/run1/trackable_course',
|
||||
'social_network': 'social_network',
|
||||
# pylint: disable=no-member
|
||||
'assertion_id': self.assertion.id,
|
||||
'assertion_json_url': 'http://www.example.com/assertion.json',
|
||||
'assertion_image_url': 'http://www.example.com/image.png',
|
||||
'user_id': self.user.id,
|
||||
'issuer': 'http://www.example.com/issuer.json',
|
||||
'enrollment_mode': 'honor'
|
||||
},
|
||||
},
|
||||
self.get_event()
|
||||
)
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
import json
|
||||
import ddt
|
||||
import mock
|
||||
from uuid import uuid4
|
||||
from nose.plugins.attrib import attr
|
||||
from mock import patch
|
||||
@@ -16,6 +15,8 @@ from django.test.client import Client
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from badges.events.course_complete import get_completion_badge
|
||||
from badges.tests.factories import BadgeAssertionFactory, CourseCompleteImageConfigurationFactory, BadgeClassFactory
|
||||
from openedx.core.lib.tests.assertions.events import assert_event_matches
|
||||
from student.tests.factories import UserFactory, CourseEnrollmentFactory
|
||||
from student.roles import CourseStaffRole
|
||||
@@ -36,15 +37,15 @@ from certificates.models import (
|
||||
from certificates.tests.factories import (
|
||||
CertificateHtmlViewConfigurationFactory,
|
||||
LinkedInAddToProfileConfigurationFactory,
|
||||
BadgeAssertionFactory,
|
||||
GeneratedCertificateFactory,
|
||||
)
|
||||
from util import organizations_helpers as organizations_api
|
||||
from django.test.client import RequestFactory
|
||||
import urllib
|
||||
|
||||
FEATURES_WITH_CERTS_ENABLED = settings.FEATURES.copy()
|
||||
FEATURES_WITH_CERTS_ENABLED['CERTIFICATES_HTML_VIEW'] = True
|
||||
FEATURES_WITH_BADGES_ENABLED = FEATURES_WITH_CERTS_ENABLED.copy()
|
||||
FEATURES_WITH_BADGES_ENABLED['ENABLE_OPENBADGES'] = True
|
||||
|
||||
FEATURES_WITH_CERTS_DISABLED = settings.FEATURES.copy()
|
||||
FEATURES_WITH_CERTS_DISABLED['CERTIFICATES_HTML_VIEW'] = False
|
||||
@@ -105,6 +106,7 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
|
||||
)
|
||||
CertificateHtmlViewConfigurationFactory.create()
|
||||
LinkedInAddToProfileConfigurationFactory.create()
|
||||
CourseCompleteImageConfigurationFactory.create()
|
||||
|
||||
def _add_course_certificates(self, count=1, signatory_count=0, is_active=True):
|
||||
"""
|
||||
@@ -333,7 +335,27 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
|
||||
)
|
||||
self.assertIn('logo_test1.png', response.content)
|
||||
|
||||
@override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED)
|
||||
@ddt.data(True, False)
|
||||
@patch('certificates.views.webview.get_completion_badge')
|
||||
@override_settings(FEATURES=FEATURES_WITH_BADGES_ENABLED)
|
||||
def test_fetch_badge_info(self, issue_badges, mock_get_completion_badge):
|
||||
"""
|
||||
Test: Fetch badge class info if badges are enabled.
|
||||
"""
|
||||
badge_class = BadgeClassFactory(course_id=self.course_id, mode=self.cert.mode)
|
||||
mock_get_completion_badge.return_value = badge_class
|
||||
|
||||
self._add_course_certificates(count=1, signatory_count=1, is_active=True)
|
||||
test_url = get_certificate_url(course_id=self.cert.course_id, uuid=self.cert.verify_uuid)
|
||||
response = self.client.get(test_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
if issue_badges:
|
||||
mock_get_completion_badge.assertCalled()
|
||||
else:
|
||||
mock_get_completion_badge.assertNotCalled()
|
||||
|
||||
@override_settings(FEATURES=FEATURES_WITH_BADGES_ENABLED)
|
||||
@patch.dict("django.conf.settings.SOCIAL_SHARING_SETTINGS", {
|
||||
"CERTIFICATE_TWITTER": True,
|
||||
"CERTIFICATE_FACEBOOK": True,
|
||||
@@ -370,8 +392,9 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
|
||||
test_org = organizations_api.add_organization(organization_data=test_organization_data)
|
||||
organizations_api.add_organization_course(organization_data=test_org, course_id=unicode(self.course.id))
|
||||
self._add_course_certificates(count=1, signatory_count=1, is_active=True)
|
||||
badge_class = get_completion_badge(course_id=self.course_id, user=self.user)
|
||||
BadgeAssertionFactory.create(
|
||||
user=self.user, course_id=self.course_id,
|
||||
user=self.user, badge_class=badge_class,
|
||||
)
|
||||
self.course.cert_html_view_overrides = {
|
||||
"logo_src": "/static/certificates/images/course_override_logo.png"
|
||||
@@ -812,8 +835,15 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
|
||||
)
|
||||
test_url = '{}?evidence_visit=1'.format(cert_url)
|
||||
self.recreate_tracker()
|
||||
badge_class = get_completion_badge(self.course_id, self.user)
|
||||
assertion = BadgeAssertionFactory.create(
|
||||
user=self.user, course_id=self.course_id,
|
||||
user=self.user, badge_class=badge_class,
|
||||
backend='DummyBackend',
|
||||
image_url='http://www.example.com/image.png',
|
||||
assertion_url='http://www.example.com/assertion.json',
|
||||
data={
|
||||
'issuer': 'http://www.example.com/issuer.json',
|
||||
}
|
||||
)
|
||||
response = self.client.get(test_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@@ -823,6 +853,10 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
|
||||
'data': {
|
||||
'course_id': 'testorg/run1/refundable_course',
|
||||
'assertion_id': assertion.id,
|
||||
'badge_generator': u'DummyBackend',
|
||||
'badge_name': u'refundable course',
|
||||
'issuing_component': u'',
|
||||
'badge_slug': u'testorgrun1refundable_course_honor_432f164',
|
||||
'assertion_json_url': 'http://www.example.com/assertion.json',
|
||||
'assertion_image_url': 'http://www.example.com/image.png',
|
||||
'user_id': self.user.id,
|
||||
|
||||
@@ -6,10 +6,11 @@ from mock import patch
|
||||
from django.conf import settings
|
||||
from nose.plugins.attrib import attr
|
||||
|
||||
from badges.tests.factories import CourseCompleteImageConfigurationFactory
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
|
||||
from student.tests.factories import UserFactory
|
||||
from student.tests.factories import UserFactory, CourseEnrollmentFactory
|
||||
from certificates.models import (
|
||||
CertificateStatuses,
|
||||
GeneratedCertificate,
|
||||
@@ -113,18 +114,18 @@ class CertificatesModelTest(ModuleStoreTestCase, MilestonesTestCaseMixin):
|
||||
self.assertEqual(completed_milestones[0]['namespace'], unicode(pre_requisite_course.id))
|
||||
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_OPENBADGES': True})
|
||||
@patch('certificates.badge_handler.BadgeHandler', spec=True)
|
||||
@patch('badges.backends.badgr.BadgrBackend', spec=True)
|
||||
def test_badge_callback(self, handler):
|
||||
student = UserFactory()
|
||||
course = CourseFactory.create(org='edx', number='998', display_name='Test Course', issue_badges=True)
|
||||
CourseCompleteImageConfigurationFactory()
|
||||
CourseEnrollmentFactory(user=student, course_id=course.location.course_key, mode='honor')
|
||||
cert = GeneratedCertificateFactory.create(
|
||||
user=student,
|
||||
course_id=course.id,
|
||||
status=CertificateStatuses.generating,
|
||||
mode='verified'
|
||||
)
|
||||
# Check return value since class instance will be stored there.
|
||||
self.assertFalse(handler.return_value.award.called)
|
||||
cert.status = CertificateStatuses.downloadable
|
||||
cert.save()
|
||||
self.assertTrue(handler.return_value.award.called)
|
||||
|
||||
@@ -31,15 +31,3 @@ urlpatterns = patterns(
|
||||
url(r'regenerate', views.regenerate_certificate_for_user, name="regenerate_certificate_for_user"),
|
||||
url(r'generate', views.generate_certificate_for_user, name="generate_certificate_for_user"),
|
||||
)
|
||||
|
||||
|
||||
if settings.FEATURES.get("ENABLE_OPENBADGES", False):
|
||||
urlpatterns += (
|
||||
url(
|
||||
r'^badge_share_tracker/{}/(?P<network>[^/]+)/(?P<student_username>[^/]+)/$'.format(
|
||||
settings.COURSE_ID_PATTERN
|
||||
),
|
||||
views.track_share_redirect,
|
||||
name='badge_share_tracker'
|
||||
),
|
||||
)
|
||||
|
||||
@@ -5,4 +5,3 @@ Aggregate all views exposed by the certificates app.
|
||||
from .xqueue import *
|
||||
from .support import *
|
||||
from .webview import *
|
||||
from .badges import *
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
"""
|
||||
Certificate views for open badges.
|
||||
"""
|
||||
from django.shortcuts import redirect, get_object_or_404
|
||||
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
from util.views import ensure_valid_course_key
|
||||
from eventtracking import tracker
|
||||
from certificates.models import BadgeAssertion
|
||||
|
||||
|
||||
@ensure_valid_course_key
|
||||
def track_share_redirect(request__unused, course_id, network, student_username):
|
||||
"""
|
||||
Tracks when a user downloads a badge for sharing.
|
||||
"""
|
||||
course_id = CourseLocator.from_string(course_id)
|
||||
assertion = get_object_or_404(BadgeAssertion, user__username=student_username, course_id=course_id)
|
||||
tracker.emit(
|
||||
'edx.badge.assertion.shared', {
|
||||
'course_id': unicode(course_id),
|
||||
'social_network': network,
|
||||
'assertion_id': assertion.id,
|
||||
'assertion_json_url': assertion.data['json']['id'],
|
||||
'assertion_image_url': assertion.image_url,
|
||||
'user_id': assertion.user.id,
|
||||
'enrollment_mode': assertion.mode,
|
||||
'issuer': assertion.data['issuer'],
|
||||
}
|
||||
)
|
||||
return redirect(assertion.image_url)
|
||||
@@ -13,8 +13,9 @@ from django.http import HttpResponse, Http404
|
||||
from django.template import RequestContext
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.encoding import smart_str
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from badges.events.course_complete import get_completion_badge
|
||||
from badges.utils import badges_enabled
|
||||
from courseware.access import has_access
|
||||
from edxmako.shortcuts import render_to_response
|
||||
from edxmako.template import Template
|
||||
@@ -42,9 +43,8 @@ from certificates.models import (
|
||||
GeneratedCertificate,
|
||||
CertificateStatuses,
|
||||
CertificateHtmlViewConfiguration,
|
||||
CertificateSocialNetworks,
|
||||
BadgeAssertion
|
||||
)
|
||||
CertificateSocialNetworks)
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -355,21 +355,41 @@ def _track_certificate_events(request, context, course, user, user_certificate):
|
||||
"""
|
||||
Tracks web certificate view related events.
|
||||
"""
|
||||
badge = context['badge']
|
||||
# Badge Request Event Tracking Logic
|
||||
if 'evidence_visit' in request.GET and badge:
|
||||
tracker.emit(
|
||||
'edx.badge.assertion.evidence_visited',
|
||||
{
|
||||
'user_id': user.id,
|
||||
'course_id': unicode(course.id),
|
||||
'enrollment_mode': badge.mode,
|
||||
'assertion_id': badge.id,
|
||||
'assertion_image_url': badge.data['image'],
|
||||
'assertion_json_url': badge.data['json']['id'],
|
||||
'issuer': badge.data['issuer'],
|
||||
}
|
||||
)
|
||||
course_key = course.location.course_key
|
||||
|
||||
if 'evidence_visit' in request.GET:
|
||||
badge_class = get_completion_badge(course_key, user)
|
||||
if not badge_class:
|
||||
log.warning('Visit to evidence URL for badge, but badges not configured for course "%s"', course_key)
|
||||
badges = []
|
||||
else:
|
||||
badges = badge_class.get_for_user(user)
|
||||
if badges:
|
||||
# There should only ever be one of these.
|
||||
badge = badges[0]
|
||||
tracker.emit(
|
||||
'edx.badge.assertion.evidence_visited',
|
||||
{
|
||||
'badge_name': badge.badge_class.display_name,
|
||||
'badge_slug': badge.badge_class.slug,
|
||||
'badge_generator': badge.backend,
|
||||
'issuing_component': badge.badge_class.issuing_component,
|
||||
'user_id': user.id,
|
||||
'course_id': unicode(course_key),
|
||||
'enrollment_mode': badge.badge_class.mode,
|
||||
'assertion_id': badge.id,
|
||||
'assertion_image_url': badge.image_url,
|
||||
'assertion_json_url': badge.assertion_url,
|
||||
'issuer': badge.data.get('issuer'),
|
||||
}
|
||||
)
|
||||
else:
|
||||
log.warn(
|
||||
"Could not find badge for %s on course %s.",
|
||||
user.id,
|
||||
course_key,
|
||||
)
|
||||
|
||||
# track certificate evidence_visited event for analytics when certificate_user and accessing_user are different
|
||||
if request.user and request.user.id != user.id:
|
||||
@@ -425,10 +445,11 @@ def _update_badge_context(context, course, user):
|
||||
"""
|
||||
Updates context with badge info.
|
||||
"""
|
||||
try:
|
||||
badge = BadgeAssertion.objects.get(user=user, course_id=course.location.course_key)
|
||||
except BadgeAssertion.DoesNotExist:
|
||||
badge = None
|
||||
badge = None
|
||||
if badges_enabled() and course.issue_badges:
|
||||
badges = get_completion_badge(course.location.course_key, user).get_for_user(user)
|
||||
if badges:
|
||||
badge = badges[0]
|
||||
context['badge'] = badge
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import re
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.conf import settings
|
||||
|
||||
from badges.service import BadgingService
|
||||
from badges.utils import badges_enabled
|
||||
from openedx.core.djangoapps.user_api.course_tag import api as user_course_tag_api
|
||||
from request_cache.middleware import RequestCache
|
||||
import xblock.reference.plugins
|
||||
@@ -211,8 +213,11 @@ class LmsModuleSystem(ModuleSystem): # pylint: disable=abstract-method
|
||||
track_function=kwargs.get('track_function', None),
|
||||
cache=request_cache_dict
|
||||
)
|
||||
store = modulestore()
|
||||
services['settings'] = SettingsService()
|
||||
services['user_tags'] = UserTagsService(self)
|
||||
if badges_enabled():
|
||||
services['badging'] = BadgingService(course_id=kwargs.get('course_id'), modulestore=store)
|
||||
self.request_token = kwargs.pop('request_token', None)
|
||||
super(LmsModuleSystem, self).__init__(**kwargs)
|
||||
|
||||
|
||||
@@ -5,17 +5,25 @@ Tests of the LMS XBlock Runtime and associated utilities
|
||||
from django.contrib.auth.models import User
|
||||
from django.conf import settings
|
||||
from ddt import ddt, data
|
||||
from mock import Mock
|
||||
from unittest import TestCase
|
||||
from django.test import TestCase
|
||||
from mock import Mock, patch
|
||||
from urlparse import urlparse
|
||||
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys.edx.locations import BlockUsageLocator, CourseLocator, SlashSeparatedCourseKey
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
|
||||
from badges.tests.factories import BadgeClassFactory
|
||||
from badges.tests.test_models import get_image
|
||||
from lms.djangoapps.lms_xblock.runtime import quote_slashes, unquote_slashes, LmsModuleSystem
|
||||
from xblock.fields import ScopeIds
|
||||
from xmodule.modulestore.django import ModuleI18nService
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xblock.exceptions import NoSuchServiceError
|
||||
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
TEST_STRINGS = [
|
||||
'',
|
||||
'foobar',
|
||||
@@ -142,9 +150,7 @@ class TestUserServiceAPI(TestCase):
|
||||
def setUp(self):
|
||||
super(TestUserServiceAPI, self).setUp()
|
||||
self.course_id = SlashSeparatedCourseKey("org", "course", "run")
|
||||
|
||||
self.user = User(username='runtime_robot', email='runtime_robot@edx.org', password='test', first_name='Robot')
|
||||
self.user.save()
|
||||
self.user = UserFactory.create()
|
||||
|
||||
def mock_get_real_user(_anon_id):
|
||||
"""Just returns the test user"""
|
||||
@@ -187,6 +193,72 @@ class TestUserServiceAPI(TestCase):
|
||||
self.runtime.service(self.mock_block, 'user_tags').get_tag('fake_scope', self.key)
|
||||
|
||||
|
||||
@ddt
|
||||
class TestBadgingService(ModuleStoreTestCase):
|
||||
"""Test the badging service interface"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestBadgingService, self).setUp()
|
||||
self.course_id = CourseKey.from_string('course-v1:org+course+run')
|
||||
|
||||
self.mock_block = Mock()
|
||||
self.mock_block.service_declaration.return_value = 'needs'
|
||||
|
||||
def create_runtime(self):
|
||||
"""
|
||||
Create the testing runtime.
|
||||
"""
|
||||
def mock_get_real_user(_anon_id):
|
||||
"""Just returns the test user"""
|
||||
return self.user
|
||||
|
||||
return LmsModuleSystem(
|
||||
static_url='/static',
|
||||
track_function=Mock(),
|
||||
get_module=Mock(),
|
||||
render_template=Mock(),
|
||||
replace_urls=str,
|
||||
course_id=self.course_id,
|
||||
get_real_user=mock_get_real_user,
|
||||
descriptor_runtime=Mock(),
|
||||
)
|
||||
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_OPENBADGES': True})
|
||||
def test_service_rendered(self):
|
||||
runtime = self.create_runtime()
|
||||
self.assertTrue(runtime.service(self.mock_block, 'badging'))
|
||||
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_OPENBADGES': False})
|
||||
def test_no_service_rendered(self):
|
||||
runtime = self.create_runtime()
|
||||
self.assertFalse(runtime.service(self.mock_block, 'badging'))
|
||||
|
||||
@data(True, False)
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_OPENBADGES': True})
|
||||
def test_course_badges_toggle(self, toggle):
|
||||
self.course_id = CourseFactory.create(metadata={'issue_badges': toggle}).location.course_key
|
||||
runtime = self.create_runtime()
|
||||
self.assertIs(runtime.service(self.mock_block, 'badging').course_badges_enabled, toggle)
|
||||
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_OPENBADGES': True})
|
||||
def test_get_badge_class(self):
|
||||
runtime = self.create_runtime()
|
||||
badge_service = runtime.service(self.mock_block, 'badging')
|
||||
premade_badge_class = BadgeClassFactory.create()
|
||||
# Ignore additional parameters. This class already exists.
|
||||
# We should get back the first class we created, rather than a new one.
|
||||
badge_class = badge_service.get_badge_class(
|
||||
slug='test_slug', issuing_component='test_component', description='Attempted override',
|
||||
criteria='test', display_name='Testola', image_file_handle=get_image('good')
|
||||
)
|
||||
# These defaults are set on the factory.
|
||||
self.assertEqual(badge_class.criteria, 'https://example.com/syllabus')
|
||||
self.assertEqual(badge_class.display_name, 'Test Badge')
|
||||
self.assertEqual(badge_class.description, "Yay! It's a test badge.")
|
||||
# File name won't always be the same.
|
||||
self.assertEqual(badge_class.image.path, premade_badge_class.image.path)
|
||||
|
||||
|
||||
class TestI18nService(ModuleStoreTestCase):
|
||||
""" Test ModuleI18nService """
|
||||
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
""" Views for a student's profile information. """
|
||||
|
||||
from django.conf import settings
|
||||
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.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import Http404
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django_countries import countries
|
||||
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||
|
||||
from edxmako.shortcuts import render_to_response
|
||||
from badges.utils import badges_enabled
|
||||
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 +87,17 @@ 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,
|
||||
'badges_logo': staticfiles_storage.url('certificates/images/backpack-logo.png'),
|
||||
'badges_icon': staticfiles_storage.url('certificates/images/ico-mozillaopenbadges.png'),
|
||||
'backpack_ui_img': staticfiles_storage.url('certificates/images/backpack-ui.png'),
|
||||
'platform_name': microsite.get_value('platform_name', settings.PLATFORM_NAME),
|
||||
},
|
||||
'disable_courseware_js': True,
|
||||
}
|
||||
|
||||
if badges_enabled():
|
||||
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
|
||||
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
this.perPage = topics.results.length;
|
||||
|
||||
this.server_api = _.extend(
|
||||
this.server_api,
|
||||
{
|
||||
course_id: function () { return encodeURIComponent(self.course_id); },
|
||||
order_by: function () { return this.sortField; }
|
||||
},
|
||||
BaseCollection.prototype.server_api
|
||||
}
|
||||
);
|
||||
delete this.server_api['sort_order']; // Sort order is not specified for the Team API
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
return function (options) {
|
||||
var teamsTab = new TeamsTabView({
|
||||
el: $('.teams-content'),
|
||||
context: options
|
||||
context: options,
|
||||
viewLabel: gettext("Teams")
|
||||
});
|
||||
teamsTab.start();
|
||||
};
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
text: gettext('All teams')
|
||||
},
|
||||
|
||||
paginationLabel: gettext('Teams Pagination'),
|
||||
|
||||
initialize: function (options) {
|
||||
this.context = options.context;
|
||||
this.itemViewClass = TeamCardView.extend({
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
'use strict';
|
||||
|
||||
define([
|
||||
'js/components/tabbed/views/tabbed_view',
|
||||
'common/js/components/views/tabbed_view',
|
||||
'teams/js/utils/team_analytics'
|
||||
], function (TabbedView, TeamAnalytics) {
|
||||
var TeamsTabbedView = TabbedView.extend({
|
||||
|
||||
@@ -77,37 +77,12 @@ def team_post_save_callback(sender, instance, **kwargs): # pylint: disable=unus
|
||||
)
|
||||
|
||||
|
||||
class TeamAPIPagination(DefaultPagination):
|
||||
"""
|
||||
Pagination format used by the teams API.
|
||||
"""
|
||||
page_size_query_param = "page_size"
|
||||
|
||||
def get_paginated_response(self, data):
|
||||
"""
|
||||
Annotate the response with pagination information.
|
||||
"""
|
||||
response = super(TeamAPIPagination, self).get_paginated_response(data)
|
||||
|
||||
# Add the current page to the response.
|
||||
# It may make sense to eventually move this field into the default
|
||||
# implementation, but for now, teams is the only API that uses this.
|
||||
response.data["current_page"] = self.page.number
|
||||
|
||||
# This field can be derived from other fields in the response,
|
||||
# so it may make sense to have the JavaScript client calculate it
|
||||
# instead of including it in the response.
|
||||
response.data["start"] = (self.page.number - 1) * self.get_page_size(self.request)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class TopicsPagination(TeamAPIPagination):
|
||||
class TopicsPagination(DefaultPagination):
|
||||
"""Paginate topics. """
|
||||
page_size = TOPICS_PER_PAGE
|
||||
|
||||
|
||||
class MyTeamsPagination(TeamAPIPagination):
|
||||
class MyTeamsPagination(DefaultPagination):
|
||||
"""Paginate the user's teams. """
|
||||
page_size = TEAM_MEMBERSHIPS_PER_PAGE
|
||||
|
||||
@@ -381,7 +356,6 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView):
|
||||
authentication_classes = (OAuth2Authentication, SessionAuthentication)
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
serializer_class = CourseTeamSerializer
|
||||
pagination_class = TeamAPIPagination
|
||||
|
||||
def get(self, request):
|
||||
"""GET /api/team/v0/teams/"""
|
||||
|
||||
@@ -290,6 +290,7 @@ MKTG_URLS = ENV_TOKENS.get('MKTG_URLS', MKTG_URLS)
|
||||
BADGR_API_TOKEN = ENV_TOKENS.get('BADGR_API_TOKEN', BADGR_API_TOKEN)
|
||||
BADGR_BASE_URL = ENV_TOKENS.get('BADGR_BASE_URL', BADGR_BASE_URL)
|
||||
BADGR_ISSUER_SLUG = ENV_TOKENS.get('BADGR_ISSUER_SLUG', BADGR_ISSUER_SLUG)
|
||||
BADGR_TIMEOUT = ENV_TOKENS.get('BADGR_TIMEOUT', BADGR_TIMEOUT)
|
||||
|
||||
# git repo loading environment
|
||||
GIT_REPO_DIR = ENV_TOKENS.get('GIT_REPO_DIR', '/edx/var/edxapp/course_repos')
|
||||
|
||||
@@ -161,6 +161,9 @@ FEATURES['ENABLE_COURSEWARE_SEARCH'] = True
|
||||
# Enable dashboard search for tests
|
||||
FEATURES['ENABLE_DASHBOARD_SEARCH'] = True
|
||||
|
||||
# Enable support for OpenBadges accomplishments
|
||||
FEATURES['ENABLE_OPENBADGES'] = True
|
||||
|
||||
# Use MockSearchEngine as the search engine for test scenario
|
||||
SEARCH_ENGINE = "search.tests.mock_search_engine.MockSearchEngine"
|
||||
# Path at which to store the mock index
|
||||
@@ -184,6 +187,8 @@ PROFILE_IMAGE_BACKEND = {
|
||||
FEATURES['ENABLE_CSMH_EXTENDED'] = True
|
||||
INSTALLED_APPS += ('coursewarehistoryextended',)
|
||||
|
||||
BADGING_BACKEND = 'lms.djangoapps.badges.backends.tests.dummy_backend.DummyBackend'
|
||||
|
||||
#####################################################################
|
||||
# Lastly, see if the developer has any local overrides.
|
||||
try:
|
||||
|
||||
@@ -2022,6 +2022,9 @@ INSTALLED_APPS = (
|
||||
|
||||
# Learner's dashboard
|
||||
'learner_dashboard',
|
||||
|
||||
# Needed whether or not enabled, due to migrations
|
||||
'badges',
|
||||
)
|
||||
|
||||
# Migrations which are not in the standard module "migrations"
|
||||
@@ -2265,12 +2268,17 @@ REGISTRATION_EMAIL_PATTERNS_ALLOWED = None
|
||||
CERT_NAME_SHORT = "Certificate"
|
||||
CERT_NAME_LONG = "Certificate of Achievement"
|
||||
|
||||
#################### Badgr OpenBadges generation #######################
|
||||
#################### OpenBadges Settings #######################
|
||||
|
||||
BADGING_BACKEND = 'badges.backends.badgr.BadgrBackend'
|
||||
|
||||
# Be sure to set up images for course modes using the BadgeImageConfiguration model in the certificates app.
|
||||
BADGR_API_TOKEN = None
|
||||
# Do not add the trailing slash here.
|
||||
BADGR_BASE_URL = "http://localhost:8005"
|
||||
BADGR_ISSUER_SLUG = "example-issuer"
|
||||
# Number of seconds to wait on the badging server when contacting it before giving up.
|
||||
BADGR_TIMEOUT = 10
|
||||
|
||||
###################### Grade Downloads ######################
|
||||
# These keys are used for all of our asynchronous downloadable files, including
|
||||
@@ -2635,6 +2643,8 @@ ACCOUNT_VISIBILITY_CONFIGURATION = {
|
||||
'language_proficiencies',
|
||||
'bio',
|
||||
'account_privacy',
|
||||
# Not an actual field, but used to signal whether badges should be public.
|
||||
'accomplishments_shared',
|
||||
],
|
||||
|
||||
# The list of account fields that are always public
|
||||
@@ -2662,6 +2672,7 @@ ACCOUNT_VISIBILITY_CONFIGURATION = {
|
||||
"mailing_address",
|
||||
"requires_parental_consent",
|
||||
"account_privacy",
|
||||
"accomplishments_shared",
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
course_id: function () { return encodeURIComponent(options.course_id); },
|
||||
fields : function () { return encodeURIComponent('display_name,path'); }
|
||||
},
|
||||
PagingCollection.prototype.server_api
|
||||
this.server_api
|
||||
);
|
||||
delete this.server_api.sort_order; // Sort order is not specified for the Bookmark API
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@ define([
|
||||
PagingCollection.prototype.initialize.call(this);
|
||||
|
||||
this.perPage = options.perPage;
|
||||
this.server_api = _.pick(PagingCollection.prototype.server_api, "page", "page_size");
|
||||
this.server_api = _.pick(this.server_api, "page", "page_size");
|
||||
if (options.text) {
|
||||
this.server_api.text = options.text;
|
||||
}
|
||||
|
||||
@@ -633,7 +633,6 @@
|
||||
define([
|
||||
// Run the LMS tests
|
||||
'lms/include/js/spec/components/header/header_spec.js',
|
||||
'lms/include/js/spec/components/tabbed/tabbed_view_spec.js',
|
||||
'lms/include/js/spec/components/card/card_spec.js',
|
||||
'lms/include/js/spec/staff_debug_actions_spec.js',
|
||||
'lms/include/js/spec/views/notification_spec.js',
|
||||
@@ -668,6 +667,11 @@
|
||||
'lms/include/js/spec/student_profile/learner_profile_factory_spec.js',
|
||||
'lms/include/js/spec/student_profile/learner_profile_view_spec.js',
|
||||
'lms/include/js/spec/student_profile/learner_profile_fields_spec.js',
|
||||
'lms/include/js/spec/student_profile/share_modal_view_spec.js',
|
||||
'lms/include/js/spec/student_profile/badge_view_spec.js',
|
||||
'lms/include/js/spec/student_profile/section_two_tab_spec.js',
|
||||
'lms/include/js/spec/student_profile/badge_list_view_spec.js',
|
||||
'lms/include/js/spec/student_profile/badge_list_container_spec.js',
|
||||
'lms/include/js/spec/verify_student/pay_and_verify_view_spec.js',
|
||||
'lms/include/js/spec/verify_student/reverify_view_spec.js',
|
||||
'lms/include/js/spec/verify_student/webcam_photo_view_spec.js',
|
||||
|
||||
@@ -3,8 +3,10 @@ define(['underscore'], function(_) {
|
||||
|
||||
var USER_ACCOUNTS_API_URL = '/api/user/v0/accounts/student';
|
||||
var USER_PREFERENCES_API_URL = '/api/user/v0/preferences/student';
|
||||
var BADGES_API_URL = '/api/badges/v1/assertions/user/student/';
|
||||
var IMAGE_UPLOAD_API_URL = '/api/profile_images/v0/staff/upload';
|
||||
var IMAGE_REMOVE_API_URL = '/api/profile_images/v0/staff/remove';
|
||||
var FIND_COURSES_URL = '/courses';
|
||||
|
||||
var PROFILE_IMAGE = {
|
||||
image_url_large: '/media/profile-images/image.jpg',
|
||||
@@ -23,7 +25,8 @@ define(['underscore'], function(_) {
|
||||
language: null,
|
||||
bio: "About the student",
|
||||
language_proficiencies: [{code: '1'}],
|
||||
profile_image: PROFILE_IMAGE
|
||||
profile_image: PROFILE_IMAGE,
|
||||
accomplishments_shared: false
|
||||
};
|
||||
|
||||
var createAccountSettingsData = function(options) {
|
||||
@@ -109,6 +112,8 @@ define(['underscore'], function(_) {
|
||||
return {
|
||||
USER_ACCOUNTS_API_URL: USER_ACCOUNTS_API_URL,
|
||||
USER_PREFERENCES_API_URL: USER_PREFERENCES_API_URL,
|
||||
BADGES_API_URL: BADGES_API_URL,
|
||||
FIND_COURSES_URL: FIND_COURSES_URL,
|
||||
IMAGE_UPLOAD_API_URL: IMAGE_UPLOAD_API_URL,
|
||||
IMAGE_REMOVE_API_URL: IMAGE_REMOVE_API_URL,
|
||||
IMAGE_MAX_BYTES: IMAGE_MAX_BYTES,
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
define(['backbone', 'jquery', 'underscore', 'URI', 'common/js/spec_helpers/ajax_helpers',
|
||||
'js/spec/student_profile/helpers',
|
||||
'js/student_profile/views/badge_list_container',
|
||||
'common/js/components/collections/paging_collection'
|
||||
],
|
||||
function (Backbone, $, _, URI, AjaxHelpers, LearnerProfileHelpers, BadgeListContainer, PagingCollection) {
|
||||
'use strict';
|
||||
describe('edx.user.BadgeListContainer', function () {
|
||||
|
||||
var view, requests;
|
||||
|
||||
var createView = function (requests, badge_list_object) {
|
||||
var badgeCollection = new PagingCollection();
|
||||
badgeCollection.url = '/api/badges/v1/assertions/user/staff/';
|
||||
var models = [];
|
||||
_.each(_.range(badge_list_object.count), function (idx) {
|
||||
models.push(LearnerProfileHelpers.makeBadge(idx));
|
||||
});
|
||||
badge_list_object.results = models;
|
||||
badgeCollection.fetch();
|
||||
var request = AjaxHelpers.currentRequest(requests);
|
||||
var path = new URI(request.url).path();
|
||||
expect(path).toBe('/api/badges/v1/assertions/user/staff/');
|
||||
AjaxHelpers.respondWithJson(requests, badge_list_object);
|
||||
var badge_list_container = new BadgeListContainer({
|
||||
'collection': badgeCollection
|
||||
|
||||
});
|
||||
badge_list_container.render();
|
||||
return badge_list_container;
|
||||
};
|
||||
|
||||
afterEach(function () {
|
||||
view.$el.remove();
|
||||
});
|
||||
|
||||
it('displays all badges', function () {
|
||||
requests = AjaxHelpers.requests(this);
|
||||
view = createView(requests, {
|
||||
count: 30,
|
||||
previous: '/arbitrary/url',
|
||||
num_pages: 3,
|
||||
next: null,
|
||||
start: 20,
|
||||
current_page: 1,
|
||||
results: []
|
||||
});
|
||||
var badges = view.$el.find('div.badge-display');
|
||||
expect(badges.length).toBe(30);
|
||||
});
|
||||
|
||||
it('displays placeholder on last page', function () {
|
||||
requests = AjaxHelpers.requests(this);
|
||||
view = createView(requests, {
|
||||
count: 30,
|
||||
previous: '/arbitrary/url',
|
||||
num_pages: 3,
|
||||
next: null,
|
||||
start: 20,
|
||||
current_page: 3,
|
||||
results: []
|
||||
});
|
||||
var placeholder = view.$el.find('span.accomplishment-placeholder');
|
||||
expect(placeholder.length).toBe(1);
|
||||
});
|
||||
|
||||
it('does not display placeholder on first page', function () {
|
||||
requests = AjaxHelpers.requests(this);
|
||||
view = createView(requests, {
|
||||
count: 30,
|
||||
previous: '/arbitrary/url',
|
||||
num_pages: 3,
|
||||
next: null,
|
||||
start: 0,
|
||||
current_page: 1,
|
||||
results: []
|
||||
});
|
||||
var placeholder = view.$el.find('span.accomplishment-placeholder');
|
||||
expect(placeholder.length).toBe(0);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
74
lms/static/js/spec/student_profile/badge_list_view_spec.js
Normal file
74
lms/static/js/spec/student_profile/badge_list_view_spec.js
Normal file
@@ -0,0 +1,74 @@
|
||||
define(['backbone', 'jquery', 'underscore',
|
||||
'js/spec/student_profile/helpers',
|
||||
'js/student_profile/views/badge_list_view',
|
||||
'common/js/components/collections/paging_collection'
|
||||
],
|
||||
function (Backbone, $, _, LearnerProfileHelpers, BadgeListView, PagingCollection) {
|
||||
"use strict";
|
||||
describe("edx.user.BadgeListView", function () {
|
||||
|
||||
var view;
|
||||
|
||||
var createView = function (badges, pages, page, hasNextPage) {
|
||||
var badgeCollection = new PagingCollection();
|
||||
badgeCollection.url = "/api/badges/v1/assertions/user/staff/";
|
||||
var models = [];
|
||||
_.each(badges, function (element) {
|
||||
models.push(new Backbone.Model(element));
|
||||
});
|
||||
badgeCollection.models = models;
|
||||
badgeCollection.length = badges.length;
|
||||
badgeCollection.currentPage = page;
|
||||
badgeCollection.totalPages = pages;
|
||||
badgeCollection.hasNextPage = function () {
|
||||
return hasNextPage;
|
||||
};
|
||||
var badge_list = new BadgeListView({
|
||||
'collection': badgeCollection
|
||||
|
||||
});
|
||||
return badge_list;
|
||||
};
|
||||
|
||||
afterEach(function () {
|
||||
view.$el.remove();
|
||||
});
|
||||
|
||||
it("there is a single row if there is only one badge", function () {
|
||||
view = createView([LearnerProfileHelpers.makeBadge(1)], 1, 1, false);
|
||||
view.render();
|
||||
var rows = view.$el.find('div.row');
|
||||
expect(rows.length).toBe(1);
|
||||
});
|
||||
|
||||
it("accomplishments placeholder is visible on a last page", function () {
|
||||
view = createView([LearnerProfileHelpers.makeBadge(1)], 2, 2, false);
|
||||
view.render();
|
||||
var placeholder = view.$el.find('span.accomplishment-placeholder');
|
||||
expect(placeholder.length).toBe(1);
|
||||
});
|
||||
|
||||
it("accomplishments placeholder to be not visible on a first page", function () {
|
||||
view = createView([LearnerProfileHelpers.makeBadge(1)], 1, 2, true);
|
||||
view.render();
|
||||
var placeholder = view.$el.find('span.accomplishment-placeholder');
|
||||
expect(placeholder.length).toBe(0);
|
||||
});
|
||||
|
||||
it("badges are in two columns (checked by counting rows for a known number of badges)", function () {
|
||||
var badges = [];
|
||||
_.each(_.range(4), function (item) {
|
||||
badges.push(LearnerProfileHelpers.makeBadge(item));
|
||||
});
|
||||
view = createView(badges, 1, 2, true);
|
||||
view.render();
|
||||
var placeholder = view.$el.find('span.accomplishment-placeholder');
|
||||
expect(placeholder.length).toBe(0);
|
||||
var rows = view.$el.find('div.row');
|
||||
expect(rows.length).toBe(2);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
104
lms/static/js/spec/student_profile/badge_view_spec.js
Normal file
104
lms/static/js/spec/student_profile/badge_view_spec.js
Normal file
@@ -0,0 +1,104 @@
|
||||
define(['backbone', 'jquery', 'underscore',
|
||||
'js/spec/student_profile/helpers',
|
||||
'js/student_profile/views/badge_view'
|
||||
],
|
||||
function (Backbone, $, _, LearnerProfileHelpers, BadgeView) {
|
||||
"use strict";
|
||||
describe("edx.user.BadgeView", function () {
|
||||
|
||||
var view, badge;
|
||||
|
||||
var createView = function (ownProfile) {
|
||||
badge = LearnerProfileHelpers.makeBadge(1);
|
||||
var options = {
|
||||
'model': new Backbone.Model(badge),
|
||||
'ownProfile': ownProfile,
|
||||
'badgeMeta': {}
|
||||
};
|
||||
var view = new BadgeView(options);
|
||||
view.render();
|
||||
$('body').append(view.$el);
|
||||
view.$el.show();
|
||||
expect(view.$el.is(':visible')).toBe(true);
|
||||
return view;
|
||||
};
|
||||
|
||||
afterEach(function () {
|
||||
view.$el.remove();
|
||||
$('.badges-modal').remove();
|
||||
});
|
||||
|
||||
it("profile of other has no share button", function () {
|
||||
view = createView(false);
|
||||
expect(view.context.ownProfile).toBeFalsy();
|
||||
expect(view.$el.find('button.share-button').length).toBe(0);
|
||||
});
|
||||
|
||||
it("own profile has share button", function () {
|
||||
view = createView(true);
|
||||
expect(view.context.ownProfile).toBeTruthy();
|
||||
expect(view.$el.find('button.share-button').length).toBe(1);
|
||||
});
|
||||
|
||||
it("click on share button calls createModal function", function () {
|
||||
view = createView(true);
|
||||
spyOn(view, "createModal");
|
||||
view.delegateEvents();
|
||||
expect(view.context.ownProfile).toBeTruthy();
|
||||
var shareButton = view.$el.find('button.share-button');
|
||||
expect(shareButton.length).toBe(1);
|
||||
expect(view.createModal).not.toHaveBeenCalled();
|
||||
shareButton.click();
|
||||
expect(view.createModal).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("click on share button calls shows the dialog", function () {
|
||||
view = createView(true);
|
||||
expect(view.context.ownProfile).toBeTruthy();
|
||||
var shareButton = view.$el.find('button.share-button');
|
||||
expect(shareButton.length).toBe(1);
|
||||
var modalElement = $('.badges-modal');
|
||||
expect(modalElement.length).toBe(0);
|
||||
expect(modalElement.is(":visible")).toBeFalsy();
|
||||
shareButton.click();
|
||||
// Note: this element should have appeared in the dom during: shareButton.click();
|
||||
modalElement = $('.badges-modal');
|
||||
waitsFor(function () {
|
||||
return modalElement.is(":visible");
|
||||
}, '', 1000);
|
||||
});
|
||||
|
||||
var testBadgeNameIsDisplayed = function (ownProfile) {
|
||||
view = createView(ownProfile);
|
||||
var badgeDiv = view.$el.find(".badge-name");
|
||||
expect(badgeDiv.length).toBeTruthy();
|
||||
expect(badgeDiv.is(':visible')).toBe(true);
|
||||
expect(_.count(badgeDiv.html(), badge.badge_class.display_name)).toBeTruthy();
|
||||
};
|
||||
|
||||
it("test badge name is displayed for own profile", function () {
|
||||
testBadgeNameIsDisplayed(true);
|
||||
});
|
||||
|
||||
it("test badge name is displayed for other profile", function () {
|
||||
testBadgeNameIsDisplayed(false);
|
||||
});
|
||||
|
||||
var testBadgeIconIsDisplayed = function (ownProfile) {
|
||||
view = createView(ownProfile);
|
||||
var badgeImg = view.$el.find("img.badge");
|
||||
expect(badgeImg.length).toBe(1);
|
||||
expect(badgeImg.attr('src')).toEqual(badge.image_url);
|
||||
};
|
||||
|
||||
it("test badge icon is displayed for own profile", function () {
|
||||
testBadgeIconIsDisplayed(true);
|
||||
});
|
||||
|
||||
it("test badge icon is displayed for other profile", function () {
|
||||
testBadgeIconIsDisplayed(false);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -1,4 +1,4 @@
|
||||
define(['underscore'], function(_) {
|
||||
define(['underscore', 'URI', 'common/js/spec_helpers/ajax_helpers'], function(_, URI, AjaxHelpers) {
|
||||
'use strict';
|
||||
|
||||
var expectProfileElementContainsField = function(element, view) {
|
||||
@@ -93,7 +93,7 @@ define(['underscore'], function(_) {
|
||||
|
||||
if (othersProfile) {
|
||||
expect($('.profile-private--message').text())
|
||||
.toBe('This edX learner is currently sharing a limited profile.');
|
||||
.toBe('This learner is currently sharing a limited profile.');
|
||||
} else {
|
||||
expect($('.profile-private--message').text()).toBe('You are currently sharing a limited profile.');
|
||||
}
|
||||
@@ -105,9 +105,138 @@ define(['underscore'], function(_) {
|
||||
expect(learnerProfileView.$('.wrapper-profile-section-two').length).toBe(0);
|
||||
};
|
||||
|
||||
var expectTabbedViewToBeHidden = function(requests, tabbedViewView) {
|
||||
// Unrelated initial request, no badge request
|
||||
expect(requests.length).toBe(1);
|
||||
expect(tabbedViewView.$el.find('.page-content-nav').is(':visible')).toBe(false);
|
||||
};
|
||||
|
||||
var expectTabbedViewToBeShown = function(tabbedViewView) {
|
||||
expect(tabbedViewView.$el.find('.page-content-nav').is(':visible')).toBe(true);
|
||||
};
|
||||
|
||||
var expectBadgesDisplayed = function(learnerProfileView, length, lastPage) {
|
||||
var badgeListingView = learnerProfileView.$el.find('#tabpanel-accomplishments');
|
||||
expect(learnerProfileView.$el.find('#tabpanel-about_me').hasClass('is-hidden')).toBe(true);
|
||||
expect(badgeListingView.hasClass('is-hidden')).toBe(false);
|
||||
if (lastPage) {
|
||||
length += 1;
|
||||
var placeholder = badgeListingView.find('.find-course');
|
||||
expect(placeholder.length).toBe(1);
|
||||
expect(placeholder.attr('href')).toBe('/courses/');
|
||||
}
|
||||
expect(badgeListingView.find('.badge-display').length).toBe(length);
|
||||
};
|
||||
|
||||
var expectBadgesHidden = function(learnerProfileView) {
|
||||
var accomplishmentsTab = learnerProfileView.$el.find('#tabpanel-accomplishments');
|
||||
if (accomplishmentsTab.length) {
|
||||
// Nonexistence counts as hidden.
|
||||
expect(learnerProfileView.$el.find('#tabpanel-accomplishments').hasClass('is-hidden')).toBe(true);
|
||||
}
|
||||
expect(learnerProfileView.$el.find('#tabpanel-about_me').hasClass('is-hidden')).toBe(false);
|
||||
};
|
||||
|
||||
var expectPage = function(learnerProfileView, pageData) {
|
||||
var badgeListContainer = learnerProfileView.$el.find('#tabpanel-accomplishments');
|
||||
var index = badgeListContainer.find('span.search-count').text().trim();
|
||||
expect(index).toBe("Showing " + (pageData.start + 1) + "-" + (pageData.start + pageData.results.length) +
|
||||
" out of " + pageData.count + " total");
|
||||
expect(badgeListContainer.find('.current-page').text()).toBe("" + pageData.current_page);
|
||||
_.each(pageData.results, function(badge) {
|
||||
expect($(".badge-display:contains(" + badge.badge_class.display_name + ")").length).toBe(1);
|
||||
});
|
||||
};
|
||||
|
||||
var expectBadgeLoadingErrorIsRendered = function(learnerProfileView) {
|
||||
var errorMessage = learnerProfileView.$el.find(".badge-set-display").text();
|
||||
expect(errorMessage).toBe(
|
||||
'Your request could not be completed. Reload the page and try again. If the issue persists, click the ' +
|
||||
'Help tab to report the problem.'
|
||||
);
|
||||
};
|
||||
|
||||
var breakBadgeLoading = function(learnerProfileView, requests) {
|
||||
var request = AjaxHelpers.currentRequest(requests);
|
||||
var path = new URI(request.url).path();
|
||||
expect(path).toBe('/api/badges/v1/assertions/user/student/');
|
||||
AjaxHelpers.respondWithError(requests, 500);
|
||||
};
|
||||
|
||||
var firstPageBadges = {
|
||||
count: 30,
|
||||
previous: null,
|
||||
next: "/arbitrary/url",
|
||||
num_pages: 3,
|
||||
start: 0,
|
||||
current_page: 1,
|
||||
results: []
|
||||
};
|
||||
|
||||
var secondPageBadges = {
|
||||
count: 30,
|
||||
previous: "/arbitrary/url",
|
||||
next: "/arbitrary/url",
|
||||
num_pages: 3,
|
||||
start: 10,
|
||||
current_page: 2,
|
||||
results: []
|
||||
};
|
||||
|
||||
var thirdPageBadges = {
|
||||
count: 30,
|
||||
previous: "/arbitrary/url",
|
||||
num_pages: 3,
|
||||
next: null,
|
||||
start: 20,
|
||||
current_page: 3,
|
||||
results: []
|
||||
};
|
||||
|
||||
function makeBadge (num) {
|
||||
return {
|
||||
"badge_class": {
|
||||
"slug": "test_slug_" + num,
|
||||
"issuing_component": "test_component",
|
||||
"display_name": "Test Badge " + num,
|
||||
"course_id": null,
|
||||
"description": "Yay! It's a test badge.",
|
||||
"criteria": "https://example.com/syllabus",
|
||||
"image_url": "http://localhost:8000/media/badge_classes/test_lMB9bRw.png"
|
||||
},
|
||||
"image_url": "http://example.com/image.png",
|
||||
"assertion_url": "http://example.com/example.json",
|
||||
"created_at": "2015-12-03T16:25:57.676113Z"
|
||||
};
|
||||
}
|
||||
|
||||
_.each(_.range(0, 10), function(i) {
|
||||
firstPageBadges.results.push(makeBadge(i));
|
||||
});
|
||||
|
||||
_.each(_.range(10, 20), function(i) {
|
||||
secondPageBadges.results.push(makeBadge(i));
|
||||
});
|
||||
|
||||
_.each(_.range(20, 30), function(i) {
|
||||
thirdPageBadges.results.push(makeBadge(i));
|
||||
});
|
||||
|
||||
var emptyBadges = {
|
||||
"count": 0,
|
||||
"previous": null,
|
||||
"num_pages": 1,
|
||||
"results": []
|
||||
};
|
||||
|
||||
return {
|
||||
expectLimitedProfileSectionsAndFieldsToBeRendered: expectLimitedProfileSectionsAndFieldsToBeRendered,
|
||||
expectProfileSectionsAndFieldsToBeRendered: expectProfileSectionsAndFieldsToBeRendered,
|
||||
expectProfileSectionsNotToBeRendered: expectProfileSectionsNotToBeRendered
|
||||
expectProfileSectionsNotToBeRendered: expectProfileSectionsNotToBeRendered,
|
||||
expectTabbedViewToBeHidden: expectTabbedViewToBeHidden, expectTabbedViewToBeShown: expectTabbedViewToBeShown,
|
||||
expectBadgesDisplayed: expectBadgesDisplayed, expectBadgesHidden: expectBadgesHidden,
|
||||
expectBadgeLoadingErrorIsRendered: expectBadgeLoadingErrorIsRendered, breakBadgeLoading: breakBadgeLoading,
|
||||
firstPageBadges: firstPageBadges, secondPageBadges: secondPageBadges, thirdPageBadges: thirdPageBadges,
|
||||
emptyBadges: emptyBadges, expectPage: expectPage, makeBadge: makeBadge
|
||||
};
|
||||
});
|
||||
|
||||
@@ -22,10 +22,15 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers
|
||||
loadFixtures('js/fixtures/student_profile/student_profile.html');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
Backbone.history.stop();
|
||||
});
|
||||
|
||||
var createProfilePage = function(ownProfile, options) {
|
||||
return new LearnerProfilePage({
|
||||
'accounts_api_url': Helpers.USER_ACCOUNTS_API_URL,
|
||||
'preferences_api_url': Helpers.USER_PREFERENCES_API_URL,
|
||||
'badges_api_url': Helpers.BADGES_API_URL,
|
||||
'own_profile': ownProfile,
|
||||
'account_settings_page_url': Helpers.USER_ACCOUNTS_API_URL,
|
||||
'country_options': Helpers.FIELD_OPTIONS,
|
||||
@@ -37,6 +42,7 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers
|
||||
'profile_image_remove_url': Helpers.IMAGE_REMOVE_API_URL,
|
||||
'default_visibility': 'all_users',
|
||||
'platform_name': 'edX',
|
||||
'find_courses_url': '/courses/',
|
||||
'account_settings_data': Helpers.createAccountSettingsData(options),
|
||||
'preferences_data': Helpers.createUserPreferencesData()
|
||||
});
|
||||
@@ -62,6 +68,165 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers
|
||||
LearnerProfileHelpers.expectLimitedProfileSectionsAndFieldsToBeRendered(learnerProfileView);
|
||||
});
|
||||
|
||||
it("doesn't show the mode toggle if badges are disabled", function() {
|
||||
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
var context = createProfilePage(true, {accomplishments_shared: false}),
|
||||
tabbedView = context.learnerProfileView.tabbedView,
|
||||
learnerProfileView = context.learnerProfileView;
|
||||
|
||||
LearnerProfileHelpers.expectTabbedViewToBeHidden(requests, tabbedView);
|
||||
LearnerProfileHelpers.expectBadgesHidden(learnerProfileView);
|
||||
});
|
||||
|
||||
it("doesn't show the mode toggle if badges fail to fetch", function() {
|
||||
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
var context = createProfilePage(true, {accomplishments_shared: false}),
|
||||
tabbedView = context.learnerProfileView.tabbedView,
|
||||
learnerProfileView = context.learnerProfileView;
|
||||
|
||||
LearnerProfileHelpers.expectTabbedViewToBeHidden(requests, tabbedView);
|
||||
LearnerProfileHelpers.expectBadgesHidden(learnerProfileView);
|
||||
});
|
||||
|
||||
it("renders the mode toggle if there are badges", function() {
|
||||
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
var context = createProfilePage(true, {accomplishments_shared: true}),
|
||||
tabbedView = context.learnerProfileView.tabbedView;
|
||||
|
||||
AjaxHelpers.expectRequest(requests, 'POST', '/event');
|
||||
AjaxHelpers.respondWithError(requests, 404);
|
||||
AjaxHelpers.respondWithJson(requests, LearnerProfileHelpers.firstPageBadges);
|
||||
|
||||
LearnerProfileHelpers.expectTabbedViewToBeShown(tabbedView);
|
||||
});
|
||||
|
||||
it("renders the mode toggle if badges enabled but none exist", function() {
|
||||
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
var context = createProfilePage(true, {accomplishments_shared: true}),
|
||||
tabbedView = context.learnerProfileView.tabbedView;
|
||||
|
||||
AjaxHelpers.expectRequest(requests, 'POST', '/event');
|
||||
AjaxHelpers.respondWithError(requests, 404);
|
||||
AjaxHelpers.respondWithJson(requests, LearnerProfileHelpers.emptyBadges);
|
||||
|
||||
LearnerProfileHelpers.expectTabbedViewToBeShown(tabbedView);
|
||||
});
|
||||
|
||||
it("displays the badges when the accomplishments toggle is selected", function () {
|
||||
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
var context = createProfilePage(true, {accomplishments_shared: true}),
|
||||
learnerProfileView = context.learnerProfileView,
|
||||
tabbedView = learnerProfileView.tabbedView;
|
||||
|
||||
AjaxHelpers.expectRequest(requests, 'POST', '/event');
|
||||
AjaxHelpers.respondWithError(requests, 404);
|
||||
AjaxHelpers.respondWithJson(requests, LearnerProfileHelpers.secondPageBadges);
|
||||
|
||||
LearnerProfileHelpers.expectBadgesHidden(learnerProfileView);
|
||||
tabbedView.$el.find('[data-url="accomplishments"]').click();
|
||||
LearnerProfileHelpers.expectBadgesDisplayed(learnerProfileView, 10, false);
|
||||
tabbedView.$el.find('[data-url="about_me"]').click();
|
||||
LearnerProfileHelpers.expectBadgesHidden(learnerProfileView);
|
||||
});
|
||||
|
||||
it("displays a placeholder on the last page of badges", function () {
|
||||
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
var context = createProfilePage(true, {accomplishments_shared: true}),
|
||||
learnerProfileView = context.learnerProfileView,
|
||||
tabbedView = learnerProfileView.tabbedView;
|
||||
|
||||
AjaxHelpers.expectRequest(requests, 'POST', '/event');
|
||||
AjaxHelpers.respondWithError(requests, 404);
|
||||
AjaxHelpers.respondWithJson(requests, LearnerProfileHelpers.thirdPageBadges);
|
||||
|
||||
LearnerProfileHelpers.expectBadgesHidden(learnerProfileView);
|
||||
tabbedView.$el.find('[data-url="accomplishments"]').click();
|
||||
LearnerProfileHelpers.expectBadgesDisplayed(learnerProfileView, 10, true);
|
||||
tabbedView.$el.find('[data-url="about_me"]').click();
|
||||
LearnerProfileHelpers.expectBadgesHidden(learnerProfileView);
|
||||
});
|
||||
|
||||
it("displays a placeholder when the accomplishments toggle is selected and no badges exist", function () {
|
||||
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
var context = createProfilePage(true, {accomplishments_shared: true}),
|
||||
learnerProfileView = context.learnerProfileView,
|
||||
tabbedView = learnerProfileView.tabbedView;
|
||||
|
||||
AjaxHelpers.expectRequest(requests, 'POST', '/event');
|
||||
AjaxHelpers.respondWithError(requests, 404);
|
||||
AjaxHelpers.respondWithJson(requests, LearnerProfileHelpers.emptyBadges);
|
||||
|
||||
LearnerProfileHelpers.expectBadgesHidden(learnerProfileView);
|
||||
tabbedView.$el.find('[data-url="accomplishments"]').click();
|
||||
LearnerProfileHelpers.expectBadgesDisplayed(learnerProfileView, 0, true);
|
||||
tabbedView.$el.find('[data-url="about_me"]').click();
|
||||
LearnerProfileHelpers.expectBadgesHidden(learnerProfileView);
|
||||
});
|
||||
|
||||
it("shows a paginated list of badges", function() {
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
var context = createProfilePage(true, {accomplishments_shared: true}),
|
||||
learnerProfileView = context.learnerProfileView,
|
||||
tabbedView = learnerProfileView.tabbedView;
|
||||
|
||||
AjaxHelpers.expectRequest(requests, 'POST', '/event');
|
||||
AjaxHelpers.respondWithError(requests, 404);
|
||||
AjaxHelpers.respondWithJson(requests, LearnerProfileHelpers.firstPageBadges);
|
||||
|
||||
tabbedView.$el.find('[data-url="accomplishments"]').click();
|
||||
LearnerProfileHelpers.expectBadgesDisplayed(learnerProfileView, 10, false);
|
||||
LearnerProfileHelpers.expectPage(learnerProfileView, LearnerProfileHelpers.firstPageBadges);
|
||||
});
|
||||
|
||||
it("allows forward and backward navigation of badges", function () {
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
var context = createProfilePage(true, {accomplishments_shared: true}),
|
||||
learnerProfileView = context.learnerProfileView,
|
||||
tabbedView = learnerProfileView.tabbedView,
|
||||
badgeListContainer = context.badgeListContainer;
|
||||
|
||||
AjaxHelpers.expectRequest(requests, 'POST', '/event');
|
||||
AjaxHelpers.respondWithError(requests, 404);
|
||||
AjaxHelpers.respondWithJson(requests, LearnerProfileHelpers.firstPageBadges);
|
||||
|
||||
tabbedView.$el.find('[data-url="accomplishments"]').click();
|
||||
|
||||
badgeListContainer.$el.find('.next-page-link').click();
|
||||
AjaxHelpers.respondWithJson(requests, LearnerProfileHelpers.secondPageBadges);
|
||||
LearnerProfileHelpers.expectPage(learnerProfileView, LearnerProfileHelpers.secondPageBadges);
|
||||
|
||||
badgeListContainer.$el.find('.next-page-link').click();
|
||||
AjaxHelpers.respondWithJson(requests, LearnerProfileHelpers.thirdPageBadges);
|
||||
LearnerProfileHelpers.expectBadgesDisplayed(learnerProfileView, 10, true);
|
||||
LearnerProfileHelpers.expectPage(learnerProfileView, LearnerProfileHelpers.thirdPageBadges);
|
||||
|
||||
badgeListContainer.$el.find('.previous-page-link').click();
|
||||
AjaxHelpers.respondWithJson(requests, LearnerProfileHelpers.secondPageBadges);
|
||||
LearnerProfileHelpers.expectPage(learnerProfileView, LearnerProfileHelpers.secondPageBadges);
|
||||
LearnerProfileHelpers.expectBadgesDisplayed(learnerProfileView, 10, false);
|
||||
|
||||
badgeListContainer.$el.find('.previous-page-link').click();
|
||||
AjaxHelpers.respondWithJson(requests, LearnerProfileHelpers.firstPageBadges);
|
||||
LearnerProfileHelpers.expectPage(learnerProfileView, LearnerProfileHelpers.firstPageBadges);
|
||||
});
|
||||
|
||||
|
||||
it("renders the limited profile for under 13 users", function() {
|
||||
|
||||
var context = createProfilePage(
|
||||
|
||||
@@ -6,12 +6,14 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers
|
||||
'js/student_account/models/user_preferences_model',
|
||||
'js/student_profile/views/learner_profile_fields',
|
||||
'js/student_profile/views/learner_profile_view',
|
||||
'js/student_profile/views/badge_list_container',
|
||||
'js/student_account/views/account_settings_fields',
|
||||
'common/js/components/collections/paging_collection',
|
||||
'js/views/message_banner'
|
||||
],
|
||||
function (Backbone, $, _, AjaxHelpers, TemplateHelpers, Helpers, LearnerProfileHelpers, FieldViews,
|
||||
UserAccountModel, AccountPreferencesModel, LearnerProfileFields, LearnerProfileView,
|
||||
AccountSettingsFieldViews, MessageBannerView) {
|
||||
BadgeListContainer, AccountSettingsFieldViews, PagingCollection, MessageBannerView) {
|
||||
'use strict';
|
||||
|
||||
describe("edx.user.LearnerProfileView", function () {
|
||||
@@ -106,6 +108,15 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers
|
||||
})
|
||||
];
|
||||
|
||||
var badgeCollection = new PagingCollection();
|
||||
badgeCollection.url = Helpers.BADGES_API_URL;
|
||||
|
||||
var badgeListContainer = new BadgeListContainer({
|
||||
'attributes': {'class': 'badge-set-display'},
|
||||
'collection': badgeCollection,
|
||||
'find_courses_url': Helpers.FIND_COURSES_URL
|
||||
});
|
||||
|
||||
return new LearnerProfileView(
|
||||
{
|
||||
el: $('.wrapper-profile'),
|
||||
@@ -117,7 +128,8 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers
|
||||
usernameFieldView: usernameFieldView,
|
||||
profileImageFieldView: profileImageFieldView,
|
||||
sectionOneFieldViews: sectionOneFieldViews,
|
||||
sectionTwoFieldViews: sectionTwoFieldViews
|
||||
sectionTwoFieldViews: sectionTwoFieldViews,
|
||||
badgeListContainer: badgeListContainer
|
||||
});
|
||||
};
|
||||
|
||||
@@ -125,6 +137,10 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers
|
||||
loadFixtures('js/fixtures/student_profile/student_profile.html');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
Backbone.history.stop();
|
||||
});
|
||||
|
||||
it("shows loading error correctly", function() {
|
||||
|
||||
var learnerProfileView = createLearnerProfileView(false, 'all_users');
|
||||
@@ -189,5 +205,16 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers
|
||||
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
|
||||
LearnerProfileHelpers.expectLimitedProfileSectionsAndFieldsToBeRendered(learnerProfileView, true);
|
||||
});
|
||||
|
||||
it("renders an error if the badges can't be fetched", function () {
|
||||
var learnerProfileView = createLearnerProfileView(false, 'all_users', true);
|
||||
learnerProfileView.options.accountSettingsModel.set({'accomplishments_shared': true});
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
|
||||
learnerProfileView.render();
|
||||
|
||||
LearnerProfileHelpers.breakBadgeLoading(learnerProfileView, requests);
|
||||
LearnerProfileHelpers.expectBadgeLoadingErrorIsRendered(learnerProfileView);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
112
lms/static/js/spec/student_profile/section_two_tab_spec.js
Normal file
112
lms/static/js/spec/student_profile/section_two_tab_spec.js
Normal file
@@ -0,0 +1,112 @@
|
||||
define(['backbone', 'jquery', 'underscore',
|
||||
'js/spec/student_account/helpers',
|
||||
'js/student_profile/views/section_two_tab',
|
||||
'js/views/fields',
|
||||
'js/student_account/models/user_account_model'
|
||||
],
|
||||
function (Backbone, $, _, Helpers, SectionTwoTabView, FieldViews, UserAccountModel) {
|
||||
"use strict";
|
||||
describe("edx.user.SectionTwoTab", function () {
|
||||
|
||||
var createSectionTwoView = function (ownProfile, profileIsPublic) {
|
||||
|
||||
var accountSettingsModel = new UserAccountModel();
|
||||
accountSettingsModel.set(Helpers.createAccountSettingsData());
|
||||
accountSettingsModel.set({'profile_is_public': profileIsPublic});
|
||||
accountSettingsModel.set({'profile_image': Helpers.PROFILE_IMAGE});
|
||||
|
||||
var editable = ownProfile ? 'toggle' : 'never';
|
||||
|
||||
var sectionTwoFieldViews = [
|
||||
new FieldViews.TextareaFieldView({
|
||||
model: accountSettingsModel,
|
||||
editable: editable,
|
||||
showMessages: false,
|
||||
title: 'About me',
|
||||
placeholderValue: "Tell other edX learners a little about yourself: where you live, " +
|
||||
"what your interests are, why you're taking courses on edX, or what you hope to learn.",
|
||||
valueAttribute: "bio",
|
||||
helpMessage: '',
|
||||
messagePosition: 'header'
|
||||
})
|
||||
];
|
||||
|
||||
return new SectionTwoTabView({
|
||||
viewList: sectionTwoFieldViews,
|
||||
showFullProfile: function(){
|
||||
return profileIsPublic;
|
||||
},
|
||||
ownProfile: ownProfile
|
||||
});
|
||||
};
|
||||
|
||||
it("full profile displayed for public profile", function () {
|
||||
var view = createSectionTwoView(false, true);
|
||||
view.render();
|
||||
var bio = view.$el.find('.u-field-bio');
|
||||
expect(bio.length).toBe(1);
|
||||
});
|
||||
|
||||
it("profile field parts are actually rendered for public profile", function () {
|
||||
var view = createSectionTwoView(false, true);
|
||||
_.each(view.options.viewList, function (fieldView) {
|
||||
spyOn(fieldView, "render").andCallThrough();
|
||||
});
|
||||
view.render();
|
||||
_.each(view.options.viewList, function (fieldView) {
|
||||
expect(fieldView.render).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
var testPrivateProfile = function (ownProfile, msg_string) {
|
||||
var view = createSectionTwoView(ownProfile, false);
|
||||
view.render();
|
||||
var bio = view.$el.find('.u-field-bio');
|
||||
expect(bio.length).toBe(0);
|
||||
var msg = view.$el.find('span.profile-private--message');
|
||||
expect(msg.length).toBe(1);
|
||||
expect(_.count(msg.html(), msg_string)).toBeTruthy();
|
||||
};
|
||||
|
||||
it("no profile when profile is private for other people", function () {
|
||||
testPrivateProfile(false, "This learner is currently sharing a limited profile");
|
||||
});
|
||||
|
||||
it("no profile when profile is private for the user herself", function () {
|
||||
testPrivateProfile(true, "You are currently sharing a limited profile");
|
||||
});
|
||||
|
||||
var testProfilePrivatePartsDoNotRender = function (ownProfile) {
|
||||
var view = createSectionTwoView(ownProfile, false);
|
||||
_.each(view.options.viewList, function (fieldView) {
|
||||
spyOn(fieldView, "render");
|
||||
});
|
||||
view.render();
|
||||
_.each(view.options.viewList, function (fieldView) {
|
||||
expect(fieldView.render).not.toHaveBeenCalled();
|
||||
});
|
||||
};
|
||||
|
||||
it("profile field parts are not rendered for private profile for owner", function () {
|
||||
testProfilePrivatePartsDoNotRender(true);
|
||||
});
|
||||
|
||||
it("profile field parts are not rendered for private profile for other people", function () {
|
||||
testProfilePrivatePartsDoNotRender(false);
|
||||
});
|
||||
|
||||
it("does not allow fields to be edited when visiting a profile for other people", function () {
|
||||
var view = createSectionTwoView(false, true);
|
||||
var bio = view.options.viewList[0];
|
||||
expect(bio.editable).toBe("never");
|
||||
});
|
||||
|
||||
it("allows fields to be edited when visiting one's own profile", function () {
|
||||
var view = createSectionTwoView(true, true);
|
||||
var bio = view.options.viewList[0];
|
||||
expect(bio.editable).toBe("toggle");
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
60
lms/static/js/spec/student_profile/share_modal_view_spec.js
Normal file
60
lms/static/js/spec/student_profile/share_modal_view_spec.js
Normal file
@@ -0,0 +1,60 @@
|
||||
define(['backbone', 'jquery', 'underscore', 'moment',
|
||||
'js/spec/student_account/helpers',
|
||||
'js/spec/student_profile/helpers',
|
||||
'js/student_profile/views/share_modal_view',
|
||||
'jquery.simulate'
|
||||
],
|
||||
function (Backbone, $, _, Moment, Helpers, LearnerProfileHelpers, ShareModalView) {
|
||||
"use strict";
|
||||
describe("edx.user.ShareModalView", function () {
|
||||
var keys = $.simulate.keyCode;
|
||||
|
||||
var view;
|
||||
|
||||
var createModalView = function () {
|
||||
var badge = LearnerProfileHelpers.makeBadge(1);
|
||||
var context = _.extend(badge, {
|
||||
'created': new Moment(badge.created),
|
||||
'ownProfile': true,
|
||||
'badgeMeta': {}
|
||||
});
|
||||
return new ShareModalView({
|
||||
model: new Backbone.Model(context),
|
||||
shareButton: $("<button/>")
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
view = createModalView();
|
||||
// Attach view to document, otherwise click won't work
|
||||
view.render();
|
||||
$('body').append(view.$el);
|
||||
view.$el.show();
|
||||
expect(view.$el.is(':visible')).toBe(true);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
view.$el.remove();
|
||||
});
|
||||
|
||||
it("modal view closes on escape", function () {
|
||||
spyOn(view, "close");
|
||||
view.delegateEvents();
|
||||
expect(view.close).not.toHaveBeenCalled();
|
||||
$(view.$el).simulate("keydown", {keyCode: keys.ESCAPE});
|
||||
expect(view.close).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("modal view closes click on close", function () {
|
||||
spyOn(view, "close");
|
||||
view.delegateEvents();
|
||||
var $closeButton = view.$el.find("button.close");
|
||||
expect($closeButton.length).toBe(1);
|
||||
expect(view.close).not.toHaveBeenCalled();
|
||||
$closeButton.trigger('click');
|
||||
expect(view.close).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -23,6 +23,7 @@
|
||||
language_proficiencies: [],
|
||||
requires_parental_consent: true,
|
||||
profile_image: null,
|
||||
accomplishments_shared: false,
|
||||
default_public_account_fields: []
|
||||
},
|
||||
|
||||
|
||||
8
lms/static/js/student_profile/models/badges_model.js
Normal file
8
lms/static/js/student_profile/models/badges_model.js
Normal file
@@ -0,0 +1,8 @@
|
||||
;(function (define) {
|
||||
'use strict';
|
||||
define(['backbone'], function(Backbone) {
|
||||
|
||||
var BadgesModel = Backbone.Model.extend({});
|
||||
return BadgesModel;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
25
lms/static/js/student_profile/views/badge_list_container.js
Normal file
25
lms/static/js/student_profile/views/badge_list_container.js
Normal file
@@ -0,0 +1,25 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define([
|
||||
'gettext', 'jquery', 'underscore', 'common/js/components/views/paginated_view',
|
||||
'js/student_profile/views/badge_view', 'js/student_profile/views/badge_list_view',
|
||||
'text!templates/student_profile/badge_list.underscore'],
|
||||
function (gettext, $, _, PaginatedView, BadgeView, BadgeListView, BadgeListTemplate) {
|
||||
var BadgeListContainer = PaginatedView.extend({
|
||||
initialize: function (options) {
|
||||
BadgeListContainer.__super__.initialize.call(this, options);
|
||||
this.listView.find_courses_url = options.find_courses_url;
|
||||
this.listView.badgeMeta = options.badgeMeta;
|
||||
this.listView.ownProfile = options.ownProfile;
|
||||
},
|
||||
type: 'badge',
|
||||
itemViewClass: BadgeView,
|
||||
listViewClass: BadgeListView,
|
||||
viewTemplate: BadgeListTemplate,
|
||||
isZeroIndexed: true,
|
||||
paginationLabel: gettext("Accomplishments Pagination")
|
||||
});
|
||||
|
||||
return BadgeListContainer;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
44
lms/static/js/student_profile/views/badge_list_view.js
Normal file
44
lms/static/js/student_profile/views/badge_list_view.js
Normal file
@@ -0,0 +1,44 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define([
|
||||
'gettext', 'jquery', 'underscore', 'common/js/components/views/list', 'js/student_profile/views/badge_view',
|
||||
'text!templates/student_profile/badge_placeholder.underscore'],
|
||||
function (gettext, $, _, ListView, BadgeView, badgePlaceholder) {
|
||||
var BadgeListView = ListView.extend({
|
||||
tagName: 'div',
|
||||
template: _.template(badgePlaceholder),
|
||||
renderCollection: function () {
|
||||
this.$el.empty();
|
||||
var self = this;
|
||||
var row;
|
||||
// Split into two columns.
|
||||
this.collection.each(function (badge, index) {
|
||||
if (index % 2 === 0) {
|
||||
row = $('<div class="row">');
|
||||
this.$el.append(row);
|
||||
}
|
||||
var item = new BadgeView({
|
||||
model: badge,
|
||||
badgeMeta: this.badgeMeta,
|
||||
ownProfile: this.ownProfile
|
||||
}).render().el;
|
||||
row.append(item);
|
||||
this.itemViews.push(item);
|
||||
}, this);
|
||||
// Placeholder must always be at the end, and may need a new row.
|
||||
if (!this.collection.hasNextPage()) {
|
||||
// find_courses_url set by BadgeListContainer during initialization.
|
||||
var placeholder = this.template({find_courses_url: self.find_courses_url});
|
||||
if (this.collection.length % 2 === 0) {
|
||||
row = $('<div class="row">');
|
||||
this.$el.append(row);
|
||||
}
|
||||
row.append(placeholder);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
return BadgeListView;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
42
lms/static/js/student_profile/views/badge_view.js
Normal file
42
lms/static/js/student_profile/views/badge_view.js
Normal file
@@ -0,0 +1,42 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define(['gettext', 'jquery', 'underscore', 'backbone', 'moment',
|
||||
'text!templates/student_profile/badge.underscore',
|
||||
'js/student_profile/views/share_modal_view'],
|
||||
function (gettext, $, _, Backbone, Moment, badgeTemplate, ShareModalView) {
|
||||
|
||||
var BadgeView = Backbone.View.extend({
|
||||
initialize: function(options) {
|
||||
this.context = _.extend(this.options.model.toJSON(), {
|
||||
'created': new Moment(this.options.model.toJSON().created),
|
||||
'ownProfile': options.ownProfile,
|
||||
'badgeMeta': options.badgeMeta
|
||||
});
|
||||
},
|
||||
attributes: {
|
||||
'class': 'badge-display'
|
||||
},
|
||||
template: _.template(badgeTemplate),
|
||||
events: {
|
||||
'click .share-button': 'createModal'
|
||||
},
|
||||
createModal: function() {
|
||||
var modal = new ShareModalView({
|
||||
model: new Backbone.Model(this.context),
|
||||
shareButton: this.shareButton
|
||||
});
|
||||
modal.$el.hide();
|
||||
modal.render();
|
||||
$('body').append(modal.$el);
|
||||
modal.$el.fadeIn('short', 'swing', _.bind(modal.ready, modal));
|
||||
},
|
||||
render: function () {
|
||||
this.$el.html(this.template(this.context));
|
||||
this.shareButton = this.$el.find('.share-button');
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
return BadgeView;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
@@ -7,11 +7,15 @@
|
||||
'js/views/fields',
|
||||
'js/student_profile/views/learner_profile_fields',
|
||||
'js/student_profile/views/learner_profile_view',
|
||||
'js/student_profile/models/badges_model',
|
||||
'common/js/components/collections/paging_collection',
|
||||
'js/student_profile/views/badge_list_container',
|
||||
'js/student_account/views/account_settings_fields',
|
||||
'js/views/message_banner',
|
||||
'string_utils'
|
||||
], function (gettext, $, _, Backbone, Logger, AccountSettingsModel, AccountPreferencesModel, FieldsView,
|
||||
LearnerProfileFieldsView, LearnerProfileView, AccountSettingsFieldViews, MessageBannerView) {
|
||||
LearnerProfileFieldsView, LearnerProfileView, BadgeModel, PagingCollection, BadgeListContainer,
|
||||
AccountSettingsFieldViews, MessageBannerView) {
|
||||
|
||||
return function (options) {
|
||||
|
||||
@@ -121,6 +125,21 @@
|
||||
})
|
||||
];
|
||||
|
||||
var badgeCollection = new PagingCollection();
|
||||
badgeCollection.url = options.badges_api_url;
|
||||
|
||||
var badgeListContainer = new BadgeListContainer({
|
||||
'attributes': {'class': 'badge-set-display'},
|
||||
'collection': badgeCollection,
|
||||
'find_courses_url': options.find_courses_url,
|
||||
'ownProfile': options.own_profile,
|
||||
'badgeMeta': {
|
||||
'badges_logo': options.badges_logo,
|
||||
'backpack_ui_img': options.backpack_ui_img,
|
||||
'badges_icon': options.badges_icon
|
||||
}
|
||||
});
|
||||
|
||||
var learnerProfileView = new LearnerProfileView({
|
||||
el: learnerProfileElement,
|
||||
ownProfile: options.own_profile,
|
||||
@@ -131,7 +150,8 @@
|
||||
profileImageFieldView: profileImageFieldView,
|
||||
usernameFieldView: usernameFieldView,
|
||||
sectionOneFieldViews: sectionOneFieldViews,
|
||||
sectionTwoFieldViews: sectionTwoFieldViews
|
||||
sectionTwoFieldViews: sectionTwoFieldViews,
|
||||
badgeListContainer: badgeListContainer
|
||||
});
|
||||
|
||||
var getProfileVisibility = function() {
|
||||
@@ -164,7 +184,8 @@
|
||||
return {
|
||||
accountSettingsModel: accountSettingsModel,
|
||||
accountPreferencesModel: accountPreferencesModel,
|
||||
learnerProfileView: learnerProfileView
|
||||
learnerProfileView: learnerProfileView,
|
||||
badgeListContainer: badgeListContainer
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define([
|
||||
'gettext', 'jquery', 'underscore', 'backbone', 'text!templates/student_profile/learner_profile.underscore'],
|
||||
function (gettext, $, _, Backbone, learnerProfileTemplate) {
|
||||
'gettext', 'jquery', 'underscore', 'backbone',
|
||||
'common/js/components/views/tabbed_view',
|
||||
'js/student_profile/views/section_two_tab',
|
||||
'text!templates/student_profile/learner_profile.underscore'],
|
||||
function (gettext, $, _, Backbone, TabbedView, SectionTwoTab, learnerProfileTemplate) {
|
||||
|
||||
var LearnerProfileView = Backbone.View.extend({
|
||||
|
||||
initialize: function () {
|
||||
_.bindAll(this, 'showFullProfile', 'render', 'renderFields', 'showLoadingError');
|
||||
this.listenTo(this.options.preferencesModel, "change:" + 'account_privacy', this.render);
|
||||
var Router = Backbone.Router.extend({
|
||||
routes: {":about_me": "loadTab", ":accomplishments": "loadTab"}
|
||||
});
|
||||
|
||||
this.router = new Router();
|
||||
this.firstRender = true;
|
||||
},
|
||||
|
||||
template: _.template(learnerProfileTemplate),
|
||||
|
||||
showFullProfile: function () {
|
||||
var isAboveMinimumAge = this.options.accountSettingsModel.isAboveMinimumAge();
|
||||
@@ -20,13 +31,73 @@
|
||||
}
|
||||
},
|
||||
|
||||
setActiveTab: function(tab) {
|
||||
// This tab may not actually exist.
|
||||
if (this.tabbedView.getTabMeta(tab).tab) {
|
||||
this.tabbedView.setActiveTab(tab);
|
||||
}
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(_.template(learnerProfileTemplate)({
|
||||
username: this.options.accountSettingsModel.get('username'),
|
||||
ownProfile: this.options.ownProfile,
|
||||
showFullProfile: this.showFullProfile()
|
||||
var self = this;
|
||||
|
||||
this.sectionTwoView = new SectionTwoTab({
|
||||
viewList: this.options.sectionTwoFieldViews,
|
||||
showFullProfile: this.showFullProfile,
|
||||
ownProfile: this.options.ownProfile
|
||||
});
|
||||
|
||||
var tabs = [
|
||||
{view: this.sectionTwoView, title: gettext("About Me"), url: "about_me"}
|
||||
];
|
||||
|
||||
this.$el.html(this.template({
|
||||
username: self.options.accountSettingsModel.get('username'),
|
||||
ownProfile: self.options.ownProfile,
|
||||
showFullProfile: self.showFullProfile()
|
||||
}));
|
||||
this.renderFields();
|
||||
|
||||
if (this.showFullProfile() && (this.options.accountSettingsModel.get('accomplishments_shared'))) {
|
||||
tabs.push({
|
||||
view: this.options.badgeListContainer,
|
||||
title: gettext("Accomplishments"),
|
||||
url: "accomplishments"
|
||||
});
|
||||
this.options.badgeListContainer.collection.fetch().done(function () {
|
||||
self.options.badgeListContainer.render();
|
||||
}).error(function () {
|
||||
self.options.badgeListContainer.renderError();
|
||||
});
|
||||
}
|
||||
this.tabbedView = new TabbedView({
|
||||
tabs: tabs,
|
||||
router: this.router,
|
||||
viewLabel: gettext("Profile")
|
||||
});
|
||||
|
||||
this.tabbedView.render();
|
||||
|
||||
if (tabs.length === 1) {
|
||||
// If the tab is unambiguous, don't display the tab interface.
|
||||
this.tabbedView.$el.find('.page-content-nav').hide();
|
||||
}
|
||||
|
||||
this.$el.find('.account-settings-container').append(this.tabbedView.el);
|
||||
|
||||
if (this.firstRender) {
|
||||
this.router.on("route:loadTab", _.bind(this.setActiveTab, this));
|
||||
Backbone.history.start();
|
||||
this.firstRender = false;
|
||||
// Load from history.
|
||||
this.router.navigate((Backbone.history.getFragment() || 'about_me'), {trigger: true});
|
||||
} else {
|
||||
// Restart the router so the tab will be brought up anew.
|
||||
Backbone.history.stop();
|
||||
Backbone.history.start();
|
||||
}
|
||||
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
@@ -54,9 +125,6 @@
|
||||
view.$('.profile-section-one-fields').append(fieldView.render().el);
|
||||
});
|
||||
|
||||
_.each(this.options.sectionTwoFieldViews, function (fieldView) {
|
||||
view.$('.profile-section-two-fields').append(fieldView.render().el);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user