Merge pull request #7315 from edx/db/creative-commons
Allow custom licensing for course content
This commit is contained in:
@@ -5,6 +5,7 @@ import mock
|
||||
from mock import patch
|
||||
import shutil
|
||||
import lxml.html
|
||||
from lxml import etree
|
||||
import ddt
|
||||
|
||||
from datetime import timedelta
|
||||
@@ -1836,6 +1837,44 @@ class RerunCourseTest(ContentStoreTestCase):
|
||||
self.assertEqual(len(rerun_state.message), CourseRerunState.MAX_MESSAGE_LENGTH)
|
||||
|
||||
|
||||
class ContentLicenseTest(ContentStoreTestCase):
|
||||
"""
|
||||
Tests around content licenses
|
||||
"""
|
||||
def test_course_license_export(self):
|
||||
content_store = contentstore()
|
||||
root_dir = path(mkdtemp_clean())
|
||||
self.course.license = "creative-commons: BY SA"
|
||||
self.store.update_item(self.course, None)
|
||||
export_course_to_xml(self.store, content_store, self.course.id, root_dir, 'test_license')
|
||||
fname = "{block}.xml".format(block=self.course.scope_ids.usage_id.block_id)
|
||||
run_file_path = root_dir / "test_license" / "course" / fname
|
||||
run_xml = etree.parse(run_file_path.open())
|
||||
self.assertEqual(run_xml.getroot().get("license"), "creative-commons: BY SA")
|
||||
|
||||
def test_video_license_export(self):
|
||||
content_store = contentstore()
|
||||
root_dir = path(mkdtemp_clean())
|
||||
video_descriptor = ItemFactory.create(
|
||||
parent_location=self.course.location, category='video',
|
||||
license="all-rights-reserved"
|
||||
)
|
||||
export_course_to_xml(self.store, content_store, self.course.id, root_dir, 'test_license')
|
||||
fname = "{block}.xml".format(block=video_descriptor.scope_ids.usage_id.block_id)
|
||||
video_file_path = root_dir / "test_license" / "video" / fname
|
||||
video_xml = etree.parse(video_file_path.open())
|
||||
self.assertEqual(video_xml.getroot().get("license"), "all-rights-reserved")
|
||||
|
||||
def test_license_import(self):
|
||||
course_items = import_course_from_xml(
|
||||
self.store, self.user.id, TEST_DATA_DIR, ['toy'], create_if_not_present=True
|
||||
)
|
||||
course = course_items[0]
|
||||
self.assertEqual(course.license, "creative-commons: BY")
|
||||
videos = self.store.get_items(course.id, qualifiers={'category': 'video'})
|
||||
self.assertEqual(videos[0].license, "all-rights-reserved")
|
||||
|
||||
|
||||
class EntryPageTestCase(TestCase):
|
||||
"""
|
||||
Tests entry pages that aren't specific to a course.
|
||||
|
||||
@@ -63,7 +63,7 @@ CONTAINER_TEMPATES = [
|
||||
"editor-mode-button", "upload-dialog", "image-modal",
|
||||
"add-xblock-component", "add-xblock-component-button", "add-xblock-component-menu",
|
||||
"add-xblock-component-menu-problem", "xblock-string-field-editor", "publish-xblock", "publish-history",
|
||||
"unit-outline", "container-message"
|
||||
"unit-outline", "container-message", "license-selector",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ from xmodule.exceptions import NotFoundError, ProcessingError
|
||||
from xmodule.library_tools import LibraryToolsService
|
||||
from xmodule.services import SettingsService
|
||||
from xmodule.modulestore.django import modulestore, ModuleI18nService
|
||||
from xmodule.mixin import wrap_with_license
|
||||
from opaque_keys.edx.keys import UsageKey
|
||||
from opaque_keys.edx.locator import LibraryUsageLocator
|
||||
from xmodule.x_module import ModuleSystem
|
||||
@@ -170,6 +171,10 @@ def _preview_module_system(request, descriptor, field_data):
|
||||
_studio_wrap_xblock,
|
||||
]
|
||||
|
||||
if settings.FEATURES.get("LICENSING", False):
|
||||
# stick the license wrapper in front
|
||||
wrappers.insert(0, wrap_with_license)
|
||||
|
||||
descriptor.runtime._services['studio_user_permissions'] = StudioPermissionsService(request) # pylint: disable=protected-access
|
||||
|
||||
return PreviewModuleSystem(
|
||||
|
||||
@@ -42,6 +42,7 @@ class CourseDetails(object):
|
||||
self.overview = "" # html to render as the overview
|
||||
self.intro_video = None # a video pointer
|
||||
self.effort = None # int hours/week
|
||||
self.license = "all-rights-reserved" # default course license is all rights reserved
|
||||
self.course_image_name = ""
|
||||
self.course_image_asset_path = "" # URL of the course image
|
||||
self.pre_requisite_courses = [] # pre-requisite courses
|
||||
@@ -79,6 +80,8 @@ class CourseDetails(object):
|
||||
course_details.pre_requisite_courses = descriptor.pre_requisite_courses
|
||||
course_details.course_image_name = descriptor.course_image
|
||||
course_details.course_image_asset_path = course_image_url(descriptor)
|
||||
# Default course license is "All Rights Reserved"
|
||||
course_details.license = getattr(descriptor, "license", "all-rights-reserved")
|
||||
|
||||
for attribute in ABOUT_ATTRIBUTES:
|
||||
value = cls._fetch_about_attribute(course_key, attribute)
|
||||
@@ -173,6 +176,10 @@ class CourseDetails(object):
|
||||
descriptor.pre_requisite_courses = jsondict['pre_requisite_courses']
|
||||
dirty = True
|
||||
|
||||
if 'license' in jsondict:
|
||||
descriptor.license = jsondict['license']
|
||||
dirty = True
|
||||
|
||||
if dirty:
|
||||
module_store.update_item(descriptor, user.id)
|
||||
|
||||
|
||||
@@ -339,3 +339,4 @@ if FEATURES['ENABLE_COURSEWARE_INDEX'] or FEATURES['ENABLE_LIBRARY_INDEX']:
|
||||
SEARCH_ENGINE = "search.elastic.ElasticSearchEngine"
|
||||
|
||||
XBLOCK_SETTINGS = ENV_TOKENS.get('XBLOCK_SETTINGS', {})
|
||||
XBLOCK_SETTINGS.setdefault("VideoDescriptor", {})["licensing_enabled"] = FEATURES.get("LICENSING", False)
|
||||
|
||||
@@ -71,6 +71,9 @@ FEATURES['ENABLE_EDXNOTES'] = True
|
||||
# Enable teams feature
|
||||
FEATURES['ENABLE_TEAMS'] = True
|
||||
|
||||
# Enable custom content licensing
|
||||
FEATURES['LICENSING'] = True
|
||||
|
||||
########################### Entrance Exams #################################
|
||||
FEATURES['ENTRANCE_EXAMS'] = True
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ from lms.djangoapps.lms_xblock.mixin import LmsBlockMixin
|
||||
from cms.lib.xblock.authoring_mixin import AuthoringMixin
|
||||
import dealer.git
|
||||
from xmodule.modulestore.edit_info import EditInfoMixin
|
||||
from xmodule.mixin import LicenseMixin
|
||||
|
||||
############################ FEATURE CONFIGURATION #############################
|
||||
STUDIO_NAME = "Studio"
|
||||
@@ -142,6 +143,9 @@ FEATURES = {
|
||||
# Toggle course entrance exams feature
|
||||
'ENTRANCE_EXAMS': False,
|
||||
|
||||
# Toggle platform-wide course licensing
|
||||
'LICENSING': False,
|
||||
|
||||
# Enable the courseware search functionality
|
||||
'ENABLE_COURSEWARE_INDEX': False,
|
||||
|
||||
@@ -312,6 +316,7 @@ from xmodule.modulestore.inheritance import InheritanceMixin
|
||||
from xmodule.modulestore import prefer_xmodules
|
||||
from xmodule.x_module import XModuleMixin
|
||||
|
||||
# These are the Mixins that should be added to every XBlock.
|
||||
# This should be moved into an XBlock Runtime/Application object
|
||||
# once the responsibility of XBlock creation is moved out of modulestore - cpennington
|
||||
XBLOCK_MIXINS = (
|
||||
@@ -465,6 +470,7 @@ PIPELINE_CSS = {
|
||||
'style-main': {
|
||||
'source_filenames': [
|
||||
'sass/studio-main.css',
|
||||
'css/edx-cc.css',
|
||||
],
|
||||
'output_filename': 'css/studio-main.css',
|
||||
},
|
||||
@@ -942,3 +948,9 @@ ELASTIC_FIELD_MAPPINGS = {
|
||||
"type": "date"
|
||||
}
|
||||
}
|
||||
|
||||
XBLOCK_SETTINGS = {
|
||||
"VideoDescriptor": {
|
||||
"licensing_enabled": FEATURES.get("LICENSING", False)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,9 @@ FEATURES['MILESTONES_APP'] = True
|
||||
################################ ENTRANCE EXAMS ################################
|
||||
FEATURES['ENTRANCE_EXAMS'] = True
|
||||
|
||||
################################ COURSE LICENSES ################################
|
||||
FEATURES['LICENSING'] = True
|
||||
|
||||
################################ SEARCH INDEX ################################
|
||||
FEATURES['ENABLE_COURSEWARE_INDEX'] = True
|
||||
FEATURES['ENABLE_LIBRARY_INDEX'] = True
|
||||
|
||||
@@ -227,6 +227,7 @@ define([
|
||||
"js/spec/models/explicit_url_spec",
|
||||
"js/spec/models/xblock_info_spec",
|
||||
"js/spec/models/xblock_validation_spec",
|
||||
"js/spec/models/license_spec",
|
||||
|
||||
"js/spec/utils/drag_and_drop_spec",
|
||||
"js/spec/utils/handle_iframe_binding_spec",
|
||||
@@ -247,6 +248,7 @@ define([
|
||||
"js/spec/views/xblock_editor_spec",
|
||||
"js/spec/views/xblock_string_field_editor_spec",
|
||||
"js/spec/views/xblock_validation_spec",
|
||||
"js/spec/views/license_spec",
|
||||
|
||||
"js/spec/views/utils/view_utils_spec",
|
||||
|
||||
|
||||
100
cms/static/js/models/license.js
Normal file
100
cms/static/js/models/license.js
Normal file
@@ -0,0 +1,100 @@
|
||||
define(["backbone", "underscore"], function(Backbone, _) {
|
||||
var LicenseModel = Backbone.Model.extend({
|
||||
defaults: {
|
||||
"type": null,
|
||||
"options": {},
|
||||
"custom": false // either `false`, or a string
|
||||
},
|
||||
|
||||
initialize: function(attributes) {
|
||||
if(attributes && attributes.asString) {
|
||||
this.setFromString(attributes.asString);
|
||||
this.unset("asString");
|
||||
}
|
||||
},
|
||||
|
||||
toString: function() {
|
||||
var custom = this.get("custom");
|
||||
if (custom) {
|
||||
return custom;
|
||||
}
|
||||
|
||||
var type = this.get("type"),
|
||||
options = this.get("options");
|
||||
|
||||
if (_.isEmpty(options)) {
|
||||
return type || "";
|
||||
}
|
||||
|
||||
// options are where it gets tricky
|
||||
var optionStrings = _.map(options, function (value, key) {
|
||||
if(_.isBoolean(value)) {
|
||||
return value ? key : null
|
||||
} else {
|
||||
return key + "=" + value
|
||||
}
|
||||
});
|
||||
// filter out nulls
|
||||
optionStrings = _.filter(optionStrings, _.identity);
|
||||
// build license string and return
|
||||
return type + ": " + optionStrings.join(" ");
|
||||
},
|
||||
|
||||
setFromString: function(string, options) {
|
||||
if (!string) {
|
||||
// reset to defaults
|
||||
return this.set(this.defaults, options);
|
||||
}
|
||||
|
||||
var colonIndex = string.indexOf(":"),
|
||||
spaceIndex = string.indexOf(" ");
|
||||
|
||||
// a string without a colon could be a custom license, or a license
|
||||
// type without options
|
||||
if (colonIndex == -1) {
|
||||
if (spaceIndex == -1) {
|
||||
// if there's no space, it's a license type without options
|
||||
return this.set({
|
||||
"type": string,
|
||||
"options": {},
|
||||
"custom": false
|
||||
}, options);
|
||||
} else {
|
||||
// if there is a space, it's a custom license
|
||||
return this.set({
|
||||
"type": null,
|
||||
"options": {},
|
||||
"custom": string
|
||||
}, options);
|
||||
}
|
||||
}
|
||||
|
||||
// there is a colon, which indicates a license type with options.
|
||||
var type = string.substring(0, colonIndex),
|
||||
optionsObj = {},
|
||||
optionsString = string.substring(colonIndex + 1);
|
||||
|
||||
_.each(optionsString.split(" "), function(optionString) {
|
||||
if (_.isEmpty(optionString)) {
|
||||
return;
|
||||
}
|
||||
var eqIndex = optionString.indexOf("=");
|
||||
if(eqIndex == -1) {
|
||||
// this is a boolean flag
|
||||
optionsObj[optionString] = true;
|
||||
} else {
|
||||
// this is a key-value pair
|
||||
var optionKey = optionString.substring(0, eqIndex);
|
||||
var optionVal = optionString.substring(eqIndex + 1);
|
||||
optionsObj[optionKey] = optionVal;
|
||||
}
|
||||
});
|
||||
|
||||
return this.set({
|
||||
"type": type, "options": optionsObj, "custom": false,
|
||||
}, options);
|
||||
}
|
||||
});
|
||||
|
||||
return LicenseModel;
|
||||
});
|
||||
@@ -15,6 +15,7 @@ var CourseDetails = Backbone.Model.extend({
|
||||
overview: "",
|
||||
intro_video: null,
|
||||
effort: null, // an int or null,
|
||||
license: null,
|
||||
course_image_name: '', // the filename
|
||||
course_image_asset_path: '', // the full URL (/c4x/org/course/num/asset/filename)
|
||||
pre_requisite_courses: [],
|
||||
|
||||
72
cms/static/js/spec/models/license_spec.js
Normal file
72
cms/static/js/spec/models/license_spec.js
Normal file
@@ -0,0 +1,72 @@
|
||||
define(["js/models/license"], function(LicenseModel) {
|
||||
describe("License model constructor", function() {
|
||||
it("accepts no arguments", function() {
|
||||
var model = new LicenseModel()
|
||||
expect(model.get("type")).toBeNull();
|
||||
expect(model.get("options")).toEqual({});
|
||||
expect(model.get("custom")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("accepts normal arguments", function() {
|
||||
var model = new LicenseModel({
|
||||
"type": "creative-commons",
|
||||
"options": {"fake-boolean": true, "version": "your momma"}
|
||||
});
|
||||
expect(model.get("type")).toEqual("creative-commons");
|
||||
expect(model.get("options")).toEqual({"fake-boolean": true, "version": "your momma"});
|
||||
})
|
||||
|
||||
it("accepts a license string argument", function() {
|
||||
var model = new LicenseModel({"asString": "all-rights-reserved"});
|
||||
expect(model.get("type")).toEqual("all-rights-reserved");
|
||||
expect(model.get("options")).toEqual({});
|
||||
expect(model.get("custom")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("accepts a custom license argument", function() {
|
||||
var model = new LicenseModel({"asString": "Mozilla Public License 2.0"})
|
||||
expect(model.get("type")).toBeNull();
|
||||
expect(model.get("options")).toEqual({});
|
||||
expect(model.get("custom")).toEqual("Mozilla Public License 2.0");
|
||||
});
|
||||
});
|
||||
|
||||
describe("License model", function() {
|
||||
beforeEach(function() {
|
||||
this.model = new LicenseModel();
|
||||
});
|
||||
|
||||
it("can parse license strings", function() {
|
||||
this.model.setFromString("creative-commons: BY")
|
||||
expect(this.model.get("type")).toEqual("creative-commons")
|
||||
expect(this.model.get("options")).toEqual({"BY": true})
|
||||
expect(this.model.get("custom")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("can stringify a null license", function() {
|
||||
expect(this.model.toString()).toEqual("");
|
||||
});
|
||||
|
||||
it("can stringify a simple license", function() {
|
||||
this.model.set("type", "foobie thinger");
|
||||
expect(this.model.toString()).toEqual("foobie thinger");
|
||||
});
|
||||
|
||||
it("can stringify a license with options", function() {
|
||||
this.model.set({
|
||||
"type": "abc",
|
||||
"options": {"ping": "pong", "bing": true, "buzz": true, "beep": false}}
|
||||
);
|
||||
expect(this.model.toString()).toEqual("abc: ping=pong bing buzz");
|
||||
});
|
||||
|
||||
it("can stringify a custom license", function() {
|
||||
this.model.set({
|
||||
"type": "doesn't matter",
|
||||
"options": {"ignore": "me"},
|
||||
"custom": "this is my super cool license"
|
||||
});
|
||||
expect(this.model.toString()).toEqual("this is my super cool license");
|
||||
});
|
||||
})
|
||||
})
|
||||
143
cms/static/js/spec/views/license_spec.js
Normal file
143
cms/static/js/spec/views/license_spec.js
Normal file
@@ -0,0 +1,143 @@
|
||||
define(["js/views/license", "js/models/license", "js/common_helpers/template_helpers"],
|
||||
function(LicenseView, LicenseModel, TemplateHelpers) {
|
||||
describe("License view", function() {
|
||||
|
||||
beforeEach(function() {
|
||||
TemplateHelpers.installTemplate("license-selector", true);
|
||||
this.model = new LicenseModel();
|
||||
this.view = new LicenseView({model: this.model});
|
||||
});
|
||||
|
||||
it("renders with no license", function() {
|
||||
this.view.render();
|
||||
expect(this.view.$("li[data-license=all-rights-reserved] button"))
|
||||
.toHaveText("All Rights Reserved");
|
||||
expect(this.view.$("li[data-license=all-rights-reserved] button"))
|
||||
.not.toHaveClass("is-selected");
|
||||
expect(this.view.$("li[data-license=creative-commons] button"))
|
||||
.toHaveText("Creative Commons");
|
||||
expect(this.view.$("li[data-license=creative-commons] button"))
|
||||
.not.toHaveClass("is-selected");
|
||||
});
|
||||
|
||||
it("renders with the right license selected", function() {
|
||||
this.model.set("type", "all-rights-reserved");
|
||||
expect(this.view.$("li[data-license=all-rights-reserved] button"))
|
||||
.toHaveClass("is-selected");
|
||||
expect(this.view.$("li[data-license=creative-commons] button"))
|
||||
.not.toHaveClass("is-selected");
|
||||
});
|
||||
|
||||
it("switches license type on click", function() {
|
||||
var arrBtn = this.view.$("li[data-license=all-rights-reserved] button");
|
||||
expect(this.model.get("type")).toBeNull();
|
||||
arrBtn.click();
|
||||
expect(this.model.get("type")).toEqual("all-rights-reserved");
|
||||
// view has re-rendered, so get a new reference to the button
|
||||
arrBtn = this.view.$("li[data-license=all-rights-reserved] button");
|
||||
expect(arrBtn).toHaveClass("is-selected");
|
||||
// now switch to creative commons
|
||||
var ccBtn = this.view.$("li[data-license=creative-commons] button");
|
||||
ccBtn.click();
|
||||
expect(this.model.get("type")).toEqual("creative-commons");
|
||||
// update references again
|
||||
arrBtn = this.view.$("li[data-license=all-rights-reserved] button");
|
||||
ccBtn = this.view.$("li[data-license=creative-commons] button");
|
||||
expect(arrBtn).not.toHaveClass("is-selected");
|
||||
expect(ccBtn).toHaveClass("is-selected");
|
||||
});
|
||||
|
||||
it("sets default license options when switching license types", function() {
|
||||
expect(this.model.get("options")).toEqual({});
|
||||
var ccBtn = this.view.$("li[data-license=creative-commons] button");
|
||||
ccBtn.click()
|
||||
expect(this.model.get("options")).toEqual(
|
||||
{"ver": "4.0", "BY": true, "NC": true, "ND": true, "SA": false}
|
||||
);
|
||||
var arrBtn = this.view.$("li[data-license=all-rights-reserved] button");
|
||||
arrBtn.click()
|
||||
expect(this.model.get("options")).toEqual({});
|
||||
});
|
||||
|
||||
it("renders license options", function() {
|
||||
this.model.set({"type": "creative-commons"})
|
||||
expect(this.view.$("ul.license-options li[data-option=BY]"))
|
||||
.toContainText("Attribution");
|
||||
expect(this.view.$("ul.license-options li[data-option=NC]"))
|
||||
.toContainText("Noncommercial");
|
||||
expect(this.view.$("ul.license-options li[data-option=ND]"))
|
||||
.toContainText("No Derivatives");
|
||||
expect(this.view.$("ul.license-options li[data-option=SA]"))
|
||||
.toContainText("Share Alike");
|
||||
expect(this.view.$("ul.license-options li").length).toEqual(4);
|
||||
});
|
||||
|
||||
it("toggles boolean options on click", function() {
|
||||
this.view.$("li[data-license=creative-commons] button").click();
|
||||
expect(this.model.get("options")).toEqual(
|
||||
{"ver": "4.0", "BY": true, "NC": true, "ND": true, "SA": false}
|
||||
);
|
||||
// toggle NC option
|
||||
this.view.$("li[data-option=NC]").click();
|
||||
expect(this.model.get("options")).toEqual(
|
||||
{"ver": "4.0", "BY": true, "NC": false, "ND": true, "SA": false}
|
||||
);
|
||||
});
|
||||
|
||||
it("doesn't toggle disabled options", function() {
|
||||
this.view.$("li[data-license=creative-commons] button").click();
|
||||
expect(this.model.get("options")).toEqual(
|
||||
{"ver": "4.0", "BY": true, "NC": true, "ND": true, "SA": false}
|
||||
);
|
||||
var BY = this.view.$("li[data-option=BY]");
|
||||
expect(BY).toHaveClass("is-disabled");
|
||||
// try to toggle BY option
|
||||
BY.click()
|
||||
// no change
|
||||
expect(this.model.get("options")).toEqual(
|
||||
{"ver": "4.0", "BY": true, "NC": true, "ND": true, "SA": false}
|
||||
);
|
||||
});
|
||||
|
||||
it("doesn't allow simultaneous conflicting options", function() {
|
||||
this.view.$("li[data-license=creative-commons] button").click();
|
||||
expect(this.model.get("options")).toEqual(
|
||||
{"ver": "4.0", "BY": true, "NC": true, "ND": true, "SA": false}
|
||||
);
|
||||
// SA and ND conflict
|
||||
var SA = this.view.$("li[data-option=SA]");
|
||||
// try to turn on SA option
|
||||
SA.click()
|
||||
// ND should no longer be selected
|
||||
expect(this.model.get("options")).toEqual(
|
||||
{"ver": "4.0", "BY": true, "NC": true, "ND": false, "SA": true}
|
||||
);
|
||||
|
||||
// try to turn on ND option
|
||||
ND = this.view.$("li[data-option=ND]");
|
||||
ND.click();
|
||||
expect(this.model.get("options")).toEqual(
|
||||
{"ver": "4.0", "BY": true, "NC": true, "ND": true, "SA": false}
|
||||
);
|
||||
});
|
||||
|
||||
it("has no preview by default", function () {
|
||||
this.view.render();
|
||||
expect(this.view.$(".license-preview").length).toEqual(0)
|
||||
this.view.$("li[data-license=creative-commons] button").click();
|
||||
expect(this.view.$(".license-preview").length).toEqual(0)
|
||||
});
|
||||
|
||||
it("displays a preview if showPreview is true", function() {
|
||||
this.view = new LicenseView({model: this.model, showPreview: true});
|
||||
this.view.render()
|
||||
expect(this.view.$(".license-preview").length).toEqual(1)
|
||||
// Expect default text to be "All Rights Reserved"
|
||||
expect(this.view.$(".license-preview")).toContainText("All Rights Reserved");
|
||||
this.view.$("li[data-license=creative-commons] button").click();
|
||||
expect(this.view.$(".license-preview").length).toEqual(1)
|
||||
expect(this.view.$(".license-preview")).toContainText("Some Rights Reserved");
|
||||
});
|
||||
|
||||
})
|
||||
})
|
||||
@@ -29,7 +29,8 @@ define([
|
||||
course_image_asset_path : '',
|
||||
pre_requisite_courses : [],
|
||||
entrance_exam_enabled : '',
|
||||
entrance_exam_minimum_score_pct: '50'
|
||||
entrance_exam_minimum_score_pct: '50',
|
||||
license: null
|
||||
},
|
||||
mockSettingsPage = readFixtures('mock/mock-settings-page.underscore');
|
||||
|
||||
|
||||
145
cms/static/js/views/license.js
Normal file
145
cms/static/js/views/license.js
Normal file
@@ -0,0 +1,145 @@
|
||||
define(["js/views/baseview", "underscore"], function(BaseView, _) {
|
||||
var defaultLicenseInfo = {
|
||||
"all-rights-reserved": {
|
||||
"name": gettext("All Rights Reserved"),
|
||||
"tooltip": gettext("You reserve all rights for your work")
|
||||
},
|
||||
"creative-commons": {
|
||||
"name": gettext("Creative Commons"),
|
||||
"tooltip": gettext("You waive some rights for your work, such that others can use it too"),
|
||||
"url": "https://creativecommons.org/about",
|
||||
"options": {
|
||||
"ver": {
|
||||
"name": gettext("Version"),
|
||||
"type": "string",
|
||||
"default": "4.0",
|
||||
},
|
||||
"BY": {
|
||||
"name": gettext("Attribution"),
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"help": gettext("Allow others to copy, distribute, display and perform your copyrighted work but only if they give credit the way you request. Currently, this option is required."),
|
||||
"disabled": true,
|
||||
},
|
||||
"NC": {
|
||||
"name": gettext("Noncommercial"),
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"help": gettext("Allow others to copy, distribute, display and perform your work - and derivative works based upon it - but for noncommercial purposes only."),
|
||||
},
|
||||
"ND": {
|
||||
"name": gettext("No Derivatives"),
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"help": gettext("Allow others to copy, distribute, display and perform only verbatim copies of your work, not derivative works based upon it. This option is incompatible with \"Share Alike\"."),
|
||||
"conflictsWith": ["SA"]
|
||||
},
|
||||
"SA": {
|
||||
"name": gettext("Share Alike"),
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"help": gettext("Allow others to distribute derivative works only under a license identical to the license that governs your work. This option is incompatible with \"No Derivatives\"."),
|
||||
"conflictsWith": ["ND"]
|
||||
}
|
||||
},
|
||||
"option_order": ["BY", "NC", "ND", "SA"]
|
||||
}
|
||||
}
|
||||
|
||||
var LicenseView = BaseView.extend({
|
||||
events: {
|
||||
"click ul.license-types li button" : "onLicenseClick",
|
||||
"click ul.license-options li": "onOptionClick"
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
this.licenseInfo = options.licenseInfo || defaultLicenseInfo;
|
||||
this.showPreview = !!options.showPreview; // coerce to boolean
|
||||
this.template = this.loadTemplate("license-selector");
|
||||
|
||||
// Rerender when the model changes
|
||||
this.listenTo(this.model, 'change', this.render);
|
||||
this.render();
|
||||
},
|
||||
|
||||
getDefaultOptionsForLicenseType: function(licenseType) {
|
||||
if (!this.licenseInfo[licenseType]) {
|
||||
// custom license type, no options
|
||||
return {};
|
||||
}
|
||||
if (!this.licenseInfo[licenseType].options) {
|
||||
// defined license type without options
|
||||
return {};
|
||||
}
|
||||
var defaults = {};
|
||||
_.each(this.licenseInfo[licenseType].options, function(value, key) {
|
||||
defaults[key] = value.default;
|
||||
})
|
||||
return defaults;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.$el.html(this.template({
|
||||
model: this.model.attributes,
|
||||
licenseString: this.model.toString() || "",
|
||||
licenseInfo: this.licenseInfo,
|
||||
showPreview: this.showPreview,
|
||||
previewButton: false,
|
||||
}));
|
||||
return this;
|
||||
},
|
||||
|
||||
onLicenseClick: function(e) {
|
||||
var $li = $(e.srcElement || e.target).closest('li');
|
||||
var licenseType = $li.data("license");
|
||||
|
||||
// Check that we've selected a different license type than what's currently selected
|
||||
if (licenseType != this.model.attributes.type) {
|
||||
this.model.set({
|
||||
"type": licenseType,
|
||||
"options": this.getDefaultOptionsForLicenseType(licenseType)
|
||||
});
|
||||
// Fire the change event manually
|
||||
this.model.trigger("change change:type")
|
||||
}
|
||||
e.preventDefault();
|
||||
},
|
||||
|
||||
onOptionClick: function(e) {
|
||||
var licenseType = this.model.get("type"),
|
||||
licenseOptions = $.extend({}, this.model.get("options")),
|
||||
$li = $(e.srcElement || e.target).closest('li');
|
||||
|
||||
var optionKey = $li.data("option")
|
||||
var licenseInfo = this.licenseInfo[licenseType];
|
||||
var optionInfo = licenseInfo.options[optionKey];
|
||||
if (optionInfo.disabled) {
|
||||
// we're done here
|
||||
return;
|
||||
}
|
||||
var currentOptionValue = licenseOptions[optionKey];
|
||||
if (optionInfo.type === "boolean") {
|
||||
// toggle current value
|
||||
currentOptionValue = !currentOptionValue;
|
||||
licenseOptions[optionKey] = currentOptionValue;
|
||||
}
|
||||
// check for conflicts
|
||||
if (currentOptionValue && optionInfo.conflictsWith) {
|
||||
var conflicts = optionInfo.conflictsWith;
|
||||
for (var i=0; i<conflicts.length; i++) {
|
||||
// Uncheck all conflicts
|
||||
licenseOptions[conflicts[i]] = false;
|
||||
console.log(licenseOptions);
|
||||
}
|
||||
}
|
||||
|
||||
this.model.set({"options": licenseOptions})
|
||||
// Backbone has trouble identifying when objects change, so we'll
|
||||
// fire the change event manually.
|
||||
this.model.trigger("change change:options")
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
});
|
||||
return LicenseView;
|
||||
});
|
||||
@@ -2,10 +2,12 @@ define(
|
||||
[
|
||||
"js/views/baseview", "underscore", "js/models/metadata", "js/views/abstract_editor",
|
||||
"js/models/uploads", "js/views/uploads",
|
||||
"js/models/license", "js/views/license",
|
||||
"js/views/video/transcripts/metadata_videolist",
|
||||
"js/views/video/translations_editor"
|
||||
],
|
||||
function(BaseView, _, MetadataModel, AbstractEditor, FileUpload, UploadDialog, VideoList, VideoTranslations) {
|
||||
function(BaseView, _, MetadataModel, AbstractEditor, FileUpload, UploadDialog,
|
||||
LicenseModel, LicenseView, VideoList, VideoTranslations) {
|
||||
var Metadata = {};
|
||||
|
||||
Metadata.Editor = BaseView.extend({
|
||||
@@ -550,5 +552,32 @@ function(BaseView, _, MetadataModel, AbstractEditor, FileUpload, UploadDialog, V
|
||||
}
|
||||
});
|
||||
|
||||
Metadata.License = AbstractEditor.extend({
|
||||
|
||||
initialize: function(options) {
|
||||
this.licenseModel = new LicenseModel({"asString": this.model.getValue()});
|
||||
this.licenseView = new LicenseView({model: this.licenseModel});
|
||||
|
||||
// Rerender when the license model changes
|
||||
this.listenTo(this.licenseModel, 'change', this.setLicense);
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.licenseView.render().$el.css("display", "inline");
|
||||
this.licenseView.undelegateEvents();
|
||||
this.$el.empty().append(this.licenseView.el);
|
||||
// restore event bindings
|
||||
this.licenseView.delegateEvents();
|
||||
return this;
|
||||
},
|
||||
|
||||
setLicense: function() {
|
||||
this.model.setValue(this.licenseModel.toString());
|
||||
this.render()
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return Metadata;
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
define(["js/views/validation", "codemirror", "underscore", "jquery", "jquery.ui", "js/utils/date_utils", "js/models/uploads",
|
||||
"js/views/uploads", "js/utils/change_on_enter", "jquery.timepicker", "date"],
|
||||
function(ValidatingView, CodeMirror, _, $, ui, DateUtils, FileUploadModel, FileUploadDialog, TriggerChangeEventOnEnter) {
|
||||
"js/views/uploads", "js/utils/change_on_enter", "js/views/license", "js/models/license", "jquery.timepicker", "date"],
|
||||
function(ValidatingView, CodeMirror, _, $, ui, DateUtils, FileUploadModel,
|
||||
FileUploadDialog, TriggerChangeEventOnEnter, LicenseView, LicenseModel) {
|
||||
|
||||
var DetailsView = ValidatingView.extend({
|
||||
// Model class is CMS.Models.Settings.CourseDetails
|
||||
@@ -39,6 +40,14 @@ var DetailsView = ValidatingView.extend({
|
||||
this.listenTo(this.model, 'invalid', this.handleValidationError);
|
||||
this.listenTo(this.model, 'change', this.showNotificationBar);
|
||||
this.selectorToField = _.invert(this.fieldToSelectorMap);
|
||||
// handle license separately, to avoid reimplementing view logic
|
||||
this.licenseModel = new LicenseModel({"asString": this.model.get('license')});
|
||||
this.licenseView = new LicenseView({
|
||||
model: this.licenseModel,
|
||||
el: this.$("#course-license-selector").get(),
|
||||
showPreview: true
|
||||
});
|
||||
this.listenTo(this.licenseModel, 'change', this.handleLicenseChange);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
@@ -79,6 +88,8 @@ var DetailsView = ValidatingView.extend({
|
||||
}
|
||||
this.$('#' + this.fieldToSelectorMap['entrance_exam_minimum_score_pct']).val(this.model.get('entrance_exam_minimum_score_pct'));
|
||||
|
||||
this.licenseView.render()
|
||||
|
||||
return this;
|
||||
},
|
||||
fieldToSelectorMap : {
|
||||
@@ -265,12 +276,13 @@ var DetailsView = ValidatingView.extend({
|
||||
this.model.fetch({
|
||||
success: function() {
|
||||
self.render();
|
||||
_.each(self.codeMirrors,
|
||||
function(mirror) {
|
||||
var ele = mirror.getTextArea();
|
||||
var field = self.selectorToField[ele.id];
|
||||
mirror.setValue(self.model.get(field));
|
||||
});
|
||||
_.each(self.codeMirrors, function(mirror) {
|
||||
var ele = mirror.getTextArea();
|
||||
var field = self.selectorToField[ele.id];
|
||||
mirror.setValue(self.model.get(field));
|
||||
});
|
||||
self.licenseModel.setFromString(self.model.get("license"), {silent: true});
|
||||
self.licenseView.render()
|
||||
},
|
||||
reset: true,
|
||||
silent: true});
|
||||
@@ -316,6 +328,11 @@ var DetailsView = ValidatingView.extend({
|
||||
}
|
||||
});
|
||||
modal.show();
|
||||
},
|
||||
|
||||
handleLicenseChange: function() {
|
||||
this.showNotificationBar()
|
||||
this.model.set("license", this.licenseModel.toString())
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ textarea.text {
|
||||
}
|
||||
|
||||
// +Fields - Not Editable
|
||||
// ====================
|
||||
// ====================
|
||||
.field.is-not-editable {
|
||||
|
||||
& label.is-focused {
|
||||
@@ -72,7 +72,7 @@ textarea.text {
|
||||
}
|
||||
|
||||
// +Fields - With Error
|
||||
// ====================
|
||||
// ====================
|
||||
.field.error {
|
||||
|
||||
input, textarea {
|
||||
@@ -81,7 +81,7 @@ textarea.text {
|
||||
}
|
||||
|
||||
// +Forms - Additional UI
|
||||
// ====================
|
||||
// ====================
|
||||
form {
|
||||
|
||||
// CASE: cosmetic checkbox input
|
||||
@@ -173,7 +173,7 @@ form {
|
||||
}
|
||||
|
||||
// +Form - Create New
|
||||
// ====================
|
||||
// ====================
|
||||
// form styling for creating a new content item (course, user, textbook)
|
||||
// TODO: refactor this into a placeholder to extend.
|
||||
.form-create {
|
||||
@@ -390,8 +390,8 @@ form {
|
||||
}
|
||||
}
|
||||
|
||||
// +Form - Inline Name Edit
|
||||
// ====================
|
||||
// +Form - Inline Name Edit
|
||||
// ====================
|
||||
// form - inline xblock name edit on unit, container, outline
|
||||
// TODO: abstract this out into a Sass placeholder
|
||||
.incontext-editor.is-editable {
|
||||
@@ -431,8 +431,8 @@ form {
|
||||
}
|
||||
}
|
||||
|
||||
// +Form - Create New Wrapper
|
||||
// ====================
|
||||
// +Form - Create New Wrapper
|
||||
// ====================
|
||||
.wrapper-create-element {
|
||||
height: auto;
|
||||
opacity: 1.0;
|
||||
@@ -449,8 +449,8 @@ form {
|
||||
}
|
||||
}
|
||||
|
||||
// +Form - Grandfathered
|
||||
// ====================
|
||||
// +Form - Grandfathered
|
||||
// ====================
|
||||
input.search {
|
||||
padding: 6px 15px 8px 30px;
|
||||
@include box-sizing(border-box);
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
// studio - elements - xblock rendering
|
||||
// ==========================
|
||||
|
||||
// styling for xblocks at various levels of nesting: page level,
|
||||
// Table of Contents
|
||||
// * +Layout - Xblocks
|
||||
// * +Licensing - Xblocks
|
||||
// * +Pagination - Xblocks
|
||||
// * +Messaging - Xblocks
|
||||
// * +Case: Page Level
|
||||
// * +Case: Nesting Level
|
||||
// * +Case: Element / Component Level
|
||||
// * +Case: Experiment Groups - Edited
|
||||
// * +Editing - Xblocks
|
||||
// * +Case - Special Xblock Type Overrides
|
||||
|
||||
// extends - UI archetypes - xblock rendering
|
||||
|
||||
// +Layout - Xblocks
|
||||
// ====================
|
||||
// styling for xblocks at various levels of nesting: page level,
|
||||
.wrapper-xblock {
|
||||
margin: ($baseline/2);
|
||||
border: 1px solid $gray-l4;
|
||||
@@ -45,7 +58,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// secondary header for meta-information and associated actions
|
||||
// UI: secondary header for meta-information and associated actions
|
||||
.xblock-header-secondary {
|
||||
overflow: hidden;
|
||||
border-top: 1px solid $gray-l3;
|
||||
@@ -103,6 +116,48 @@
|
||||
}
|
||||
}
|
||||
|
||||
// +Licensing - Xblocks
|
||||
// ====================
|
||||
.xblock-license,
|
||||
.xmodule_display.xmodule_HtmlModule .xblock-license,
|
||||
.xmodule_VideoModule .xblock-license {
|
||||
@include text-align(right);
|
||||
@extend %t-title7;
|
||||
display: block;
|
||||
width: auto;
|
||||
border-top: 1px solid $gray-l3;
|
||||
padding: ($baseline/4) 0;
|
||||
color: $gray;
|
||||
text-align: $bi-app-right;
|
||||
|
||||
.license-label,
|
||||
.license-value,
|
||||
.license-actions {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $gray;
|
||||
|
||||
&:hover {
|
||||
color: $ui-link-color;
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
|
||||
// CASE: xblocks video
|
||||
.xmodule_VideoModule .xblock-license {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
|
||||
// +Pagination - Xblocks
|
||||
.container-paging-header {
|
||||
.meta-wrap {
|
||||
margin: $baseline ($baseline/2);
|
||||
@@ -132,12 +187,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ====================
|
||||
|
||||
//UI: default internal xblock content styles
|
||||
|
||||
// ====================
|
||||
// TO-DO: clean-up / remove this reset
|
||||
// internal headings for problems and video components
|
||||
h2 {
|
||||
@extend %t-title5;
|
||||
@@ -198,6 +250,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
// +Messaging - Xblocks
|
||||
// ====================
|
||||
// xblock message area, for general information as well as validation
|
||||
.wrapper-xblock-message {
|
||||
|
||||
@@ -259,7 +313,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
// CASE: page level - outer most level
|
||||
// +Case: Page Level
|
||||
// ====================
|
||||
&.level-page {
|
||||
margin: 0;
|
||||
box-shadow: none;
|
||||
@@ -303,7 +358,9 @@
|
||||
|
||||
}
|
||||
|
||||
// CASE: nesting level - element wrapper level
|
||||
// +Case: Nesting Level
|
||||
// ====================
|
||||
// element wrapper level
|
||||
&.level-nesting {
|
||||
@include transition(all $tmg-f2 linear 0s);
|
||||
border: 1px solid $gray-l3;
|
||||
@@ -337,7 +394,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
// CASE: element/component level
|
||||
// +Case: Element / Component Level
|
||||
// ====================
|
||||
&.level-element {
|
||||
@include transition(all $tmg-f2 linear 0s);
|
||||
box-shadow: none;
|
||||
@@ -394,7 +452,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
// CASE: edited experiment groups: active and inactive
|
||||
// +Case: Experiment Groups - Edited
|
||||
// ====================
|
||||
// edited experiment groups: active and inactive
|
||||
.wrapper-groups {
|
||||
|
||||
.title {
|
||||
@@ -434,8 +494,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
// +Editing - Xblocks
|
||||
// ====================
|
||||
// XBlock editing
|
||||
|
||||
// xblock Editor tab wrapper
|
||||
.wrapper-comp-editor {
|
||||
@@ -785,7 +845,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
// CASE: special xblock type overrides
|
||||
|
||||
// +Case - Special Xblock Type Overrides
|
||||
// ====================
|
||||
// TO-DO - remove this reset styling from base _xblocks.scss file
|
||||
|
||||
// Latex Compiler
|
||||
// make room for the launch compiler button
|
||||
@@ -816,3 +879,96 @@ div.wrapper-comp-editor.is-inactive ~ div.launch-latex-compiler {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
// CASE: xblock license settings
|
||||
.wrapper-license {
|
||||
|
||||
.license-types {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
|
||||
.license-type {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.action.license-button {
|
||||
@include grey-button;
|
||||
@extend %t-action2;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
width: 220px;
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
|
||||
&.is-selected {
|
||||
@include blue-button;
|
||||
}
|
||||
}
|
||||
|
||||
.tip {
|
||||
@extend %t-copy-sub2;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper-license-options {
|
||||
margin-bottom: ($baseline/2);
|
||||
|
||||
.tip {
|
||||
@extend %t-copy-sub2;
|
||||
}
|
||||
.license-options {
|
||||
padding-bottom: ($baseline/2);
|
||||
|
||||
.license-option {
|
||||
line-height: 1.5;
|
||||
border-bottom: 1px solid $gray-l4;
|
||||
padding: ($baseline/2) 0 ($baseline*0.4);
|
||||
|
||||
&.is-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
input[type=checkbox] {
|
||||
vertical-align: top;
|
||||
width: auto;
|
||||
min-width: auto;
|
||||
height: auto;
|
||||
border: 0;
|
||||
margin: ($baseline*0.15) 15px 0px;
|
||||
}
|
||||
|
||||
.option-name {
|
||||
@extend %t-action3;
|
||||
@extend %t-strong;
|
||||
display: inline-block;
|
||||
width: 15%;
|
||||
vertical-align: top;
|
||||
cursor: pointer;
|
||||
}
|
||||
.explanation {
|
||||
@extend %t-action4;
|
||||
display: inline-block;
|
||||
width: 75%;
|
||||
vertical-align: top;
|
||||
color: $gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.license-preview a {
|
||||
color: $gray;
|
||||
|
||||
&:hover {
|
||||
color: $ui-link-color;
|
||||
}
|
||||
}
|
||||
.list-input.settings-list ul.license-options li {
|
||||
// to make sure the padding is correctly overridden
|
||||
padding: ($baseline / 2) 0 ($baseline * 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,6 +176,7 @@
|
||||
// course status
|
||||
// --------------------
|
||||
.course-status {
|
||||
float: $bi-app-left;
|
||||
margin-bottom: $baseline;
|
||||
|
||||
.status-release {
|
||||
@@ -216,6 +217,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
// REMOVE BEFORE MERGE - removed outline styling here from cms
|
||||
|
||||
.wrapper-dnd {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
// outline
|
||||
// --------------------
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
// studio - views - course settings
|
||||
// ====================
|
||||
// Table of Contents
|
||||
// * +Settings - Base / All
|
||||
// * +Settings - Licenses
|
||||
|
||||
// +Settings - Base / All
|
||||
// ====================
|
||||
.view-settings {
|
||||
@include text-align(left);
|
||||
@include direction();
|
||||
@@ -104,10 +109,11 @@
|
||||
margin-top: ($baseline/4);
|
||||
color: $gray-d1;
|
||||
}
|
||||
.tip-inline{
|
||||
display: inline;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.tip-inline {
|
||||
display: inline;
|
||||
@include margin-left($baseline/4);
|
||||
}
|
||||
|
||||
.message-error {
|
||||
@extend %t-copy-sub1;
|
||||
@@ -149,6 +155,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#heading-entrance-exam{
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -156,6 +163,7 @@
|
||||
label[for="entrance-exam-enabled"] {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.field {
|
||||
margin: 0 0 ($baseline*2) 0;
|
||||
|
||||
@@ -977,4 +985,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
115
cms/templates/js/license-selector.underscore
Normal file
115
cms/templates/js/license-selector.underscore
Normal file
@@ -0,0 +1,115 @@
|
||||
<div class="wrapper-license">
|
||||
<h3 class="label setting-label">
|
||||
<%= gettext("License Type") %>
|
||||
</h3>
|
||||
<ul class="license-types">
|
||||
<% var link_start_tpl = '<a href="{url}" target="_blank">'; %>
|
||||
<% _.each(licenseInfo, function(license, licenseType) { %>
|
||||
<li class="license-type" data-license="<%- licenseType %>">
|
||||
<button name="license-<%- licenseType %>"
|
||||
class="action license-button <% if(model.type === licenseType) { print("is-selected"); } %>"
|
||||
aria-pressed="<%- (model.type === licenseType).toString() %>"
|
||||
<% if (license.tooltip) { %>data-tooltip="<%- license.tooltip %>"<% } %>>
|
||||
<%- license.name %>
|
||||
</button>
|
||||
<p class="tip">
|
||||
<% if(license.url) { %>
|
||||
<a href="<%- license.url %>" target="_blank">
|
||||
<%= gettext("Learn more about {license_name}")
|
||||
.replace("{license_name}", license.name)
|
||||
%>
|
||||
</a>
|
||||
<% } else { %>
|
||||
|
||||
<% } %>
|
||||
</p>
|
||||
</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
|
||||
<% var license = licenseInfo[model.type]; %>
|
||||
<% if(license && !_.isEmpty(license.options)) { %>
|
||||
<div class="wrapper-license-options">
|
||||
<h4 class="label setting-label">
|
||||
<%- gettext("Options for {license_name}").replace("{license_name}", license.name) %>
|
||||
</h4>
|
||||
<p class='tip tip-inline'>
|
||||
<%- gettext("The following options are available for the {license_name} license.")
|
||||
.replace("{license_name}", license.name) %>
|
||||
</p>
|
||||
<ul class="license-options">
|
||||
<% _.each(license.option_order, function(optionKey) { %>
|
||||
<% var optionInfo = license.options[optionKey]; %>
|
||||
<% if (optionInfo.type == "boolean") { %>
|
||||
<% var optionSelected = model.options[optionKey]; %>
|
||||
<% var optionDisabled = optionInfo.disabled %>
|
||||
<li data-option="<%- optionKey %>"
|
||||
class="action-item license-option
|
||||
<% if (optionSelected) { print("is-selected"); } %>
|
||||
<% if (optionDisabled) { print("is-disabled"); } else { print("is-clickable"); } %>"
|
||||
>
|
||||
<input type="checkbox"
|
||||
id="<%- model.type %>-<%- optionKey %>"
|
||||
name="<%- model.type %>-<%- optionKey %>"
|
||||
aria-describedby="<%- optionKey %>-explanation"
|
||||
<% if(optionSelected) { print('checked="checked"'); } %>
|
||||
<% if(optionDisabled) { print('disabled="disabled"'); } %>
|
||||
/>
|
||||
<label for="<%- model.type %>-<%- optionKey %>" class="option-name">
|
||||
<%- optionInfo.name %>
|
||||
</label>
|
||||
<div id="<%- optionKey %>-explanation" class="explanation">
|
||||
<%- optionInfo.help %>
|
||||
</div>
|
||||
</li>
|
||||
<% } // could implement other types here %>
|
||||
<% }) %>
|
||||
</ul>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% if (showPreview) { %>
|
||||
<div class="wrapper-license-preview">
|
||||
<h4 class="label setting-label">
|
||||
<%= gettext("License Display") %>
|
||||
</h4>
|
||||
<p class="tip">
|
||||
<%= gettext("The following message will be displayed at the bottom of the courseware pages within your course:") %>
|
||||
</p>
|
||||
<div class="license-preview">
|
||||
<% // keep this synchronized with the contents of common/templates/license.html %>
|
||||
<% if (model.type === "all-rights-reserved") { %>
|
||||
© <span class="license-text"><%= gettext("All Rights Reserved") %></span>
|
||||
<% } else if (model.type === "creative-commons") {
|
||||
var possible = ["by", "nc", "nd", "sa"];
|
||||
var enabled = _.filter(possible, function(option) {
|
||||
return model.options[option] === true || model.options[option.toUpperCase()] === true;
|
||||
});
|
||||
var version = model.options.ver || "4.0";
|
||||
if (_.isEmpty(enabled)) {
|
||||
enabled = ["zero"];
|
||||
version = model.options.ver || "1.0";
|
||||
}
|
||||
%>
|
||||
<a rel="license" href="https://creativecommons.org/licenses/<%- enabled.join("-") %>/<%- version %>/">
|
||||
<% if (previewButton) { %>
|
||||
<img src="https://licensebuttons.net/l/<%- enabled.join("-") %>/<%- version %>/<%- typeof buttonSize == "string" ? buttonSize : "88x31" %>.png"
|
||||
alt="<%- typeof licenseString == "string" ? licenseString : "" %>"
|
||||
/>
|
||||
<% } else { %>
|
||||
<% //<span> must come before <i> icon or else spacing gets messed up %>
|
||||
<span class="sr">gettext("Creative Commons licensed content, with terms as follow:") </span><i aria-hidden="true" class="icon-cc"></i>
|
||||
<% _.each(enabled, function(option) { %>
|
||||
<span class="sr"><%- license.options[option.toUpperCase()].name %> </span><i aria-hidden="true" class="icon-cc-<%- option %>"></i>
|
||||
<% }); %>
|
||||
<span class="license-text"><%= gettext("Some Rights Reserved") %></span>
|
||||
<% } %>
|
||||
<% } else { %>
|
||||
<%= typeof licenseString == "string" ? licenseString : "" %>
|
||||
<% // Default to ARR license %>
|
||||
© <span class="license-text"><%= gettext("All Rights Reserved") %></span>
|
||||
<% } %>
|
||||
</a>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
@@ -11,7 +11,7 @@
|
||||
%>
|
||||
|
||||
<%block name="header_extras">
|
||||
% for template_name in ["basic-modal", "modal-button", "upload-dialog"]:
|
||||
% for template_name in ["basic-modal", "modal-button", "upload-dialog", "license-selector"]:
|
||||
<script type="text/template" id="${template_name}-tpl">
|
||||
<%static:include path="js/${template_name}.underscore" />
|
||||
</script>
|
||||
@@ -328,6 +328,26 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}';
|
||||
</ol>
|
||||
</section>
|
||||
% endif
|
||||
|
||||
% if settings.FEATURES.get("LICENSING", False):
|
||||
<hr class="divide" />
|
||||
|
||||
<section class="group-settings license">
|
||||
<header>
|
||||
<h2 class="title-2">${_("Course Content License")}</h2>
|
||||
## Translators: At the course settings, the editor is able to select the default course content license.
|
||||
## The course content will have this license set, some assets can override the license with their own.
|
||||
## In the form, the license selector for course content is described using the following string:
|
||||
<span class="tip">${_("Select the default license for course content")}</span>
|
||||
</header>
|
||||
|
||||
<ol class="list-input">
|
||||
<li class="field text" id="field-course-license">
|
||||
<div id="course-license-selector"></div>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
% endif
|
||||
</form>
|
||||
</article>
|
||||
<aside class="content-supplementary" role="complementary">
|
||||
|
||||
@@ -10,9 +10,11 @@
|
||||
%>
|
||||
|
||||
## js templates
|
||||
<script id="metadata-editor-tpl" type="text/template">
|
||||
<%static:include path="js/metadata-editor.underscore" />
|
||||
</script>
|
||||
% for template_name in ["metadata-editor", "license-selector"]:
|
||||
<script id="${template_name}-tpl" type="text/template">
|
||||
<%static:include path="js/${template_name}.underscore" />
|
||||
</script>
|
||||
% endfor
|
||||
% for template_name in ["metadata-number-entry", "metadata-string-entry", "metadata-option-entry", "metadata-list-entry", "metadata-dict-entry", "metadata-file-uploader-entry", "metadata-file-uploader-item"]:
|
||||
<script id="${template_name}" type="text/template">
|
||||
<%static:include path="js/${template_name}.underscore" />
|
||||
|
||||
@@ -16,6 +16,7 @@ from xmodule.exceptions import UndefinedContext
|
||||
from xmodule.seq_module import SequenceDescriptor, SequenceModule
|
||||
from xmodule.graders import grader_from_conf
|
||||
from xmodule.tabs import CourseTabList
|
||||
from xmodule.mixin import LicenseMixin
|
||||
import json
|
||||
|
||||
from xblock.fields import Scope, List, String, Dict, Boolean, Integer, Float
|
||||
@@ -864,7 +865,10 @@ class CourseModule(CourseFields, SequenceModule): # pylint: disable=abstract-me
|
||||
"""
|
||||
|
||||
|
||||
class CourseDescriptor(CourseFields, SequenceDescriptor):
|
||||
class CourseDescriptor(CourseFields, LicenseMixin, SequenceDescriptor):
|
||||
"""
|
||||
The descriptor for the course XModule
|
||||
"""
|
||||
module_class = CourseModule
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -995,10 +999,12 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
|
||||
xml_object.remove(wiki_tag)
|
||||
|
||||
definition, children = super(CourseDescriptor, cls).definition_from_xml(xml_object, system)
|
||||
|
||||
definition['textbooks'] = textbooks
|
||||
definition['wiki_slug'] = wiki_slug
|
||||
|
||||
# load license if it exists
|
||||
definition = LicenseMixin.parse_license_from_xml(definition, xml_object)
|
||||
|
||||
return definition, children
|
||||
|
||||
def definition_to_xml(self, resource_fs):
|
||||
@@ -1017,6 +1023,10 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
|
||||
wiki_xml_object.set('slug', self.wiki_slug)
|
||||
xml_object.append(wiki_xml_object)
|
||||
|
||||
# handle license specifically. Default the course to have a license
|
||||
# of "All Rights Reserved", if a license is not explicitly set.
|
||||
self.add_license_to_xml(xml_object, default="all-rights-reserved")
|
||||
|
||||
return xml_object
|
||||
|
||||
def has_ended(self):
|
||||
|
||||
@@ -22,6 +22,15 @@ class EditingDescriptor(EditingFields, MakoModuleDescriptor):
|
||||
"""
|
||||
mako_template = "widgets/raw-edit.html"
|
||||
|
||||
@property
|
||||
def non_editable_metadata_fields(self):
|
||||
"""
|
||||
`data` should not be editable in the Studio settings editor.
|
||||
"""
|
||||
non_editable_fields = super(EditingDescriptor, self).non_editable_metadata_fields
|
||||
non_editable_fields.append(self.fields['data'])
|
||||
return non_editable_fields
|
||||
|
||||
# cdodge: a little refactoring here, since we're basically doing the same thing
|
||||
# here as with our parent class, let's call into it to get the basic fields
|
||||
# set and then add our additional fields. Trying to keep it DRY.
|
||||
|
||||
@@ -87,7 +87,7 @@ class HtmlModule(HtmlModuleMixin):
|
||||
pass
|
||||
|
||||
|
||||
class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor):
|
||||
class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor): # pylint: disable=abstract-method
|
||||
"""
|
||||
Module for putting raw html in a course
|
||||
"""
|
||||
@@ -263,6 +263,9 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor):
|
||||
|
||||
@property
|
||||
def non_editable_metadata_fields(self):
|
||||
"""
|
||||
`use_latex_compiler` should not be editable in the Studio settings editor.
|
||||
"""
|
||||
non_editable_fields = super(HtmlDescriptor, self).non_editable_metadata_fields
|
||||
non_editable_fields.append(HtmlDescriptor.use_latex_compiler)
|
||||
return non_editable_fields
|
||||
|
||||
64
common/lib/xmodule/xmodule/mixin.py
Normal file
64
common/lib/xmodule/xmodule/mixin.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
Reusable mixins for XBlocks and/or XModules
|
||||
"""
|
||||
|
||||
from xblock.fields import Scope, String, XBlockMixin
|
||||
|
||||
# Make '_' a no-op so we can scrape strings
|
||||
_ = lambda text: text
|
||||
|
||||
|
||||
class LicenseMixin(XBlockMixin):
|
||||
"""
|
||||
Mixin that allows an author to indicate a license on the contents of an
|
||||
XBlock. For example, a video could be marked as Creative Commons SA-BY
|
||||
licensed. You can even indicate the license on an entire course.
|
||||
|
||||
If this mixin is not applied to an XBlock, or if the license field is
|
||||
blank, then the content is subject to whatever legal licensing terms that
|
||||
apply to content by default. For example, in the United States, that content
|
||||
is exclusively owned by the creator of the content by default. Other
|
||||
countries may have similar laws.
|
||||
"""
|
||||
license = String(
|
||||
display_name=_("License"),
|
||||
help=_("A license defines how the contents of this block can be shared and reused."),
|
||||
default=None,
|
||||
scope=Scope.content,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def parse_license_from_xml(cls, definition, node):
|
||||
"""
|
||||
When importing an XBlock from XML, this method will parse the license
|
||||
information out of the XML and attach it to the block.
|
||||
It is defined here so that classes that use this mixin can simply refer
|
||||
to this method, rather than reimplementing it in their XML import
|
||||
functions.
|
||||
"""
|
||||
license = node.get('license', default=None) # pylint: disable=redefined-builtin
|
||||
if license:
|
||||
definition['license'] = license
|
||||
return definition
|
||||
|
||||
def add_license_to_xml(self, node, default=None):
|
||||
"""
|
||||
When generating XML from an XBlock, this method will add the XBlock's
|
||||
license to the XML representation before it is serialized.
|
||||
It is defined here so that classes that use this mixin can simply refer
|
||||
to this method, rather than reimplementing it in their XML export
|
||||
functions.
|
||||
"""
|
||||
if getattr(self, "license", default):
|
||||
node.set('license', self.license)
|
||||
|
||||
|
||||
def wrap_with_license(block, view, frag, context): # pylint: disable=unused-argument
|
||||
"""
|
||||
In the LMS, display the custom license underneath the XBlock.
|
||||
"""
|
||||
license = getattr(block, "license", None) # pylint: disable=redefined-builtin
|
||||
if license:
|
||||
context = {"license": license}
|
||||
frag.content += block.runtime.render_template('license_wrapper.html', context)
|
||||
return frag
|
||||
@@ -49,6 +49,7 @@ from xmodule.modulestore.edit_info import EditInfoRuntimeMixin
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateCourseError, ReferentialIntegrityError
|
||||
from xmodule.modulestore.inheritance import InheritanceMixin, inherit_metadata, InheritanceKeyValueStore
|
||||
from xmodule.modulestore.xml import CourseLocationManager
|
||||
from xmodule.services import SettingsService
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -900,6 +901,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
|
||||
|
||||
if self.user_service:
|
||||
services["user"] = self.user_service
|
||||
services["settings"] = SettingsService()
|
||||
|
||||
system = CachingDescriptorSystem(
|
||||
modulestore=self,
|
||||
|
||||
@@ -181,6 +181,15 @@ class SequenceDescriptor(SequenceFields, MakoModuleDescriptor, XmlDescriptor):
|
||||
self.runtime.add_block_as_child_node(child, xml_object)
|
||||
return xml_object
|
||||
|
||||
@property
|
||||
def non_editable_metadata_fields(self):
|
||||
"""
|
||||
`is_entrance_exam` should not be editable in the Studio settings editor.
|
||||
"""
|
||||
non_editable_fields = super(SequenceDescriptor, self).non_editable_metadata_fields
|
||||
non_editable_fields.append(self.fields['is_entrance_exam'])
|
||||
return non_editable_fields
|
||||
|
||||
def index_dictionary(self):
|
||||
"""
|
||||
Return dictionary prepared with module content and type for indexing.
|
||||
|
||||
@@ -473,7 +473,8 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor, StudioEditableDes
|
||||
non_editable_fields = super(SplitTestDescriptor, self).non_editable_metadata_fields
|
||||
non_editable_fields.extend([
|
||||
SplitTestDescriptor.due,
|
||||
SplitTestDescriptor.user_partitions
|
||||
SplitTestDescriptor.user_partitions,
|
||||
SplitTestDescriptor.group_id_to_child,
|
||||
])
|
||||
return non_editable_fields
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ from pkg_resources import resource_string
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from xblock.core import XBlock
|
||||
from xblock.fields import ScopeIds
|
||||
from xblock.runtime import KvsFieldData
|
||||
|
||||
@@ -41,6 +42,7 @@ from .video_xfields import VideoFields
|
||||
from .video_handlers import VideoStudentViewHandlers, VideoStudioViewHandlers
|
||||
|
||||
from xmodule.video_module import manage_video_subtitles_save
|
||||
from xmodule.mixin import LicenseMixin
|
||||
|
||||
# The following import/except block for edxval is temporary measure until
|
||||
# edxval is a proper XBlock Runtime Service.
|
||||
@@ -282,10 +284,13 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
|
||||
'transcript_languages': json.dumps(sorted_languages),
|
||||
'transcript_translation_url': self.runtime.handler_url(self, 'transcript', 'translation').rstrip('/?'),
|
||||
'transcript_available_translations_url': self.runtime.handler_url(self, 'transcript', 'available_translations').rstrip('/?'),
|
||||
'license': getattr(self, "license", None),
|
||||
})
|
||||
|
||||
|
||||
class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandlers, TabsEditingDescriptor, EmptyDataRawDescriptor):
|
||||
@XBlock.wants("settings")
|
||||
class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandlers,
|
||||
TabsEditingDescriptor, EmptyDataRawDescriptor):
|
||||
"""
|
||||
Descriptor for `VideoModule`.
|
||||
"""
|
||||
@@ -381,6 +386,12 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler
|
||||
def editable_metadata_fields(self):
|
||||
editable_fields = super(VideoDescriptor, self).editable_metadata_fields
|
||||
|
||||
settings_service = self.runtime.service(self, 'settings')
|
||||
if settings_service:
|
||||
xb_settings = settings_service.get_settings_bucket(self)
|
||||
if not xb_settings.get("licensing_enabled", False) and "license" in editable_fields:
|
||||
del editable_fields["license"]
|
||||
|
||||
if self.source_visible:
|
||||
editable_fields['source']['non_editable'] = True
|
||||
else:
|
||||
@@ -483,6 +494,9 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler
|
||||
except edxval_api.ValVideoNotFoundError:
|
||||
pass
|
||||
|
||||
# handle license specifically
|
||||
self.add_license_to_xml(xml)
|
||||
|
||||
return xml
|
||||
|
||||
def get_context(self):
|
||||
@@ -642,6 +656,9 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler
|
||||
course_id=getattr(id_generator, 'target_course_id', None)
|
||||
)
|
||||
|
||||
# load license if it exists
|
||||
field_data = LicenseMixin.parse_license_from_xml(field_data, xml)
|
||||
|
||||
return field_data
|
||||
|
||||
def index_dictionary(self):
|
||||
|
||||
@@ -6,12 +6,13 @@ import datetime
|
||||
from xblock.fields import Scope, String, Float, Boolean, List, Dict
|
||||
|
||||
from xmodule.fields import RelativeTime
|
||||
from xmodule.mixin import LicenseMixin
|
||||
|
||||
# Make '_' a no-op so we can scrape strings
|
||||
_ = lambda text: text
|
||||
|
||||
|
||||
class VideoFields(object):
|
||||
class VideoFields(LicenseMixin):
|
||||
"""Fields for `VideoModule` and `VideoDescriptor`."""
|
||||
display_name = String(
|
||||
help=_("The name students see. This name appears in the course ribbon and as a header for the video."),
|
||||
|
||||
@@ -620,8 +620,9 @@ class XModuleMixin(XModuleFields, XBlockMixin):
|
||||
fields = getattr(self, 'unmixed_class', self.__class__).fields
|
||||
|
||||
for field in fields.values():
|
||||
|
||||
if field.scope != Scope.settings or field in self.non_editable_metadata_fields:
|
||||
if field in self.non_editable_metadata_fields:
|
||||
continue
|
||||
if field.scope not in (Scope.settings, Scope.content):
|
||||
continue
|
||||
|
||||
metadata_fields[field.name] = self._create_metadata_editor_info(field)
|
||||
@@ -681,6 +682,8 @@ class XModuleMixin(XModuleFields, XBlockMixin):
|
||||
editor_type = "Dict"
|
||||
elif isinstance(field, RelativeTime):
|
||||
editor_type = "RelativeTime"
|
||||
elif isinstance(field, String) and field.name == "license":
|
||||
editor_type = "License"
|
||||
metadata_field_editor_info['type'] = editor_type
|
||||
metadata_field_editor_info['options'] = [] if values is None else values
|
||||
|
||||
|
||||
26
common/static/css/edx-cc.css
Normal file
26
common/static/css/edx-cc.css
Normal file
@@ -0,0 +1,26 @@
|
||||
@font-face {
|
||||
font-family: 'edx-cc';
|
||||
src: url('../fonts/edx-cc/edx-cc.eot?52318265');
|
||||
src: url('../fonts/edx-cc/edx-cc.eot?52318265#iefix') format('embedded-opentype'),
|
||||
url('../fonts/edx-cc/edx-cc.woff?52318265') format('woff'),
|
||||
url('../fonts/edx-cc/edx-cc.ttf?52318265') format('truetype'),
|
||||
url('../fonts/edx-cc/edx-cc.svg?52318265#edx-cc') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
[class^="icon-cc"]:before, [class*=" icon-cc"]:before {
|
||||
font-family: "edx-cc";
|
||||
}
|
||||
|
||||
.icon-cc:before { content: '\e800'; } /* '' */
|
||||
.icon-cc-by:before { content: '\e801'; } /* '' */
|
||||
.icon-cc-nc:before { content: '\e802'; } /* '' */
|
||||
.icon-cc-nc-eu:before { content: '\e803'; } /* '' */
|
||||
.icon-cc-nc-jp:before { content: '\e804'; } /* '' */
|
||||
.icon-cc-sa:before { content: '\e805'; } /* '' */
|
||||
.icon-cc-nd:before { content: '\e806'; } /* '' */
|
||||
.icon-cc-pd:before { content: '\e807'; } /* '' */
|
||||
.icon-cc-zero:before { content: '\e808'; } /* '' */
|
||||
.icon-cc-share:before { content: '\e809'; } /* '' */
|
||||
.icon-cc-remix:before { content: '\e80a'; } /* '' */
|
||||
BIN
common/static/fonts/edx-cc/edx-cc.eot
Normal file
BIN
common/static/fonts/edx-cc/edx-cc.eot
Normal file
Binary file not shown.
22
common/static/fonts/edx-cc/edx-cc.svg
Normal file
22
common/static/fonts/edx-cc/edx-cc.svg
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<metadata>Copyright (C) 2014 by original authors @ fontello.com</metadata>
|
||||
<defs>
|
||||
<font id="edx-cc" horiz-adv-x="1000" >
|
||||
<font-face font-family="edx-cc" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" />
|
||||
<missing-glyph horiz-adv-x="1000" />
|
||||
<glyph glyph-name="cc" unicode="" d="m474 830q198 2 340-136t146-336q2-200-136-342t-338-146q-198-2-341 137t-145 337q-4 200 135 342t339 144z m12-858q156 2 266 114t108 270-115 267-269 107q-158-2-267-114t-107-270 114-267 270-107z m-124 298q40 0 58 40l56-30q-20-36-50-52-32-20-70-20-62 0-100 38-38 36-38 104t38 106 96 38q86 0 124-66l-62-32q-10 20-24 28t-28 8q-60 0-60-82 0-38 14-58 18-22 46-22z m266 0q42 0 56 40l58-30q-18-32-50-52t-70-20q-64 0-100 38-38 36-38 104 0 64 38 106 38 38 98 38 84 0 120-66l-60-32q-10 20-24 28t-28 8q-62 0-62-82 0-36 16-58t46-22z" horiz-adv-x="960" />
|
||||
<glyph glyph-name="cc-by" unicode="" d="m480 526q-66 0-66 68t66 68q68 0 68-68t-68-68z m98-26q14 0 22-8 10-10 10-22l0-196-56 0 0-234-148 0 0 234-56 0 0 196q0 12 10 22 8 8 22 8l196 0z m-98 330q200 0 340-140t140-340q0-198-140-339t-340-141q-198 0-339 141t-141 339q0 200 141 340t339 140z m0-872q162 0 277 115t115 277q0 164-115 278t-277 114-277-114-115-278q0-162 115-277t277-115z" horiz-adv-x="960" />
|
||||
<glyph glyph-name="cc-nc" unicode="" d="m480 830q200 0 340-140t140-340q0-198-140-339t-340-141q-198 0-339 141t-141 339q0 200 141 340t339 140z m-370-350q-22-62-22-130 0-162 115-277t277-115q110 0 202 56t142 148l-178 80q-8-46-46-74-38-30-86-34l0-74-56 0 0 74q-78 0-146 58l66 66q50-44 108-44 24 0 42 12t18 36q0 18-14 30l-46 20-56 26-76 32z m506-122l242-108q14 44 14 100 0 164-115 278t-277 114q-102 0-188-48t-140-130l182-82q12 36 46 62 32 22 78 24l0 74 56 0 0-74q68-4 120-44l-62-64q-44 28-84 28-24 0-38-8-18-10-18-30 0-8 4-12l60-28 42-18z" horiz-adv-x="960" />
|
||||
<glyph glyph-name="cc-nc-eu" unicode="" d="m480 830q200 0 340-140t140-340q0-198-140-339t-340-141q-198 0-339 141t-141 339q0 200 141 340t339 140z m-370-352q-22-62-22-128 0-162 115-277t277-115q110 0 201 55t143 149l-246 108-174 0q10-36 26-56 38-40 104-40 46 0 92 20l18-90q-56-30-124-30-128 0-196 92-34 44-46 104l-52 0 0 58 44 0 0 14q0 4 1 12t1 12l-46 0 0 56 10 0z m488-112l262-116q12 48 12 100 0 164-115 278t-277 114q-102 0-189-48t-141-130l158-70q8 14 28 38 72 82 184 82 70 0 122-24l-24-92q-40 20-88 20-64 0-100-44-10-10-16-28l56-24 136 0 0-56-8 0z" horiz-adv-x="960" />
|
||||
<glyph glyph-name="cc-nc-jp" unicode="" d="m480 830q200 0 340-140t140-340q0-198-140-339t-340-141q-198 0-339 141t-141 339q0 200 141 340t339 140z m-374-364q-18-54-18-116 0-162 115-277t277-115q106 0 195 52t141 140l-152 68 0-68-126 0 0-108-118 0 0 108-124 0 0 74 124 0 0 36-12 24-112 0 0 74 54 0z m432-242l112 0-106 48-6-12 0-36z m126 100l192-86q16 58 16 112 0 164-115 278t-277 114q-106 0-194-51t-140-137l158-70-54 98 128 0 76-166 46-20 82 186 128 0-122-224 76 0 0-34z" horiz-adv-x="960" />
|
||||
<glyph glyph-name="cc-sa" unicode="" d="m478 604q114 0 180-74 66-72 66-186 0-110-68-184-74-74-180-74-80 0-142 50-58 48-70 138l120 0q6-86 106-86 50 0 82 42 30 44 30 118 0 76-28 116-30 40-82 40-96 0-108-86l36 0-96-94-94 94 36 0q14 90 72 138t140 48z m2 226q200 0 340-140t140-340q0-198-140-339t-340-141q-198 0-339 141t-141 339q0 200 141 340t339 140z m0-872q162 0 277 115t115 277q0 164-115 278t-277 114-277-114-115-278q0-162 115-277t277-115z" horiz-adv-x="960" />
|
||||
<glyph glyph-name="cc-nd" unicode="" d="m306 382l0 82 348 0 0-82-348 0z m0-154l0 82 348 0 0-82-348 0z m174 602q200 0 340-140t140-340q0-198-140-339t-340-141q-198 0-339 141t-141 339q0 200 141 340t339 140z m0-872q162 0 277 115t115 277q0 164-115 278t-277 114-277-114-115-278q0-162 115-277t277-115z" horiz-adv-x="960" />
|
||||
<glyph glyph-name="cc-pd" unicode="" d="m480 830q200 0 340-140t140-340q0-198-140-339t-340-141q-198 0-339 141t-141 339q0 200 141 340t339 140z m-370-352q-22-62-22-128 0-162 115-277t277-115q110 0 201 55t143 149l-424 188q2-54 28-96t76-42q36 0 64 26l6 6 70-84q-4-2-10-7t-8-9q-62-42-136-42-86 0-159 58t-73 188q0 32 6 62z m310-34l440-194q12 48 12 100 0 164-115 278t-277 114q-102 0-189-48t-141-130l148-66q64 102 196 102 88 0 150-54l-78-80q-8 8-14 12-22 16-52 16-52 0-80-50z" horiz-adv-x="960" />
|
||||
<glyph glyph-name="cc-zero" unicode="" d="m480 628q108 0 153-81t45-197q0-114-45-195t-153-81-153 81-45 195q0 116 45 197t153 81z m-86-278q0-18 4-66l106 194q14 24-6 42-12 4-18 4-86 0-86-174z m86-172q86 0 86 172 0 40-6 84l-118-204q-22-30 12-46 2-2 6-2 2 0 2-2 2 0 8-1t10-1z m0 652q200 0 340-140t140-340q0-198-140-339t-340-141q-198 0-339 141t-141 339q0 200 141 340t339 140z m0-872q162 0 277 115t115 277q0 164-115 278t-277 114-277-114-115-278q0-162 115-277t277-115z" horiz-adv-x="960" />
|
||||
<glyph glyph-name="cc-share" unicode="" d="m676 488q12 0 20-8t8-18l0-354q0-10-8-18t-20-8l-260 0q-12 0-20 8t-8 18l0 104-104 0q-10 0-18 8t-8 20l0 352q0 12 6 18 4 6 18 10l264 0q10 0 18-8t8-20l0-104 104 0z m-264 0l108 0 0 78-210 0 0-300 78 0 0 196q0 10 8 18 4 4 16 8z m238-354l0 302-210 0 0-302 210 0z m-170 696q200 0 340-140t140-340q0-198-140-339t-340-141q-198 0-339 141t-141 339q0 200 141 340t339 140z m0-872q162 0 277 115t115 277q0 164-115 278t-277 114-277-114-115-278q0-162 115-277t277-115z" horiz-adv-x="960" />
|
||||
<glyph glyph-name="cc-remix" unicode="" d="m794 342l10-4 0-136-10-4-116-50-4-2-6 2-252 104-8 4-124-52-124 54 0 122 116 48-2 2 0 136 130 56 294-122 0-118z m-136-158l0 86-2 0 0 2-220 90 0-86 220-92 0 2z m14 112l78 32-72 30-76-32z m102-74l0 84-86-36 0-84z m-294 608q200 0 340-140t140-340q0-198-140-339t-340-141q-198 0-339 141t-141 339q0 200 141 340t339 140z m0-872q162 0 277 115t115 277q0 164-115 278t-277 114-277-114-115-278q0-162 115-277t277-115z" horiz-adv-x="960" />
|
||||
</font>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.7 KiB |
BIN
common/static/fonts/edx-cc/edx-cc.ttf
Normal file
BIN
common/static/fonts/edx-cc/edx-cc.ttf
Normal file
Binary file not shown.
BIN
common/static/fonts/edx-cc/edx-cc.woff
Normal file
BIN
common/static/fonts/edx-cc/edx-cc.woff
Normal file
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
// studio - utilities - mixins and extends
|
||||
// common - utilities - mixins and extends
|
||||
// ====================
|
||||
|
||||
// Table of Contents
|
||||
@@ -427,3 +427,4 @@
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
|
||||
60
common/templates/license.html
Normal file
60
common/templates/license.html
Normal file
@@ -0,0 +1,60 @@
|
||||
<%page args="license, button=False, button_size='88x31'"/>
|
||||
## keep this synchronized with the contents of cms/templates/js/license-selector.underscore
|
||||
<%!
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
def parse_license(lic):
|
||||
"""
|
||||
Returns a two-tuple: license type, and options.
|
||||
"""
|
||||
if not lic:
|
||||
return None, {}
|
||||
if ":" not in lic:
|
||||
# no options, so the entire thing is the license type
|
||||
return lic, {}
|
||||
|
||||
ltype, option_str = lic.split(":", 1)
|
||||
options = {}
|
||||
for part in option_str.split():
|
||||
if "=" in part:
|
||||
key, value = part.split("=", 1)
|
||||
options[key] = value
|
||||
else:
|
||||
options[part] = True
|
||||
return ltype, options
|
||||
%>
|
||||
<% license_type, license_options = parse_license(license) %>
|
||||
% if license_type == "all-rights-reserved":
|
||||
© <span class="license-text">${_("All Rights Reserved")}</span>
|
||||
% elif license_type == "creative-commons":
|
||||
<%
|
||||
possible = ["by", "nc", "nd", "sa"]
|
||||
names = {
|
||||
"by": _("Attribution"), "nc": _("Noncommercial"),
|
||||
"nd": _("No Derivatives"), "sa": _("Share Alike")
|
||||
}
|
||||
enabled = [opt for opt in possible
|
||||
if license_options.get(opt) or license_options.get(opt.upper())]
|
||||
version = license_options.get("ver", "4.0")
|
||||
if len(enabled) == 0:
|
||||
enabled = ["zero"]
|
||||
version = license_options.get("ver", "1.0")
|
||||
%>
|
||||
<a rel="license" href="https://creativecommons.org/licenses/${'-'.join(enabled)}/${version}/" target="_blank">
|
||||
% if button:
|
||||
<img src="https://licensebuttons.net/l/${'-'.join(enabled)}/${version}/${button_size}.png"
|
||||
alt="${license}"
|
||||
/>
|
||||
</a>
|
||||
% else:
|
||||
## <span> must come before <i> icon or else spacing gets messed up
|
||||
<span class="sr">${_("Creative Commons licensed content, with terms as follow:")} </span><i aria-hidden="true" class="icon-cc"></i>
|
||||
% for option in enabled:
|
||||
<span class="sr">${names[option]} </span><i aria-hidden="true" class="icon-cc-${option}"></i>
|
||||
% endfor
|
||||
<span class="license-text">${_("Some Rights Reserved")}</span>
|
||||
% endif
|
||||
</a>
|
||||
% else:
|
||||
${license}
|
||||
% endif
|
||||
3
common/templates/license_wrapper.html
Normal file
3
common/templates/license_wrapper.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<div class="xblock-license">
|
||||
<%include file="license.html" args="license=license" />
|
||||
</div>
|
||||
@@ -88,6 +88,16 @@ class CoursewarePage(CoursePage):
|
||||
|
||||
return True
|
||||
|
||||
@property
|
||||
def course_license(self):
|
||||
"""
|
||||
Returns the course license text, if present. Else returns None.
|
||||
"""
|
||||
element = self.q(css="#content .container-footer .course-license")
|
||||
if element.is_present():
|
||||
return element.text[0]
|
||||
return None
|
||||
|
||||
def get_active_subsection_url(self):
|
||||
"""
|
||||
return the url of the active subsection in the left nav
|
||||
|
||||
@@ -487,6 +487,19 @@ class XBlockWrapper(PageObject):
|
||||
"""
|
||||
type_in_codemirror(self, index, text, find_prefix='$("{}").find'.format(self.editor_selector))
|
||||
|
||||
def set_license(self, license_type):
|
||||
"""
|
||||
Uses the UI to set the course's license to the given license_type (str)
|
||||
"""
|
||||
css_selector = (
|
||||
"ul.license-types li[data-license={license_type}] button"
|
||||
).format(license_type=license_type)
|
||||
self.wait_for_element_presence(
|
||||
css_selector,
|
||||
"{license_type} button is present".format(license_type=license_type)
|
||||
)
|
||||
self.q(css=css_selector).click()
|
||||
|
||||
def save_settings(self):
|
||||
"""
|
||||
Click on settings Save button.
|
||||
|
||||
@@ -579,6 +579,13 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer):
|
||||
"""
|
||||
return self.children(CourseOutlineChild)
|
||||
|
||||
@property
|
||||
def license(self):
|
||||
"""
|
||||
Returns the course license text, if present. Else returns None.
|
||||
"""
|
||||
return self.q(css=".license-value").first.text[0]
|
||||
|
||||
|
||||
class CourseOutlineModal(object):
|
||||
MODAL_SELECTOR = ".wrapper-modal-window"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# coding: utf-8
|
||||
"""
|
||||
Course Schedule and Details Settings page.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
from bok_choy.promise import EmptyPromise
|
||||
|
||||
from .course_page import CoursePage
|
||||
@@ -17,6 +19,13 @@ class SettingsPage(CoursePage):
|
||||
def is_browser_on_page(self):
|
||||
return self.q(css='body.view-settings').present
|
||||
|
||||
def refresh_and_wait_for_load(self):
|
||||
"""
|
||||
Refresh the page and wait for all resources to load.
|
||||
"""
|
||||
self.browser.refresh()
|
||||
self.wait_for_page()
|
||||
|
||||
def get_elements(self, css_selector):
|
||||
self.wait_for_element_presence(
|
||||
css_selector,
|
||||
@@ -70,6 +79,50 @@ class SettingsPage(CoursePage):
|
||||
'Entrance exam minimum score percent is invisible'
|
||||
)
|
||||
|
||||
@property
|
||||
def course_license(self):
|
||||
"""
|
||||
Property. Returns the text of the license type for the course
|
||||
("All Rights Reserved" or "Creative Commons")
|
||||
"""
|
||||
license_types_css = "section.license ul.license-types li.license-type"
|
||||
self.wait_for_element_presence(
|
||||
license_types_css,
|
||||
"license type buttons are present",
|
||||
)
|
||||
selected = self.q(css=license_types_css + " button.is-selected")
|
||||
if selected.is_present():
|
||||
return selected.text[0]
|
||||
|
||||
# Look for the license text that will be displayed by default,
|
||||
# if no button is yet explicitly selected
|
||||
license_text = self.q(css='section.license span.license-text')
|
||||
if license_text.is_present():
|
||||
return license_text.text[0]
|
||||
return None
|
||||
|
||||
@course_license.setter
|
||||
def course_license(self, license_name):
|
||||
"""
|
||||
Sets the course license to the given license_name
|
||||
(str, "All Rights Reserved" or "Creative Commons")
|
||||
"""
|
||||
license_types_css = "section.license ul.license-types li.license-type"
|
||||
self.wait_for_element_presence(
|
||||
license_types_css,
|
||||
"license type buttons are present",
|
||||
)
|
||||
button_xpath = (
|
||||
"//section[contains(@class, 'license')]"
|
||||
"//ul[contains(@class, 'license-types')]"
|
||||
"//li[contains(@class, 'license-type')]"
|
||||
"//button[contains(text(),'{license_name}')]"
|
||||
).format(license_name=license_name)
|
||||
button = self.q(xpath=button_xpath)
|
||||
if not button.present:
|
||||
raise Exception("Invalid license name: {name}".format(name=license_name))
|
||||
button.click()
|
||||
|
||||
def save_changes(self, wait_for_confirmation=True):
|
||||
"""
|
||||
Clicks save button, waits for confirmation unless otherwise specified
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
# coding: utf-8
|
||||
"""
|
||||
Acceptance tests for Studio's Setting pages
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
from nose.plugins.attrib import attr
|
||||
|
||||
from base_studio_test import StudioCourseTest
|
||||
from bok_choy.promise import EmptyPromise
|
||||
from ...fixtures.course import XBlockFixtureDesc
|
||||
from ..helpers import create_user_partition_json
|
||||
from ...pages.studio.overview import CourseOutlinePage
|
||||
from ...pages.studio.settings import SettingsPage
|
||||
from ...pages.studio.settings_advanced import AdvancedSettingsPage
|
||||
from ...pages.studio.settings_group_configurations import GroupConfigurationsPage
|
||||
from ...pages.lms.courseware import CoursewarePage
|
||||
from unittest import skip
|
||||
from textwrap import dedent
|
||||
from xmodule.partitions.partitions import Group
|
||||
@@ -397,3 +402,77 @@ class AdvancedSettingsValidationTest(StudioCourseTest):
|
||||
expected_fields = self.advanced_settings.expected_settings_names
|
||||
displayed_fields = self.advanced_settings.displayed_settings_names
|
||||
self.assertEquals(set(displayed_fields), set(expected_fields))
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
class ContentLicenseTest(StudioCourseTest):
|
||||
"""
|
||||
Tests for course-level licensing (that is, setting the license,
|
||||
for an entire course's content, to All Rights Reserved or Creative Commons)
|
||||
"""
|
||||
def setUp(self): # pylint: disable=arguments-differ
|
||||
super(ContentLicenseTest, self).setUp()
|
||||
self.outline_page = CourseOutlinePage(
|
||||
self.browser,
|
||||
self.course_info['org'],
|
||||
self.course_info['number'],
|
||||
self.course_info['run']
|
||||
)
|
||||
self.settings_page = SettingsPage(
|
||||
self.browser,
|
||||
self.course_info['org'],
|
||||
self.course_info['number'],
|
||||
self.course_info['run']
|
||||
)
|
||||
self.lms_courseware = CoursewarePage(
|
||||
self.browser,
|
||||
self.course_id,
|
||||
)
|
||||
self.settings_page.visit()
|
||||
|
||||
def test_empty_license(self):
|
||||
"""
|
||||
When I visit the Studio settings page,
|
||||
I see that the course license is "All Rights Reserved" by default.
|
||||
Then I visit the LMS courseware page,
|
||||
and I see that the default course license is displayed.
|
||||
"""
|
||||
self.assertEqual(self.settings_page.course_license, "All Rights Reserved")
|
||||
self.lms_courseware.visit()
|
||||
self.assertEqual(self.lms_courseware.course_license, "© All Rights Reserved")
|
||||
|
||||
def test_arr_license(self):
|
||||
"""
|
||||
When I visit the Studio settings page,
|
||||
and I set the course license to "All Rights Reserved",
|
||||
and I refresh the page,
|
||||
I see that the course license is "All Rights Reserved".
|
||||
Then I visit the LMS courseware page,
|
||||
and I see that the course license is "All Rights Reserved".
|
||||
"""
|
||||
self.settings_page.course_license = "All Rights Reserved"
|
||||
self.settings_page.save_changes()
|
||||
self.settings_page.refresh_and_wait_for_load()
|
||||
self.assertEqual(self.settings_page.course_license, "All Rights Reserved")
|
||||
|
||||
self.lms_courseware.visit()
|
||||
self.assertEqual(self.lms_courseware.course_license, "© All Rights Reserved")
|
||||
|
||||
def test_cc_license(self):
|
||||
"""
|
||||
When I visit the Studio settings page,
|
||||
and I set the course license to "Creative Commons",
|
||||
and I refresh the page,
|
||||
I see that the course license is "Creative Commons".
|
||||
Then I visit the LMS courseware page,
|
||||
and I see that the course license is "Some Rights Reserved".
|
||||
"""
|
||||
self.settings_page.course_license = "Creative Commons"
|
||||
self.settings_page.save_changes()
|
||||
self.settings_page.refresh_and_wait_for_load()
|
||||
self.assertEqual(self.settings_page.course_license, "Creative Commons")
|
||||
|
||||
self.lms_courseware.visit()
|
||||
# The course_license text will include a bunch of screen reader text to explain
|
||||
# the selected options
|
||||
self.assertIn("Some Rights Reserved", self.lms_courseware.course_license)
|
||||
|
||||
116
common/test/acceptance/tests/video/test_video_license.py
Normal file
116
common/test/acceptance/tests/video/test_video_license.py
Normal file
@@ -0,0 +1,116 @@
|
||||
# coding: utf-8
|
||||
"""
|
||||
Acceptance tests for licensing of the Video module
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
from nose.plugins.attrib import attr
|
||||
from ..studio.base_studio_test import StudioCourseTest
|
||||
|
||||
#from ..helpers import UniqueCourseTest
|
||||
from ...pages.studio.overview import CourseOutlinePage
|
||||
from ...pages.lms.courseware import CoursewarePage
|
||||
from ...fixtures.course import XBlockFixtureDesc
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
class VideoLicenseTest(StudioCourseTest):
|
||||
"""
|
||||
Tests for video module-level licensing (that is, setting the license,
|
||||
for a specific video module, to All Rights Reserved or Creative Commons)
|
||||
"""
|
||||
def setUp(self): # pylint: disable=arguments-differ
|
||||
super(VideoLicenseTest, self).setUp()
|
||||
|
||||
self.lms_courseware = CoursewarePage(
|
||||
self.browser,
|
||||
self.course_id,
|
||||
)
|
||||
self.studio_course_outline = CourseOutlinePage(
|
||||
self.browser,
|
||||
self.course_info['org'],
|
||||
self.course_info['number'],
|
||||
self.course_info['run']
|
||||
)
|
||||
|
||||
# used by StudioCourseTest.setUp()
|
||||
def populate_course_fixture(self, course_fixture):
|
||||
"""
|
||||
Create a course with a single chapter.
|
||||
That chapter has a single section.
|
||||
That section has a single vertical.
|
||||
That vertical has a single video element.
|
||||
"""
|
||||
video_block = XBlockFixtureDesc('video', "Test Video")
|
||||
vertical = XBlockFixtureDesc('vertical', "Test Vertical")
|
||||
vertical.add_children(video_block)
|
||||
sequential = XBlockFixtureDesc('sequential', "Test Section")
|
||||
sequential.add_children(vertical)
|
||||
chapter = XBlockFixtureDesc('chapter', "Test Chapter")
|
||||
chapter.add_children(sequential)
|
||||
self.course_fixture.add_children(chapter)
|
||||
|
||||
def test_empty_license(self):
|
||||
"""
|
||||
When I visit the LMS courseware,
|
||||
I can see that the video is present
|
||||
but it has no license displayed by default.
|
||||
"""
|
||||
self.lms_courseware.visit()
|
||||
video = self.lms_courseware.q(css=".vert .xblock .video")
|
||||
self.assertTrue(video.is_present())
|
||||
video_license = self.lms_courseware.q(css=".vert .xblock.xmodule_VideoModule .xblock-license")
|
||||
self.assertFalse(video_license.is_present())
|
||||
|
||||
def test_arr_license(self):
|
||||
"""
|
||||
When I edit a video element in Studio,
|
||||
I can set an "All Rights Reserved" license on that video element.
|
||||
When I visit the LMS courseware,
|
||||
I can see that the video is present
|
||||
and that it has "All Rights Reserved" displayed for the license.
|
||||
"""
|
||||
self.studio_course_outline.visit()
|
||||
subsection = self.studio_course_outline.section_at(0).subsection_at(0)
|
||||
subsection.expand_subsection()
|
||||
unit = subsection.unit_at(0)
|
||||
container_page = unit.go_to()
|
||||
container_page.edit()
|
||||
video = [xb for xb in container_page.xblocks if xb.name == "Test Video"][0]
|
||||
video.edit().open_advanced_tab()
|
||||
video.set_license('all-rights-reserved')
|
||||
video.save_settings()
|
||||
container_page.publish_action.click()
|
||||
|
||||
self.lms_courseware.visit()
|
||||
video = self.lms_courseware.q(css=".vert .xblock .video")
|
||||
self.assertTrue(video.is_present())
|
||||
video_license = self.lms_courseware.q(css=".vert .xblock.xmodule_VideoModule .xblock-license")
|
||||
self.assertTrue(video_license.is_present())
|
||||
self.assertEqual(video_license.text[0], "© All Rights Reserved")
|
||||
|
||||
def test_cc_license(self):
|
||||
"""
|
||||
When I edit a video element in Studio,
|
||||
I can set a "Creative Commons" license on that video element.
|
||||
When I visit the LMS courseware,
|
||||
I can see that the video is present
|
||||
and that it has "Some Rights Reserved" displayed for the license.
|
||||
"""
|
||||
self.studio_course_outline.visit()
|
||||
subsection = self.studio_course_outline.section_at(0).subsection_at(0)
|
||||
subsection.expand_subsection()
|
||||
unit = subsection.unit_at(0)
|
||||
container_page = unit.go_to()
|
||||
container_page.edit()
|
||||
video = [xb for xb in container_page.xblocks if xb.name == "Test Video"][0]
|
||||
video.edit().open_advanced_tab()
|
||||
video.set_license('creative-commons')
|
||||
video.save_settings()
|
||||
container_page.publish_action.click()
|
||||
|
||||
self.lms_courseware.visit()
|
||||
video = self.lms_courseware.q(css=".vert .xblock .video")
|
||||
self.assertTrue(video.is_present())
|
||||
video_license = self.lms_courseware.q(css=".vert .xblock.xmodule_VideoModule .xblock-license")
|
||||
self.assertTrue(video_license.is_present())
|
||||
self.assertIn("Some Rights Reserved", video_license.text[0])
|
||||
@@ -1,4 +1,4 @@
|
||||
<course course_image="just_a_test.jpg">
|
||||
<course course_image="just_a_test.jpg" license="creative-commons: BY">
|
||||
<textbook title="Textbook" book_url="https://s3.amazonaws.com/edx-textbooks/guttag_computation_v3/"/>
|
||||
<chapter url_name="Overview">
|
||||
<videosequence url_name="Toy_Videos">
|
||||
@@ -10,7 +10,7 @@
|
||||
<html url_name="badlink"/>
|
||||
<html url_name="with_styling"/>
|
||||
<html url_name="just_img"/>
|
||||
<video url_name="Video_Resources" youtube_id_1_0="1bK-WdDi6Qw" display_name="Video Resources"/>
|
||||
<video url_name="Video_Resources" youtube_id_1_0="1bK-WdDi6Qw" display_name="Video Resources" license="all-rights-reserved"/>
|
||||
</videosequence>
|
||||
<video url_name="Welcome" youtube_id_1_0="p2Q6BrNhdh8" display_name="Welcome"/>
|
||||
<video url_name="video_123456789012" youtube_id_1_0="p2Q6BrNhdh8" display_name='Test Video'/>
|
||||
|
||||
@@ -67,6 +67,7 @@ from openedx.core.lib.xblock_utils import (
|
||||
)
|
||||
from xmodule.lti_module import LTIModule
|
||||
from xmodule.x_module import XModuleDescriptor
|
||||
from xmodule.mixin import wrap_with_license
|
||||
from xblock_django.user_service import DjangoXBlockUserService
|
||||
from util.json_request import JsonResponse
|
||||
from util.sandboxing import can_execute_unsafe_code, get_python_lib_zip
|
||||
@@ -533,6 +534,9 @@ def get_module_system_for_user(user, field_data_cache,
|
||||
# to the Fragment content coming out of the xblocks that are about to be rendered.
|
||||
block_wrappers = []
|
||||
|
||||
if settings.FEATURES.get("LICENSING", False):
|
||||
block_wrappers.append(wrap_with_license)
|
||||
|
||||
# Wrap the output display in a single div to allow for the XModule
|
||||
# javascript to be bound correctly
|
||||
if wrap_xmodule_display is True:
|
||||
|
||||
@@ -37,6 +37,7 @@ class TestVideoYouTube(TestVideo):
|
||||
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
|
||||
'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', False),
|
||||
'branding_info': None,
|
||||
'license': None,
|
||||
'cdn_eval': False,
|
||||
'cdn_exp_group': None,
|
||||
'data_dir': getattr(self, 'data_dir', None),
|
||||
@@ -104,6 +105,7 @@ class TestVideoNonYouTube(TestVideo):
|
||||
expected_context = {
|
||||
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
|
||||
'branding_info': None,
|
||||
'license': None,
|
||||
'cdn_eval': False,
|
||||
'cdn_exp_group': None,
|
||||
'data_dir': getattr(self, 'data_dir', None),
|
||||
@@ -211,6 +213,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
|
||||
expected_context = {
|
||||
'branding_info': None,
|
||||
'license': None,
|
||||
'cdn_eval': False,
|
||||
'cdn_exp_group': None,
|
||||
'data_dir': getattr(self, 'data_dir', None),
|
||||
@@ -330,6 +333,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
|
||||
initial_context = {
|
||||
'branding_info': None,
|
||||
'license': None,
|
||||
'cdn_eval': False,
|
||||
'cdn_exp_group': None,
|
||||
'data_dir': getattr(self, 'data_dir', None),
|
||||
@@ -472,6 +476,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
# Video found for edx_video_id
|
||||
initial_context = {
|
||||
'branding_info': None,
|
||||
'license': None,
|
||||
'cdn_eval': False,
|
||||
'cdn_exp_group': None,
|
||||
'data_dir': getattr(self, 'data_dir', None),
|
||||
@@ -584,6 +589,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
# Video found for edx_video_id
|
||||
initial_context = {
|
||||
'branding_info': None,
|
||||
'license': None,
|
||||
'cdn_eval': False,
|
||||
'cdn_exp_group': None,
|
||||
'data_dir': getattr(self, 'data_dir', None),
|
||||
@@ -705,6 +711,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
'logo_tag': 'Video hosted by XuetangX.com',
|
||||
'url': 'http://www.xuetangx.com'
|
||||
},
|
||||
'license': None,
|
||||
'cdn_eval': False,
|
||||
'cdn_exp_group': None,
|
||||
'data_dir': getattr(self, 'data_dir', None),
|
||||
|
||||
@@ -576,6 +576,7 @@ FACEBOOK_APP_SECRET = AUTH_TOKENS.get("FACEBOOK_APP_SECRET")
|
||||
FACEBOOK_APP_ID = AUTH_TOKENS.get("FACEBOOK_APP_ID")
|
||||
|
||||
XBLOCK_SETTINGS = ENV_TOKENS.get('XBLOCK_SETTINGS', {})
|
||||
XBLOCK_SETTINGS.setdefault("VideoDescriptor", {})["licensing_enabled"] = FEATURES.get("LICENSING", False)
|
||||
|
||||
##### CDN EXPERIMENT/MONITORING FLAGS #####
|
||||
CDN_VIDEO_URLS = ENV_TOKENS.get('CDN_VIDEO_URLS', CDN_VIDEO_URLS)
|
||||
|
||||
@@ -99,6 +99,9 @@ FEATURES['ENABLE_EDXNOTES'] = True
|
||||
# Enable teams feature
|
||||
FEATURES['ENABLE_TEAMS'] = True
|
||||
|
||||
# Enable custom content licensing
|
||||
FEATURES['LICENSING'] = True
|
||||
|
||||
# Unfortunately, we need to use debug mode to serve staticfiles
|
||||
DEBUG = True
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from .discussionsettings import *
|
||||
import dealer.git
|
||||
from xmodule.modulestore.modulestore_settings import update_module_store_settings
|
||||
from xmodule.mixin import LicenseMixin
|
||||
from lms.djangoapps.lms_xblock.mixin import LmsBlockMixin
|
||||
|
||||
################################### FEATURES ###################################
|
||||
@@ -371,6 +372,9 @@ FEATURES = {
|
||||
# enable beacons for lms onload event statistics
|
||||
'ENABLE_ONLOAD_BEACON': False,
|
||||
|
||||
# Toggle platform-wide course licensing
|
||||
'LICENSING': False,
|
||||
|
||||
# Certificates Web/HTML Views
|
||||
'CERTIFICATES_HTML_VIEW': False,
|
||||
|
||||
@@ -676,6 +680,7 @@ from xmodule.modulestore.inheritance import InheritanceMixin
|
||||
from xmodule.modulestore import prefer_xmodules
|
||||
from xmodule.x_module import XModuleMixin
|
||||
|
||||
# These are the Mixins that should be added to every XBlock.
|
||||
# This should be moved into an XBlock Runtime/Application object
|
||||
# once the responsibility of XBlock creation is moved out of modulestore - cpennington
|
||||
XBLOCK_MIXINS = (LmsBlockMixin, InheritanceMixin, XModuleMixin)
|
||||
@@ -1278,6 +1283,7 @@ PIPELINE_CSS = {
|
||||
'style-main': {
|
||||
'source_filenames': [
|
||||
'sass/lms-main.css',
|
||||
'css/edx-cc.css',
|
||||
],
|
||||
'output_filename': 'css/lms-main.css',
|
||||
},
|
||||
|
||||
@@ -115,6 +115,9 @@ FEATURES['MILESTONES_APP'] = True
|
||||
########################### Entrance Exams #################################
|
||||
FEATURES['ENTRANCE_EXAMS'] = True
|
||||
|
||||
################################ COURSE LICENSES ################################
|
||||
FEATURES['LICENSING'] = True
|
||||
|
||||
|
||||
########################## Courseware Search #######################
|
||||
FEATURES['ENABLE_COURSEWARE_SEARCH'] = True
|
||||
|
||||
@@ -1,6 +1,40 @@
|
||||
// lms - course - base
|
||||
// ====================
|
||||
|
||||
// Table of Contents
|
||||
// * +Containers
|
||||
// * +Resets - Old, Body
|
||||
// * +Resets - Old, Forms
|
||||
// * +Resets - Old, Images
|
||||
// * +Resets - Old, Misc
|
||||
|
||||
|
||||
// +Containers
|
||||
// ====================
|
||||
|
||||
.content-wrapper {
|
||||
background: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0;
|
||||
|
||||
> div {
|
||||
display: table;
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $outer-border-color;
|
||||
background: $container-bg;
|
||||
box-shadow: 0 1px 2px $shadow-l2;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// +Resets - Old, Body
|
||||
// ====================
|
||||
|
||||
body {
|
||||
min-width: 980px;
|
||||
min-height: 100%;
|
||||
@@ -26,25 +60,8 @@ a {
|
||||
}
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
background: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0;
|
||||
|
||||
> div {
|
||||
display: table;
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $outer-border-color;
|
||||
background: $container-bg;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
// +Resets - Old, Forms
|
||||
// ====================
|
||||
form {
|
||||
label {
|
||||
display: block;
|
||||
@@ -102,6 +119,8 @@ button,
|
||||
}
|
||||
|
||||
|
||||
// +Resets - Old, Images
|
||||
// ====================
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
@@ -134,6 +153,9 @@ img {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// +Resets - Old, Misc
|
||||
// ====================
|
||||
.test-class {
|
||||
border: 1px solid #f00;
|
||||
}
|
||||
|
||||
@@ -41,12 +41,63 @@ html.video-fullscreen{
|
||||
}
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
|
||||
.container-footer {
|
||||
margin: 0 auto;
|
||||
max-width: grid-width(12);
|
||||
min-width: 760px;
|
||||
width: flex-grid(12);
|
||||
color: $gray;
|
||||
text-align: $bi-app-right;
|
||||
}
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
|
||||
.course-license, .xblock-license {
|
||||
@include text-align(right);
|
||||
@extend %t-title7;
|
||||
display: block;
|
||||
width: auto;
|
||||
padding: ($baseline/4) 0;
|
||||
|
||||
span {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
a:link, a:visited {
|
||||
color: $gray;
|
||||
}
|
||||
a:active, a:hover {
|
||||
color: $link-hover;
|
||||
}
|
||||
|
||||
.license-label,
|
||||
.license-value,
|
||||
.license-actions {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
i {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
img {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TO-DO should this be content wrapper?
|
||||
div.course-wrapper {
|
||||
position: relative;
|
||||
|
||||
section.course-content {
|
||||
@extend .content;
|
||||
padding: 40px;
|
||||
padding: ($baseline*2);
|
||||
line-height: 1.6;
|
||||
|
||||
h1 {
|
||||
|
||||
@@ -174,6 +174,7 @@ ${fragment.foot_html()}
|
||||
% endif
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
% endif
|
||||
<section class="course-content" id="course-content" role="main" aria-label=“Content”>
|
||||
@@ -205,6 +206,18 @@ ${fragment.foot_html()}
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-footer">
|
||||
% if settings.FEATURES.get("LICENSING", False):
|
||||
<div class="course-license">
|
||||
% if getattr(course, "license", None):
|
||||
<%include file="../license.html" args="license=course.license" />
|
||||
% else:
|
||||
## Default course license: All Rights Reserved, if none is explicitly set.
|
||||
<%include file="../license.html" args="license='all-rights-reserved'" />
|
||||
% endif
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
||||
|
||||
<nav class="nav-utilities ${"has-utility-calculator" if course.show_calculator else ""}" aria-label="${_('Course Utilities')}">
|
||||
## Utility: Chat
|
||||
|
||||
Reference in New Issue
Block a user