Commit a3887e95 by David Baumgold Committed by Sarina Canelake

Courseware license (Creative Commons): FED

Use native checkboxes for courseware license options
In Studio settings editor for video module, don't show license if feature-flagged off
Don't let Scope.contents fields leak to Studio editor
JS gettext() must all be on the same line for i18n
Add docstrings for bok-choy tests
Remove LicenseMixin from HTMLDescriptor
Responding to UX review feedback
Add aria-pressed attribute
Use https links instead of protocol-relative links for links to creativecommons.org
Remove license from course outline page in Studio
parent 8fbaa66d
...@@ -339,3 +339,4 @@ if FEATURES['ENABLE_COURSEWARE_INDEX'] or FEATURES['ENABLE_LIBRARY_INDEX']: ...@@ -339,3 +339,4 @@ if FEATURES['ENABLE_COURSEWARE_INDEX'] or FEATURES['ENABLE_LIBRARY_INDEX']:
SEARCH_ENGINE = "search.elastic.ElasticSearchEngine" SEARCH_ENGINE = "search.elastic.ElasticSearchEngine"
XBLOCK_SETTINGS = ENV_TOKENS.get('XBLOCK_SETTINGS', {}) XBLOCK_SETTINGS = ENV_TOKENS.get('XBLOCK_SETTINGS', {})
XBLOCK_SETTINGS.setdefault("VideoDescriptor", {})["licensing_enabled"] = FEATURES.get("LICENSING", False)
...@@ -143,6 +143,9 @@ FEATURES = { ...@@ -143,6 +143,9 @@ FEATURES = {
# Toggle course entrance exams feature # Toggle course entrance exams feature
'ENTRANCE_EXAMS': False, 'ENTRANCE_EXAMS': False,
# Toggle platform-wide course licensing
'LICENSING': False,
# Enable the courseware search functionality # Enable the courseware search functionality
'ENABLE_COURSEWARE_INDEX': False, 'ENABLE_COURSEWARE_INDEX': False,
...@@ -945,3 +948,9 @@ ELASTIC_FIELD_MAPPINGS = { ...@@ -945,3 +948,9 @@ ELASTIC_FIELD_MAPPINGS = {
"type": "date" "type": "date"
} }
} }
XBLOCK_SETTINGS = {
"VideoDescriptor": {
"licensing_enabled": FEATURES.get("LICENSING", False)
}
}
...@@ -78,6 +78,9 @@ FEATURES['MILESTONES_APP'] = True ...@@ -78,6 +78,9 @@ FEATURES['MILESTONES_APP'] = True
################################ ENTRANCE EXAMS ################################ ################################ ENTRANCE EXAMS ################################
FEATURES['ENTRANCE_EXAMS'] = True FEATURES['ENTRANCE_EXAMS'] = True
################################ COURSE LICENSES ################################
FEATURES['LICENSING'] = True
################################ SEARCH INDEX ################################ ################################ SEARCH INDEX ################################
FEATURES['ENABLE_COURSEWARE_INDEX'] = True FEATURES['ENABLE_COURSEWARE_INDEX'] = True
FEATURES['ENABLE_LIBRARY_INDEX'] = True FEATURES['ENABLE_LIBRARY_INDEX'] = True
......
...@@ -138,19 +138,19 @@ define(["js/views/license", "js/models/license", "js/common_helpers/template_hel ...@@ -138,19 +138,19 @@ define(["js/views/license", "js/models/license", "js/common_helpers/template_hel
it("has no preview by default", function () { it("has no preview by default", function () {
this.view.render(); this.view.render();
expect(this.view.$("#license-preview").length).toEqual(0) expect(this.view.$(".license-preview").length).toEqual(0)
this.view.$("li[data-license=creative-commons] button").click(); this.view.$("li[data-license=creative-commons] button").click();
expect(this.view.$("#license-preview").length).toEqual(0) expect(this.view.$(".license-preview").length).toEqual(0)
}); });
it("displays a preview if showPreview is true", function() { it("displays a preview if showPreview is true", function() {
this.view = new LicenseView({model: this.model, showPreview: true}); this.view = new LicenseView({model: this.model, showPreview: true});
this.view.render() this.view.render()
expect(this.view.$("#license-preview").length).toEqual(1) expect(this.view.$(".license-preview").length).toEqual(1)
expect(this.view.$("#license-preview")).toHaveText(""); expect(this.view.$(".license-preview")).toHaveText("");
this.view.$("li[data-license=creative-commons] button").click(); this.view.$("li[data-license=creative-commons] button").click();
expect(this.view.$("#license-preview").length).toEqual(1) expect(this.view.$(".license-preview").length).toEqual(1)
expect(this.view.$("#license-preview")).toContainText("Some Rights Reserved"); expect(this.view.$(".license-preview")).toContainText("Some Rights Reserved");
}); });
}) })
......
...@@ -7,7 +7,7 @@ define(["js/views/baseview", "underscore"], function(BaseView, _) { ...@@ -7,7 +7,7 @@ define(["js/views/baseview", "underscore"], function(BaseView, _) {
"creative-commons": { "creative-commons": {
"name": gettext("Creative Commons"), "name": gettext("Creative Commons"),
"tooltip": gettext("You waive some rights for your work, such that others can use it too"), "tooltip": gettext("You waive some rights for your work, such that others can use it too"),
"url": "//creativecommons.org/about", "url": "https://creativecommons.org/about",
"options": { "options": {
"ver": { "ver": {
"name": gettext("Version"), "name": gettext("Version"),
...@@ -18,31 +18,27 @@ define(["js/views/baseview", "underscore"], function(BaseView, _) { ...@@ -18,31 +18,27 @@ define(["js/views/baseview", "underscore"], function(BaseView, _) {
"name": gettext("Attribution"), "name": gettext("Attribution"),
"type": "boolean", "type": "boolean",
"default": true, "default": true,
"help": gettext("Allow others to copy, distribute, display and perform " + "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."),
"your copyrighted work but only if they give credit the way you request."),
"disabled": true, "disabled": true,
}, },
"NC": { "NC": {
"name": gettext("Noncommercial"), "name": gettext("Noncommercial"),
"type": "boolean", "type": "boolean",
"default": true, "default": true,
"help": gettext("Allow others to copy, distribute, display and perform " + "help": gettext("Allow others to copy, distribute, display and perform your work - and derivative works based upon it - but for noncommercial purposes only."),
"your work - and derivative works based upon it - but for noncommercial purposes only."),
}, },
"ND": { "ND": {
"name": gettext("No Derivatives"), "name": gettext("No Derivatives"),
"type": "boolean", "type": "boolean",
"default": true, "default": true,
"help": gettext("Allow others to copy, distribute, display and perform " + "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\"."),
"only verbatim copies of your work, not derivative works based upon it."),
"conflictsWith": ["SA"] "conflictsWith": ["SA"]
}, },
"SA": { "SA": {
"name": gettext("Share Alike"), "name": gettext("Share Alike"),
"type": "boolean", "type": "boolean",
"default": false, "default": false,
"help": gettext("Allow others to distribute derivative works only under " + "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\"."),
"a license identical to the license that governs your work."),
"conflictsWith": ["ND"] "conflictsWith": ["ND"]
} }
}, },
...@@ -132,6 +128,7 @@ define(["js/views/baseview", "underscore"], function(BaseView, _) { ...@@ -132,6 +128,7 @@ define(["js/views/baseview", "underscore"], function(BaseView, _) {
// fire the change event manually. // fire the change event manually.
this.model.trigger("change change:options") this.model.trigger("change change:options")
} }
e.preventDefault();
} }
}); });
......
...@@ -276,12 +276,13 @@ var DetailsView = ValidatingView.extend({ ...@@ -276,12 +276,13 @@ var DetailsView = ValidatingView.extend({
this.model.fetch({ this.model.fetch({
success: function() { success: function() {
self.render(); self.render();
_.each(self.codeMirrors, _.each(self.codeMirrors, function(mirror) {
function(mirror) { var ele = mirror.getTextArea();
var ele = mirror.getTextArea(); var field = self.selectorToField[ele.id];
var field = self.selectorToField[ele.id]; mirror.setValue(self.model.get(field));
mirror.setValue(self.model.get(field)); });
}); self.licenseModel.setFromString(self.model.get("license"), {silent: true});
self.licenseView.render()
}, },
reset: true, reset: true,
silent: true}); silent: true});
......
...@@ -172,107 +172,6 @@ form { ...@@ -172,107 +172,6 @@ form {
} }
} }
// +Forms - License selector UI
// ====================
.license-img {
padding: 4px;
}
ul.license-types {
text-align: middle;
vertical-align: middle;
display: inline-block;
li {
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: 10px;
.tip {
@extend %t-copy-sub2;
}
#list-license-options {
padding-bottom: 10px;
li {
line-height: 1.5;
border-bottom: 1px solid #EEE;
padding: ($baseline / 2) 0 ($baseline * 0.4);
&.is-clickable {
cursor: pointer;
}
&.last {
border-bottom: none;
}
.fa {
vertical-align: top;
}
.fa-square-o {
display: inline-block;
margin: ($baseline * 0.15) 15px 0px;
}
.fa-square-o.is-disabled {
color: $gray;
}
.fa-check-square-o {
color: $blue;
display: none;
margin: ($baseline * 0.15) 14px 0px 16px;
}
.fa-check-square-o.is-disabled {
color: $gray;
}
&.is-selected {
.fa-check-square-o {
display: inline-block;
}
.fa-square-o {
display: none;
}
}
.option-name {
@extend %t-action3;
@extend %t-strong;
display: inline-block;
width: 15%;
vertical-align: top;
}
.explanation {
@extend %t-action4;
display: inline-block;
width: 75%;
vertical-align: top;
color: $gray;
}
}
}
}
// +Form - Create New // +Form - Create New
// ==================== // ====================
// form styling for creating a new content item (course, user, textbook) // form styling for creating a new content item (course, user, textbook)
......
// studio - elements - xblock rendering // studio - elements - xblock rendering
// ========================== // ==========================
// 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
// +Layout - Xblocks
// ====================
// styling for xblocks at various levels of nesting: page level, // styling for xblocks at various levels of nesting: page level,
// extends - UI archetypes - xblock rendering
.wrapper-xblock { .wrapper-xblock {
margin: ($baseline/2); margin: ($baseline/2);
border: 1px solid $gray-l4; border: 1px solid $gray-l4;
...@@ -45,7 +58,7 @@ ...@@ -45,7 +58,7 @@
} }
} }
// secondary header for meta-information and associated actions // UI: secondary header for meta-information and associated actions
.xblock-header-secondary { .xblock-header-secondary {
overflow: hidden; overflow: hidden;
border-top: 1px solid $gray-l3; border-top: 1px solid $gray-l3;
...@@ -103,28 +116,48 @@ ...@@ -103,28 +116,48 @@
} }
} }
// +Licensing - Xblocks
// ====================
.xblock-license, .xblock-license,
.xmodule_display.xmodule_HtmlModule .xblock-license { .xmodule_display.xmodule_HtmlModule .xblock-license,
text-align: $bi-app-right; .xmodule_VideoModule .xblock-license {
@include text-align(right);
@extend %t-title7;
display: block;
width: auto;
border-top: 1px solid $gray-l3; border-top: 1px solid $gray-l3;
margin: 0 15px; padding: ($baseline/4) 0;
padding: 5px; color: $gray;
font-size: 80%; text-align: $bi-app-right;
color: $gray-l3;
.license-label,
.license-value,
.license-actions {
display: inline-block;
vertical-align: middle;
margin-bottom: 0;
}
a { a {
color: $gray-l3; color: $gray;
&:hover { &:hover {
color: $ui-link-color; color: $ui-link-color;
} }
} }
span {
color: inherit; i {
font-style: normal;
} }
}
// CASE: xblocks video
.xmodule_VideoModule .xblock-license {
border: 0;
} }
// +Pagination - Xblocks
.container-paging-header { .container-paging-header {
.meta-wrap { .meta-wrap {
margin: $baseline ($baseline/2); margin: $baseline ($baseline/2);
...@@ -154,12 +187,9 @@ ...@@ -154,12 +187,9 @@
} }
} }
// ====================
//UI: default internal xblock content styles //UI: default internal xblock content styles
// ====================
// TO-DO: clean-up / remove this reset
// internal headings for problems and video components // internal headings for problems and video components
h2 { h2 {
@extend %t-title5; @extend %t-title5;
...@@ -220,6 +250,8 @@ ...@@ -220,6 +250,8 @@
} }
} }
// +Messaging - Xblocks
// ====================
// xblock message area, for general information as well as validation // xblock message area, for general information as well as validation
.wrapper-xblock-message { .wrapper-xblock-message {
...@@ -281,7 +313,8 @@ ...@@ -281,7 +313,8 @@
} }
} }
// CASE: page level - outer most level // +Case: Page Level
// ====================
&.level-page { &.level-page {
margin: 0; margin: 0;
box-shadow: none; box-shadow: none;
...@@ -325,7 +358,9 @@ ...@@ -325,7 +358,9 @@
} }
// CASE: nesting level - element wrapper level // +Case: Nesting Level
// ====================
// element wrapper level
&.level-nesting { &.level-nesting {
@include transition(all $tmg-f2 linear 0s); @include transition(all $tmg-f2 linear 0s);
border: 1px solid $gray-l3; border: 1px solid $gray-l3;
...@@ -359,7 +394,8 @@ ...@@ -359,7 +394,8 @@
} }
} }
// CASE: element/component level // +Case: Element / Component Level
// ====================
&.level-element { &.level-element {
@include transition(all $tmg-f2 linear 0s); @include transition(all $tmg-f2 linear 0s);
box-shadow: none; box-shadow: none;
...@@ -416,7 +452,9 @@ ...@@ -416,7 +452,9 @@
} }
} }
// CASE: edited experiment groups: active and inactive // +Case: Experiment Groups - Edited
// ====================
// edited experiment groups: active and inactive
.wrapper-groups { .wrapper-groups {
.title { .title {
...@@ -456,8 +494,8 @@ ...@@ -456,8 +494,8 @@
} }
} }
// +Editing - Xblocks
// ==================== // ====================
// XBlock editing
// xblock Editor tab wrapper // xblock Editor tab wrapper
.wrapper-comp-editor { .wrapper-comp-editor {
...@@ -807,7 +845,10 @@ ...@@ -807,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 // Latex Compiler
// make room for the launch compiler button // make room for the launch compiler button
......
...@@ -217,27 +217,7 @@ ...@@ -217,27 +217,7 @@
} }
} }
// course content license // REMOVE BEFORE MERGE - removed outline styling here from cms
// --------------------
.course-license {
@extend .content-primary;
@include text-align(right);
display: block;
float: $bi-app-right;
width: auto;
.license-label,
.license-value,
.license-actions {
display: inline-block;
vertical-align: middle;
margin-bottom: 0;
}
img {
display: inline;
}
}
.wrapper-dnd { .wrapper-dnd {
clear: both; clear: both;
......
// studio - views - course settings // studio - views - course settings
// ==================== // ====================
// Table of Contents
// * +Settings - Base / All
// * +Settings - Licenses
// +Settings - Base / All
// ====================
.view-settings { .view-settings {
@include text-align(left); @include text-align(left);
@include direction(); @include direction();
...@@ -104,10 +109,11 @@ ...@@ -104,10 +109,11 @@
margin-top: ($baseline/4); margin-top: ($baseline/4);
color: $gray-d1; color: $gray-d1;
} }
.tip-inline{
display: inline; .tip-inline {
margin-left: 5px; display: inline;
} @include margin-left($baseline/4);
}
.message-error { .message-error {
@extend %t-copy-sub1; @extend %t-copy-sub1;
...@@ -149,6 +155,7 @@ ...@@ -149,6 +155,7 @@
} }
} }
} }
#heading-entrance-exam{ #heading-entrance-exam{
font-weight: 600; font-weight: 600;
} }
...@@ -156,6 +163,7 @@ ...@@ -156,6 +163,7 @@
label[for="entrance-exam-enabled"] { label[for="entrance-exam-enabled"] {
font-size: 14px; font-size: 14px;
} }
.field { .field {
margin: 0 0 ($baseline*2) 0; margin: 0 0 ($baseline*2) 0;
...@@ -978,3 +986,100 @@ ...@@ -978,3 +986,100 @@
} }
} }
} }
// +Settings - Licenses
// ====================
.view-settings {
.license-img {
padding: ($baseline/5);
}
.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);
}
}
\ No newline at end of file
...@@ -109,13 +109,6 @@ from contentstore.utils import reverse_usage_url ...@@ -109,13 +109,6 @@ from contentstore.utils import reverse_usage_url
</div> </div>
</div> </div>
% if settings.FEATURES.get("LICENSING", False):
<div class="course-license">
<h2 class="license-label">${_("License:")}</h2>
<p class="license-value"><%include file="license.html" args="license=context_course.license" /></p>
</div>
% endif
<div class="wrapper-dnd"> <div class="wrapper-dnd">
<% <%
course_locator = context_course.location course_locator = context_course.location
......
<div class="license-wrapper"> <div class="wrapper-license">
<h3 class="label setting-label"> <h3 class="label setting-label">
<%= gettext("License Type") %> <%= gettext("License Type") %>
</h3> </h3>
<ul class="license-types"> <ul class="license-types">
<% var link_start_tpl = '<a href="{url}" target="_blank">'; %> <% var link_start_tpl = '<a href="{url}" target="_blank">'; %>
<% _.each(licenseInfo, function(license, licenseType) { %> <% _.each(licenseInfo, function(license, licenseType) { %>
<li data-license="<%- licenseType %>"> <li class="license-type" data-license="<%- licenseType %>">
<button name="license-<%- licenseType %>" <button name="license-<%- licenseType %>"
class="action license-button <% if(model.type === licenseType) { print("is-selected"); } %>" class="action license-button <% if(model.type === licenseType) { print("is-selected"); } %>"
aria-pressed="<%- (model.type === licenseType).toString() %>"
<% if (license.tooltip) { %>data-tooltip="<%- license.tooltip %>"<% } %>> <% if (license.tooltip) { %>data-tooltip="<%- license.tooltip %>"<% } %>>
<%- license.name %> <%- license.name %>
</button> </button>
...@@ -45,31 +46,23 @@ ...@@ -45,31 +46,23 @@
(optionInfo.conflictsWith && _.any(optionInfo.conflictsWith, function(key) {return model.options[key];})); (optionInfo.conflictsWith && _.any(optionInfo.conflictsWith, function(key) {return model.options[key];}));
%> %>
<li data-option="<%- optionKey %>" <li data-option="<%- optionKey %>"
class="action-item license-button class="action-item license-option
<% if (optionSelected) { print("is-selected"); } %> <% if (optionSelected) { print("is-selected"); } %>
<% if (optionDisabled) { print("is-disabled"); } else { print("is-clickable"); } %>" <% if (optionDisabled) { print("is-disabled"); } else { print("is-clickable"); } %>"
> >
<i aria-hidden="true" <input type="checkbox"
class="fa fa-fw id="<%- model.type %>-<%- optionKey %>"
<% if(optionSelected) { print("fa-check-square-o"); } else { print("fa-square-o"); } %> name="<%- model.type %>-<%- optionKey %>"
<% if(optionDisabled) { print("is-disabled"); } %> aria-describedby="<%- optionKey %>-explanation"
"></i> <% if(optionSelected) { print('checked="checked"'); } %>
<h4 class="option-name"> <% if(optionDisabled) { print('disabled="disabled"'); } %>
/>
<label for="<%- model.type %>-<%- optionKey %>" class="option-name">
<%- optionInfo.name %> <%- optionInfo.name %>
<% </label>
var states = []; <div id="<%- optionKey %>-explanation" class="explanation">
if (optionSelected) { <%- optionInfo.help %>
states.push(gettext("selected")); </div>
}
if (optionDisabled) {
states.push(gettext("disabled"));
}
if (states) {
%>
<span class="sr">, <%- states.join(", ") %></span>
<% } %>
</h4>
<div class="explanation"><%- optionInfo.help %></div>
</li> </li>
<% } // could implement other types here %> <% } // could implement other types here %>
<% }) %> <% }) %>
...@@ -78,7 +71,7 @@ ...@@ -78,7 +71,7 @@
<% } %> <% } %>
<% if (showPreview) { %> <% if (showPreview) { %>
<div class="license-preview-wrapper"> <div class="wrapper-license-preview">
<h4 class="label setting-label"> <h4 class="label setting-label">
<%= gettext("License Display") %> <%= gettext("License Display") %>
</h4> </h4>
...@@ -100,7 +93,7 @@ ...@@ -100,7 +93,7 @@
version = model.options.ver || "1.0"; version = model.options.ver || "1.0";
} }
%> %>
<a rel="license" href="//creativecommons.org/licenses/<%- enabled.join("-") %>/<%- version %>/"> <a rel="license" href="https://creativecommons.org/licenses/<%- enabled.join("-") %>/<%- version %>/">
<% if (previewButton) { %> <% if (previewButton) { %>
<img src="https://licensebuttons.net/l/<%- enabled.join("-") %>/<%- version %>/<%- typeof buttonSize == "string" ? buttonSize : "88x31" %>.png" <img src="https://licensebuttons.net/l/<%- enabled.join("-") %>/<%- version %>/<%- typeof buttonSize == "string" ? buttonSize : "88x31" %>.png"
alt="<%- typeof licenseString == "string" ? licenseString : "" %>" alt="<%- typeof licenseString == "string" ? licenseString : "" %>"
......
...@@ -20,7 +20,6 @@ from xmodule.x_module import XModule, DEPRECATION_VSCOMPAT_EVENT ...@@ -20,7 +20,6 @@ from xmodule.x_module import XModule, DEPRECATION_VSCOMPAT_EVENT
from xmodule.xml_module import XmlDescriptor, name_to_pathname from xmodule.xml_module import XmlDescriptor, name_to_pathname
from xblock.core import XBlock from xblock.core import XBlock
from xblock.fields import Scope, String, Boolean, List from xblock.fields import Scope, String, Boolean, List
from xmodule.mixin import LicenseMixin
log = logging.getLogger("edx.courseware") log = logging.getLogger("edx.courseware")
...@@ -88,7 +87,7 @@ class HtmlModule(HtmlModuleMixin): ...@@ -88,7 +87,7 @@ class HtmlModule(HtmlModuleMixin):
pass pass
class HtmlDescriptor(HtmlFields, LicenseMixin, XmlDescriptor, EditingDescriptor): # pylint: disable=abstract-method class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor): # pylint: disable=abstract-method
""" """
Module for putting raw html in a course Module for putting raw html in a course
""" """
......
...@@ -49,6 +49,7 @@ from xmodule.modulestore.edit_info import EditInfoRuntimeMixin ...@@ -49,6 +49,7 @@ from xmodule.modulestore.edit_info import EditInfoRuntimeMixin
from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateCourseError, ReferentialIntegrityError from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateCourseError, ReferentialIntegrityError
from xmodule.modulestore.inheritance import InheritanceMixin, inherit_metadata, InheritanceKeyValueStore from xmodule.modulestore.inheritance import InheritanceMixin, inherit_metadata, InheritanceKeyValueStore
from xmodule.modulestore.xml import CourseLocationManager from xmodule.modulestore.xml import CourseLocationManager
from xmodule.services import SettingsService
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -900,6 +901,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo ...@@ -900,6 +901,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
if self.user_service: if self.user_service:
services["user"] = self.user_service services["user"] = self.user_service
services["settings"] = SettingsService()
system = CachingDescriptorSystem( system = CachingDescriptorSystem(
modulestore=self, modulestore=self,
......
...@@ -181,6 +181,15 @@ class SequenceDescriptor(SequenceFields, MakoModuleDescriptor, XmlDescriptor): ...@@ -181,6 +181,15 @@ class SequenceDescriptor(SequenceFields, MakoModuleDescriptor, XmlDescriptor):
self.runtime.add_block_as_child_node(child, xml_object) self.runtime.add_block_as_child_node(child, xml_object)
return 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): def index_dictionary(self):
""" """
Return dictionary prepared with module content and type for indexing. Return dictionary prepared with module content and type for indexing.
......
...@@ -473,7 +473,8 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor, StudioEditableDes ...@@ -473,7 +473,8 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor, StudioEditableDes
non_editable_fields = super(SplitTestDescriptor, self).non_editable_metadata_fields non_editable_fields = super(SplitTestDescriptor, self).non_editable_metadata_fields
non_editable_fields.extend([ non_editable_fields.extend([
SplitTestDescriptor.due, SplitTestDescriptor.due,
SplitTestDescriptor.user_partitions SplitTestDescriptor.user_partitions,
SplitTestDescriptor.group_id_to_child,
]) ])
return non_editable_fields return non_editable_fields
......
...@@ -25,6 +25,7 @@ from pkg_resources import resource_string ...@@ -25,6 +25,7 @@ from pkg_resources import resource_string
from django.conf import settings from django.conf import settings
from xblock.core import XBlock
from xblock.fields import ScopeIds from xblock.fields import ScopeIds
from xblock.runtime import KvsFieldData from xblock.runtime import KvsFieldData
...@@ -287,6 +288,7 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers, ...@@ -287,6 +288,7 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
}) })
@XBlock.wants("settings")
class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandlers, class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandlers,
TabsEditingDescriptor, EmptyDataRawDescriptor): TabsEditingDescriptor, EmptyDataRawDescriptor):
""" """
...@@ -384,6 +386,12 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler ...@@ -384,6 +386,12 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler
def editable_metadata_fields(self): def editable_metadata_fields(self):
editable_fields = super(VideoDescriptor, self).editable_metadata_fields 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: if self.source_visible:
editable_fields['source']['non_editable'] = True editable_fields['source']['non_editable'] = True
else: else:
......
[class^="icon-cc"], [class*=" icon-cc"] {
font-family: 'edx-cc';
font-style: normal;
font-weight: normal;
/* fix buttons height */
line-height: 1em;
/* you can be more comfortable with increased icons size */
/* font-size: 120%; */
}
.icon-cc { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe800;&nbsp;'); }
.icon-cc-by { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe801;&nbsp;'); }
.icon-cc-nc { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe802;&nbsp;'); }
.icon-cc-nc-eu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe803;&nbsp;'); }
.icon-cc-nc-jp { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe804;&nbsp;'); }
.icon-cc-sa { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe805;&nbsp;'); }
.icon-cc-nd { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe806;&nbsp;'); }
.icon-cc-pd { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe807;&nbsp;'); }
.icon-cc-zero { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe808;&nbsp;'); }
.icon-cc-share { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe809;&nbsp;'); }
.icon-cc-remix { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80a;&nbsp;'); }
...@@ -8,46 +8,9 @@ ...@@ -8,46 +8,9 @@
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
/*
@media screen and (-webkit-min-device-pixel-ratio:0) {
@font-face {
font-family: 'edx-cc';
src: url('../fonts/edx-cc/edx-cc.svg?52318265#edx-cc') format('svg');
}
}
*/
[class^="icon-cc"]:before, [class*=" icon-cc"]:before { [class^="icon-cc"]:before, [class*=" icon-cc"]:before {
font-family: "edx-cc"; font-family: "edx-cc";
font-style: normal;
font-weight: normal;
speak: none;
display: inline-block;
text-decoration: inherit;
width: 1em;
margin-right: .2em;
text-align: center;
/* opacity: .8; */
/* For safety - reset parent styles, that can break glyph codes*/
font-variant: normal;
text-transform: none;
/* fix buttons height, for twitter bootstrap */
line-height: 1em;
/* Animation center compensation - margins should be symmetric */
/* remove if not needed */
margin-left: .2em;
/* you can be more comfortable with increased icons size */
/* font-size: 120%; */
/* Uncomment for 3D effect */
/* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
} }
.icon-cc:before { content: '\e800'; } /* '' */ .icon-cc:before { content: '\e800'; } /* '' */
......
// studio - utilities - mixins and extends // common - utilities - mixins and extends
// ==================== // ====================
// Table of Contents // Table of Contents
...@@ -427,3 +427,4 @@ ...@@ -427,3 +427,4 @@
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
...@@ -36,7 +36,7 @@ def parse_license(lic): ...@@ -36,7 +36,7 @@ def parse_license(lic):
enabled = ["zero"] enabled = ["zero"]
version = license_options.get("ver", "1.0") version = license_options.get("ver", "1.0")
%> %>
<a rel="license" href="//creativecommons.org/licenses/${'-'.join(enabled)}/${version}/"> <a rel="license" href="https://creativecommons.org/licenses/${'-'.join(enabled)}/${version}/">
% if button: % if button:
<img src="https://licensebuttons.net/l/${'-'.join(enabled)}/${version}/${button_size}.png" <img src="https://licensebuttons.net/l/${'-'.join(enabled)}/${version}/${button_size}.png"
alt="${license}" alt="${license}"
......
...@@ -19,6 +19,13 @@ class SettingsPage(CoursePage): ...@@ -19,6 +19,13 @@ class SettingsPage(CoursePage):
def is_browser_on_page(self): def is_browser_on_page(self):
return self.q(css='body.view-settings').present 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): def get_elements(self, css_selector):
self.wait_for_element_presence( self.wait_for_element_presence(
css_selector, css_selector,
...@@ -72,16 +79,35 @@ class SettingsPage(CoursePage): ...@@ -72,16 +79,35 @@ class SettingsPage(CoursePage):
'Entrance exam minimum score percent is invisible' 'Entrance exam minimum score percent is invisible'
) )
def set_course_license(self, license_type): @property
css_selector = ( def course_license(self):
"section.license ul.license-types " license_types_css = "section.license ul.license-types li.license-type"
"li[data-license={license_type}] button"
).format(license_type=license_type)
self.wait_for_element_presence( self.wait_for_element_presence(
css_selector, license_types_css,
'{license_type} button is present'.format(license_type=license_type) "license type buttons are present",
)
selected = self.q(css=license_types_css + " button.is-selected")
if selected.is_present():
return selected.text[0]
return None
@course_license.setter
def course_license(self, license_name):
license_types_css = "section.license ul.license-types li.license-type"
self.wait_for_element_presence(
license_types_css,
"license type buttons are present",
) )
self.q(css=css_selector).click() 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): def save_changes(self, wait_for_confirmation=True):
""" """
......
...@@ -424,27 +424,49 @@ class ContentLicenseTest(StudioCourseTest): ...@@ -424,27 +424,49 @@ class ContentLicenseTest(StudioCourseTest):
self.browser, self.browser,
self.course_id, self.course_id,
) )
self.outline_page.visit() self.settings_page.visit()
def test_empty_license(self): def test_empty_license(self):
self.assertEqual(self.outline_page.license, "None") """
When I visit the Studio settings page,
I see that the course license is "None" by default.
Then I visit the LMS courseware page,
and I see that there is no course license displayed.
"""
self.assertIsNone(self.settings_page.course_license)
self.lms_courseware.visit() self.lms_courseware.visit()
self.assertIsNone(self.lms_courseware.course_license) self.assertIsNone(self.lms_courseware.course_license)
def test_arr_license(self): def test_arr_license(self):
self.outline_page.edit_course_start_date() """
self.settings_page.set_course_license("all-rights-reserved") 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.save_changes()
self.outline_page.visit() self.settings_page.refresh_and_wait_for_load()
self.assertEqual(self.outline_page.license, "© All Rights Reserved") self.assertEqual(self.settings_page.course_license, "All Rights Reserved")
self.lms_courseware.visit() self.lms_courseware.visit()
self.assertEqual(self.lms_courseware.course_license, "© All Rights Reserved") self.assertEqual(self.lms_courseware.course_license, "© All Rights Reserved")
def test_cc_license(self): def test_cc_license(self):
self.outline_page.edit_course_start_date() """
self.settings_page.set_course_license("creative-commons") 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.save_changes()
self.outline_page.visit() self.settings_page.refresh_and_wait_for_load()
self.assertEqual(self.outline_page.license, "Some Rights Reserved") self.assertEqual(self.settings_page.course_license, "Creative Commons")
self.lms_courseware.visit() self.lms_courseware.visit()
self.assertEqual(self.lms_courseware.course_license, "Some Rights Reserved") self.assertEqual(self.lms_courseware.course_license, "Some Rights Reserved")
...@@ -28,6 +28,12 @@ class VideoLicenseTest(StudioCourseTest): ...@@ -28,6 +28,12 @@ class VideoLicenseTest(StudioCourseTest):
# used by StudioCourseTest.setUp() # used by StudioCourseTest.setUp()
def populate_course_fixture(self, course_fixture): 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") video_block = XBlockFixtureDesc('video', "Test Video")
vertical = XBlockFixtureDesc('vertical', "Test Vertical") vertical = XBlockFixtureDesc('vertical', "Test Vertical")
vertical.add_children(video_block) vertical.add_children(video_block)
...@@ -38,6 +44,11 @@ class VideoLicenseTest(StudioCourseTest): ...@@ -38,6 +44,11 @@ class VideoLicenseTest(StudioCourseTest):
self.course_fixture.add_children(chapter) self.course_fixture.add_children(chapter)
def test_empty_license(self): 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() self.lms_courseware.visit()
video = self.lms_courseware.q(css=".vert .xblock .video") video = self.lms_courseware.q(css=".vert .xblock .video")
self.assertTrue(video.is_present()) self.assertTrue(video.is_present())
...@@ -45,6 +56,13 @@ class VideoLicenseTest(StudioCourseTest): ...@@ -45,6 +56,13 @@ class VideoLicenseTest(StudioCourseTest):
self.assertFalse(video_license.is_present()) self.assertFalse(video_license.is_present())
def test_arr_license(self): 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() self.studio_course_outline.visit()
subsection = self.studio_course_outline.section_at(0).subsection_at(0) subsection = self.studio_course_outline.section_at(0).subsection_at(0)
subsection.expand_subsection() subsection.expand_subsection()
...@@ -65,6 +83,13 @@ class VideoLicenseTest(StudioCourseTest): ...@@ -65,6 +83,13 @@ class VideoLicenseTest(StudioCourseTest):
self.assertEqual(video_license.text[0], "© All Rights Reserved") self.assertEqual(video_license.text[0], "© All Rights Reserved")
def test_cc_license(self): 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() self.studio_course_outline.visit()
subsection = self.studio_course_outline.section_at(0).subsection_at(0) subsection = self.studio_course_outline.section_at(0).subsection_at(0)
subsection.expand_subsection() subsection.expand_subsection()
......
...@@ -576,6 +576,7 @@ FACEBOOK_APP_SECRET = AUTH_TOKENS.get("FACEBOOK_APP_SECRET") ...@@ -576,6 +576,7 @@ FACEBOOK_APP_SECRET = AUTH_TOKENS.get("FACEBOOK_APP_SECRET")
FACEBOOK_APP_ID = AUTH_TOKENS.get("FACEBOOK_APP_ID") FACEBOOK_APP_ID = AUTH_TOKENS.get("FACEBOOK_APP_ID")
XBLOCK_SETTINGS = ENV_TOKENS.get('XBLOCK_SETTINGS', {}) XBLOCK_SETTINGS = ENV_TOKENS.get('XBLOCK_SETTINGS', {})
XBLOCK_SETTINGS.setdefault("VideoDescriptor", {})["licensing_enabled"] = FEATURES.get("LICENSING", False)
##### CDN EXPERIMENT/MONITORING FLAGS ##### ##### CDN EXPERIMENT/MONITORING FLAGS #####
CDN_VIDEO_URLS = ENV_TOKENS.get('CDN_VIDEO_URLS', CDN_VIDEO_URLS) CDN_VIDEO_URLS = ENV_TOKENS.get('CDN_VIDEO_URLS', CDN_VIDEO_URLS)
......
...@@ -1277,7 +1277,6 @@ PIPELINE_CSS = { ...@@ -1277,7 +1277,6 @@ PIPELINE_CSS = {
'source_filenames': [ 'source_filenames': [
'sass/lms-main.css', 'sass/lms-main.css',
'css/edx-cc.css', 'css/edx-cc.css',
'css/edx-cc-ie7.css',
], ],
'output_filename': 'css/lms-main.css', 'output_filename': 'css/lms-main.css',
}, },
......
...@@ -137,25 +137,6 @@ a:focus { ...@@ -137,25 +137,6 @@ a:focus {
min-width: 760px; min-width: 760px;
width: flex-grid(12); width: flex-grid(12);
} }
.container-footer {
max-width: grid-width(12);
min-width: 760px;
width: flex-grid(12);
margin: 0 auto;
text-align: $bi-app-right;
color: $gray-l2;
span {
color: inherit;
}
a:link, a:visited {
color: $gray-l2;
}
a:active, a:hover {
color: $blue;
}
}
span.edx { span.edx {
text-transform: none; text-transform: none;
......
// lms - course - base // 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 { body {
min-width: 980px; min-width: 980px;
min-height: 100%; min-height: 100%;
...@@ -26,25 +60,8 @@ a { ...@@ -26,25 +60,8 @@ a {
} }
} }
.content-wrapper { // +Resets - Old, Forms
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);
}
}
form { form {
label { label {
display: block; display: block;
...@@ -102,6 +119,8 @@ button, ...@@ -102,6 +119,8 @@ button,
} }
// +Resets - Old, Images
// ====================
img { img {
max-width: 100%; max-width: 100%;
} }
...@@ -134,6 +153,9 @@ img { ...@@ -134,6 +153,9 @@ img {
} }
} }
// +Resets - Old, Misc
// ====================
.test-class { .test-class {
border: 1px solid #f00; border: 1px solid #f00;
} }
......
...@@ -41,61 +41,63 @@ html.video-fullscreen{ ...@@ -41,61 +41,63 @@ html.video-fullscreen{
} }
} }
div.course-wrapper { .content-wrapper {
position: relative;
.container-footer {
margin: 0 auto;
max-width: grid-width(12);
min-width: 760px;
width: flex-grid(12);
color: $gray;
text-align: $bi-app-right;
}
}
.course-license { .content-wrapper {
position: absolute;
margin: 5px auto; .course-license, .xblock-license {
bottom: 0; @include text-align(right);
text-align: center; @extend %t-title7;
width: 100%; display: block;
color: $gray-l3; width: auto;
padding: ($baseline/4) 0;
span {
color: inherit;
}
a:link, a:visited { a:link, a:visited {
color: $gray-l3; color: $gray;
} }
a:active, a:hover { a:active, a:hover {
color: $link-hover; color: $link-hover;
} }
}
.xblock-license {
text-align: $bi-app-right;
border-top: 1px solid $gray-l3;
margin: 0 15px;
padding: 5px;
font-size: 80%;
span { .license-label,
color: inherit; .license-value,
.license-actions {
display: inline-block;
vertical-align: middle;
margin-bottom: 0;
} }
&, a { i {
color: $gray-l3; font-style: normal;
&:hover {
color: $link-hover;
}
} }
}
.xmodule_display .xblock-license,
.xmodule_display .xblock-license a {
color: $gray-l3;
&:hover { img {
color: $link-hover; display: inline;
} }
} }
}
.xmodule_VideoModule .xblock-license { // TO-DO should this be content wrapper?
border: 0; div.course-wrapper {
} position: relative;
section.course-content { section.course-content {
@extend .content; @extend .content;
padding: 40px; padding: ($baseline*2);
line-height: 1.6; line-height: 1.6;
h1 { h1 {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment