Commit ca64c665 by Sarina Canelake

Default course license to All Rights Reserved

Clicking on conflicting option box unchecks all conflicts
LMS: Clicking license text should bring to new window
updated styles to reflect html reuse inside of xblock edit modal area.

Add ARIA attributes to license for a11y

Gracefully handle re-selecting of selected license
parent a3887e95
......@@ -42,7 +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 = None
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
......@@ -80,7 +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)
course_details.license = getattr(descriptor, "license", None)
# 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)
......
......@@ -106,33 +106,18 @@ define(["js/views/license", "js/models/license", "js/common_helpers/template_hel
);
// SA and ND conflict
var SA = this.view.$("li[data-option=SA]");
expect(SA).toHaveClass("is-disabled");
// try to turn on SA option, fail
SA.click()
// no change
expect(this.model.get("options")).toEqual(
{"ver": "4.0", "BY": true, "NC": true, "ND": true, "SA": false}
);
// turn off ND
var ND = this.view.$("li[data-option=ND]");
expect(ND).not.toHaveClass("is-disabled");
ND.click()
expect(this.model.get("options")).toEqual(
{"ver": "4.0", "BY": true, "NC": true, "ND": false, "SA": false}
);
// turn on SA
SA = this.view.$("li[data-option=SA]");
expect(SA).not.toHaveClass("is-disabled");
// 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, fail
// try to turn on ND option
ND = this.view.$("li[data-option=ND]");
expect(ND).toHaveClass("is-disabled");
ND.click();
expect(this.model.get("options")).toEqual(
{"ver": "4.0", "BY": true, "NC": true, "ND": false, "SA": true}
{"ver": "4.0", "BY": true, "NC": true, "ND": true, "SA": false}
);
});
......@@ -147,7 +132,8 @@ define(["js/views/license", "js/models/license", "js/common_helpers/template_hel
this.view = new LicenseView({model: this.model, showPreview: true});
this.view.render()
expect(this.view.$(".license-preview").length).toEqual(1)
expect(this.view.$(".license-preview")).toHaveText("");
// 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");
......
......@@ -92,10 +92,17 @@ define(["js/views/baseview", "underscore"], function(BaseView, _) {
onLicenseClick: function(e) {
var $li = $(e.srcElement || e.target).closest('li');
var licenseType = $li.data("license");
this.model.set({
"type": licenseType,
"options": this.getDefaultOptionsForLicenseType(licenseType)
});
// 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) {
......@@ -117,17 +124,19 @@ define(["js/views/baseview", "underscore"], function(BaseView, _) {
licenseOptions[optionKey] = currentOptionValue;
}
// check for conflicts
if (currentOptionValue && optionInfo.conflictsWith &&
_.any(optionInfo.conflictsWith, function (key) { return licenseOptions[key];})) {
// conflict! don't set new options
// need some feedback here
return;
} else {
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")
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();
}
......
......@@ -879,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);
}
}
......@@ -985,101 +985,4 @@
}
}
}
}
// +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
......@@ -42,9 +42,7 @@
<% var optionInfo = license.options[optionKey]; %>
<% if (optionInfo.type == "boolean") { %>
<% var optionSelected = model.options[optionKey]; %>
<% var optionDisabled = optionInfo.disabled ||
(optionInfo.conflictsWith && _.any(optionInfo.conflictsWith, function(key) {return model.options[key];}));
%>
<% var optionDisabled = optionInfo.disabled %>
<li data-option="<%- optionKey %>"
class="action-item license-option
<% if (optionSelected) { print("is-selected"); } %>
......@@ -76,7 +74,7 @@
<%= gettext("License Display") %>
</h4>
<p class="tip">
<%= gettext("The following message will be displayed at the bottom of the courseware pages within your course.") %>
<%= 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 %>
......@@ -99,14 +97,17 @@
alt="<%- typeof licenseString == "string" ? licenseString : "" %>"
/>
<% } else { %>
<i aria-hidden="true" class="icon-cc"></i>
<% //<span> must come before <i> icon or else spacing gets messed up %>
<span class="sr">gettext("Creative Commons licensed content, with terms as follow:")&nbsp;</span><i aria-hidden="true" class="icon-cc"></i>
<% _.each(enabled, function(option) { %>
<i aria-hidden="true" class="icon-cc-<%- option %>"></i>
<span class="sr"><%- license.options[option.toUpperCase()].name %>&nbsp;</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>
......
......@@ -1023,8 +1023,9 @@ class CourseDescriptor(CourseFields, LicenseMixin, SequenceDescriptor):
wiki_xml_object.set('slug', self.wiki_slug)
xml_object.append(wiki_xml_object)
# handle license specifically
self.add_license_to_xml(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
......
......@@ -41,7 +41,7 @@ class LicenseMixin(XBlockMixin):
definition['license'] = license
return definition
def add_license_to_xml(self, node):
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.
......@@ -49,7 +49,7 @@ class LicenseMixin(XBlockMixin):
to this method, rather than reimplementing it in their XML export
functions.
"""
if getattr(self, "license", None):
if getattr(self, "license", default):
node.set('license', self.license)
......
......@@ -29,6 +29,10 @@ def parse_license(lic):
% 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")
......@@ -36,16 +40,17 @@ def parse_license(lic):
enabled = ["zero"]
version = license_options.get("ver", "1.0")
%>
<a rel="license" href="https://creativecommons.org/licenses/${'-'.join(enabled)}/${version}/">
<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:
<i aria-hidden="true" class="icon-cc"></i>
## <span> must come before <i> icon or else spacing gets messed up
<span class="sr">${_("Creative Commons licensed content, with terms as follow:")}&nbsp;</span><i aria-hidden="true" class="icon-cc"></i>
% for option in enabled:
<i aria-hidden="true" class="icon-cc-${option}"></i>
<span class="sr">${names[option]}&nbsp;</span><i aria-hidden="true" class="icon-cc-${option}"></i>
% endfor
<span class="license-text">${_("Some Rights Reserved")}</span>
% endif
......
......@@ -90,6 +90,9 @@ class CoursewarePage(CoursePage):
@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]
......
......@@ -488,6 +488,9 @@ 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)
......
......@@ -12,7 +12,6 @@ from selenium.webdriver.common.action_chains import ActionChains
from .course_page import CoursePage
from .container import ContainerPage
from .settings import SettingsPage
from .utils import set_input_value_and_save, set_input_value, click_css, confirm_prompt
......@@ -582,19 +581,11 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer):
@property
def license(self):
"""
Returns the course license text, if present. Else returns None.
"""
return self.q(css=".license-value").first.text[0]
def edit_course_start_date(self):
self.q(css=".status-release .action-edit a.action-button").click()
sp = SettingsPage(
self.browser,
self.course_info['course_org'],
self.course_info['course_num'],
self.course_info['course_run'],
)
sp.wait_for_page()
return sp
class CourseOutlineModal(object):
MODAL_SELECTOR = ".wrapper-modal-window"
......
......@@ -81,6 +81,10 @@ class SettingsPage(CoursePage):
@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,
......@@ -89,10 +93,20 @@ class SettingsPage(CoursePage):
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,
......
......@@ -406,7 +406,11 @@ class AdvancedSettingsValidationTest(StudioCourseTest):
@attr('shard_1')
class ContentLicenseTest(StudioCourseTest):
def setUp(self):
"""
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,
......@@ -429,13 +433,13 @@ class ContentLicenseTest(StudioCourseTest):
def test_empty_license(self):
"""
When I visit the Studio settings page,
I see that the course license is "None" by default.
I see that the course license is "All Rights Reserved" by default.
Then I visit the LMS courseware page,
and I see that there is no course license displayed.
and I see that the default course license is displayed.
"""
self.assertIsNone(self.settings_page.course_license)
self.assertEqual(self.settings_page.course_license, "All Rights Reserved")
self.lms_courseware.visit()
self.assertIsNone(self.lms_courseware.course_license)
self.assertEqual(self.lms_courseware.course_license, "© All Rights Reserved")
def test_arr_license(self):
"""
......@@ -469,4 +473,6 @@ class ContentLicenseTest(StudioCourseTest):
self.assertEqual(self.settings_page.course_license, "Creative Commons")
self.lms_courseware.visit()
self.assertEqual(self.lms_courseware.course_license, "Some Rights Reserved")
# 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)
# 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
......@@ -11,8 +14,11 @@ from ...fixtures.course import XBlockFixtureDesc
@attr('shard_1')
class VideoLicenseTest(StudioCourseTest):
def setUp(self):
"""
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(
......@@ -107,4 +113,4 @@ class VideoLicenseTest(StudioCourseTest):
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], "Some Rights Reserved")
self.assertIn("Some Rights Reserved", video_license.text[0])
......@@ -369,6 +369,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,
......
......@@ -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
......
......@@ -207,9 +207,14 @@ ${fragment.foot_html()}
</div>
</div>
<div class="container-footer">
% if settings.FEATURES.get("LICENSING", False) and getattr(course, "license", None):
% 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>
......
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