diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 54bc6e785f..b13c341070 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -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. diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index 35dd95384b..a3fadaad2a 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -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", ] diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index 7305eb248a..d23c4476e1 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -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( diff --git a/cms/djangoapps/models/settings/course_details.py b/cms/djangoapps/models/settings/course_details.py index ab96448dad..e2f8119841 100644 --- a/cms/djangoapps/models/settings/course_details.py +++ b/cms/djangoapps/models/settings/course_details.py @@ -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) diff --git a/cms/envs/aws.py b/cms/envs/aws.py index 44b6ea2ffb..7fdd7496a4 100644 --- a/cms/envs/aws.py +++ b/cms/envs/aws.py @@ -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) diff --git a/cms/envs/bok_choy.py b/cms/envs/bok_choy.py index a01b334dbb..df8d0b73b1 100644 --- a/cms/envs/bok_choy.py +++ b/cms/envs/bok_choy.py @@ -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 diff --git a/cms/envs/common.py b/cms/envs/common.py index 5264605db3..d10c98bcc8 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -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) + } +} diff --git a/cms/envs/devstack.py b/cms/envs/devstack.py index 3aa7f47b00..1acaadd6a5 100644 --- a/cms/envs/devstack.py +++ b/cms/envs/devstack.py @@ -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 diff --git a/cms/static/coffee/spec/main.coffee b/cms/static/coffee/spec/main.coffee index edc8b3ac65..e2d10555d4 100644 --- a/cms/static/coffee/spec/main.coffee +++ b/cms/static/coffee/spec/main.coffee @@ -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", diff --git a/cms/static/js/models/license.js b/cms/static/js/models/license.js new file mode 100644 index 0000000000..1d032149c9 --- /dev/null +++ b/cms/static/js/models/license.js @@ -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; +}); diff --git a/cms/static/js/models/settings/course_details.js b/cms/static/js/models/settings/course_details.js index c3bff857fd..6f8487697e 100644 --- a/cms/static/js/models/settings/course_details.js +++ b/cms/static/js/models/settings/course_details.js @@ -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: [], diff --git a/cms/static/js/spec/models/license_spec.js b/cms/static/js/spec/models/license_spec.js new file mode 100644 index 0000000000..291e0e22ca --- /dev/null +++ b/cms/static/js/spec/models/license_spec.js @@ -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"); + }); + }) +}) diff --git a/cms/static/js/spec/views/license_spec.js b/cms/static/js/spec/views/license_spec.js new file mode 100644 index 0000000000..dd56cc1928 --- /dev/null +++ b/cms/static/js/spec/views/license_spec.js @@ -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"); + }); + + }) +}) diff --git a/cms/static/js/spec/views/settings/main_spec.js b/cms/static/js/spec/views/settings/main_spec.js index e04be25c2a..f489732eab 100644 --- a/cms/static/js/spec/views/settings/main_spec.js +++ b/cms/static/js/spec/views/settings/main_spec.js @@ -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'); diff --git a/cms/static/js/views/license.js b/cms/static/js/views/license.js new file mode 100644 index 0000000000..3e8f9a1cdf --- /dev/null +++ b/cms/static/js/views/license.js @@ -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 +

+ <%= gettext("License Type") %> +

+ + +<% var license = licenseInfo[model.type]; %> +<% if(license && !_.isEmpty(license.options)) { %> +
+

+ <%- gettext("Options for {license_name}").replace("{license_name}", license.name) %> +

+

+ <%- gettext("The following options are available for the {license_name} license.") + .replace("{license_name}", license.name) %> +

+ +
+<% } %> + +<% if (showPreview) { %> +
+

+ <%= gettext("License Display") %> +

+

+ <%= gettext("The following message will be displayed at the bottom of the courseware pages within your course:") %> +

+
+ <% // keep this synchronized with the contents of common/templates/license.html %> + <% if (model.type === "all-rights-reserved") { %> + © <%= gettext("All Rights Reserved") %> + <% } 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"; + } + %> + /<%- version %>/"> + <% if (previewButton) { %> + /<%- version %>/<%- typeof buttonSize == "string" ? buttonSize : "88x31" %>.png" + alt="<%- typeof licenseString == "string" ? licenseString : "" %>" + /> + <% } else { %> + <% // must come before icon or else spacing gets messed up %> + gettext("Creative Commons licensed content, with terms as follow:")  + <% _.each(enabled, function(option) { %> + <%- license.options[option.toUpperCase()].name %>  + <% }); %> + <%= gettext("Some Rights Reserved") %> + <% } %> + <% } else { %> + <%= typeof licenseString == "string" ? licenseString : "" %> + <% // Default to ARR license %> + © <%= gettext("All Rights Reserved") %> + <% } %> + +
+<% } %> +
diff --git a/cms/templates/settings.html b/cms/templates/settings.html index ba12f9af60..21f41289b6 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -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"]: @@ -328,6 +328,26 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}'; % endif + + % if settings.FEATURES.get("LICENSING", False): +
+ +
+
+

${_("Course Content License")}

+ ## 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: + ${_("Select the default license for course content")} +
+ +
    +
  1. +
    +
  2. +
+
+ % endif