From bab442829742deffe6e4996ae251865b963351cc Mon Sep 17 00:00:00 2001 From: Dmitry Viskov Date: Mon, 25 Jan 2016 22:38:39 +0300 Subject: [PATCH 1/6] Invalid StudioPermissionsService object in API to show/save xblock settings in CMS. Randomized Content Block editor did not check Studio user's permissions --- .../contentstore/tests/test_libraries.py | 70 ++++++++++++++++++- .../contentstore/views/component.py | 3 +- cms/djangoapps/contentstore/views/item.py | 50 +++++++++++++ cms/djangoapps/contentstore/views/preview.py | 26 ------- .../xmodule/xmodule/library_content_module.py | 10 ++- common/lib/xmodule/xmodule/x_module.py | 5 +- 6 files changed, 127 insertions(+), 37 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_libraries.py b/cms/djangoapps/contentstore/tests/test_libraries.py index 6f02429b08..5558c57625 100644 --- a/cms/djangoapps/contentstore/tests/test_libraries.py +++ b/cms/djangoapps/contentstore/tests/test_libraries.py @@ -23,6 +23,8 @@ from mock import Mock from opaque_keys.edx.locator import CourseKey, LibraryLocator from openedx.core.djangoapps.content.course_structures.tests import SignalDisconnectTestMixin from xblock_django.user_service import DjangoXBlockUserService +from xmodule.x_module import STUDIO_VIEW +from student import auth class LibraryTestCase(ModuleStoreTestCase): @@ -30,16 +32,22 @@ class LibraryTestCase(ModuleStoreTestCase): Common functionality for content libraries tests """ def setUp(self): - user_password = super(LibraryTestCase, self).setUp() + self.user_password = super(LibraryTestCase, self).setUp() self.client = AjaxEnabledTestClient() - self.client.login(username=self.user.username, password=user_password) + self._login_as_staff_user(logout_first=False) self.lib_key = self._create_library() self.library = modulestore().get_library(self.lib_key) self.session_data = {} # Used by _bind_module + def _login_as_staff_user(self, logout_first=True): + """ Login as a staff user """ + if logout_first: + self.client.logout() + self.client.login(username=self.user.username, password=self.user_password) + def _create_library(self, org="org", library="lib", display_name="Test Library"): """ Helper method used to create a library. Uses the REST API. @@ -729,6 +737,64 @@ class TestLibraryAccess(SignalDisconnectTestMixin, LibraryTestCase): lc_block = self._refresh_children(lc_block, status_code_expected=200 if expected_result else 403) self.assertEqual(len(lc_block.children), 1 if expected_result else 0) + def test_studio_user_permissions(self): + """ + Test that user could attach to the problem only libraries that he has access (or which were created by him). + This test was created on the basis of bug described in the pull requests on github: + https://github.com/edx/edx-platform/pull/11331 + https://github.com/edx/edx-platform/pull/11611 + """ + self._create_library(org='admin_org_1', library='lib_adm_1', display_name='admin_lib_1') + self._create_library(org='admin_org_2', library='lib_adm_2', display_name='admin_lib_2') + + self._login_as_non_staff_user() + + self._create_library(org='staff_org_1', library='lib_staff_1', display_name='staff_lib_1') + self._create_library(org='staff_org_2', library='lib_staff_2', display_name='staff_lib_2') + + with modulestore().default_store(ModuleStoreEnum.Type.split): + course = CourseFactory.create() + + instructor_role = CourseInstructorRole(course.id) + auth.add_users(self.user, instructor_role, self.non_staff_user) + + lib_block = ItemFactory.create( + category='library_content', + parent_location=course.location, + user_id=self.non_staff_user.id, + publish_item=False + ) + + def _get_settings_html(): + """ + Helper function to get block settings HTML + Used to check the available libraries. + """ + edit_view_url = reverse_usage_url("xblock_view_handler", lib_block.location, {"view_name": STUDIO_VIEW}) + + resp = self.client.get_json(edit_view_url) + self.assertEquals(resp.status_code, 200) + + return parse_json(resp)['html'] + + self._login_as_staff_user() + staff_settings_html = _get_settings_html() + self.assertIn('staff_lib_1', staff_settings_html) + self.assertIn('staff_lib_2', staff_settings_html) + self.assertIn('admin_lib_1', staff_settings_html) + self.assertIn('admin_lib_2', staff_settings_html) + + self._login_as_non_staff_user() + response = self.client.get_json(LIBRARY_REST_URL) + staff_libs = parse_json(response) + self.assertEquals(2, len(staff_libs)) + + non_staff_settings_html = _get_settings_html() + self.assertIn('staff_lib_1', non_staff_settings_html) + self.assertIn('staff_lib_2', non_staff_settings_html) + self.assertNotIn('admin_lib_1', non_staff_settings_html) + self.assertNotIn('admin_lib_2', non_staff_settings_html) + @ddt.ddt @override_settings(SEARCH_ENGINE=None) diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index d7ac594700..68c7d78e18 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -21,7 +21,7 @@ from xblock.runtime import Mixologist from contentstore.utils import get_lms_link_for_item from contentstore.views.helpers import get_parent_xblock, is_unit, xblock_type_display_name -from contentstore.views.item import create_xblock_info, add_container_page_publishing_info +from contentstore.views.item import create_xblock_info, add_container_page_publishing_info, StudioEditModuleRuntime from opaque_keys.edx.keys import UsageKey @@ -330,6 +330,7 @@ def component_handler(request, usage_key_string, handler, suffix=''): usage_key = UsageKey.from_string(usage_key_string) descriptor = modulestore().get_item(usage_key) + descriptor.xmodule_runtime = StudioEditModuleRuntime(request.user) # Let the module handle the AJAX req = django_to_webob_request(request) diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py index b75a0be37f..d60c570efc 100644 --- a/cms/djangoapps/contentstore/views/item.py +++ b/cms/djangoapps/contentstore/views/item.py @@ -21,6 +21,7 @@ from opaque_keys.edx.locator import LibraryUsageLocator from pytz import UTC from xblock.fields import Scope from xblock.fragment import Fragment +from xblock_django.user_service import DjangoXBlockUserService from cms.lib.xblock.authoring_mixin import VISIBILITY_VIEW from contentstore.utils import ( @@ -51,6 +52,7 @@ from xmodule.modulestore.inheritance import own_metadata from xmodule.tabs import CourseTabList from xmodule.x_module import PREVIEW_VIEWS, STUDIO_VIEW, STUDENT_VIEW, DEPRECATION_VSCOMPAT_EVENT + __all__ = [ 'orphan_handler', 'xblock_handler', 'xblock_view_handler', 'xblock_outline_handler', 'xblock_container_handler' ] @@ -198,6 +200,49 @@ def xblock_handler(request, usage_key_string): ) +class StudioPermissionsService(object): + """ + Service that can provide information about a user's permissions. + + Deprecated. To be replaced by a more general authorization service. + + Only used by LibraryContentDescriptor (and library_tools.py). + """ + def __init__(self, user): + self._user = user + + def can_read(self, course_key): + """ Does the user have read access to the given course/library? """ + return has_studio_read_access(self._user, course_key) + + def can_write(self, course_key): + """ Does the user have read access to the given course/library? """ + return has_studio_write_access(self._user, course_key) + + +class StudioEditModuleRuntime(object): + """ + An extremely minimal ModuleSystem shim used for XBlock edits and studio_view. + (i.e. whenever we're not using PreviewModuleSystem.) This is required to make information + about the current user (especially permissions) available via services as needed. + """ + def __init__(self, user): + self._user = user + + def service(self, block, service_name): + """ + This block is not bound to a user but some blocks (LibraryContentModule) may need + user-specific services to check for permissions, etc. + If we return None here, CombinedSystem will load services from the descriptor runtime. + """ + if block.service_declaration(service_name) is not None: + if service_name == "user": + return DjangoXBlockUserService(self._user) + if service_name == "studio_user_permissions": + return StudioPermissionsService(self._user) + return None + + @require_http_methods(("GET")) @login_required @expect_json @@ -231,6 +276,9 @@ def xblock_view_handler(request, usage_key_string, view_name): )) if view_name in (STUDIO_VIEW, VISIBILITY_VIEW): + if view_name == STUDIO_VIEW and xblock.xmodule_runtime is None: + xblock.xmodule_runtime = StudioEditModuleRuntime(request.user) + try: fragment = xblock.render(view_name) # catch exceptions indiscriminately, since after this point they escape the @@ -375,6 +423,7 @@ def _update_with_callback(xblock, user, old_metadata=None, old_content=None): old_metadata = own_metadata(xblock) if old_content is None: old_content = xblock.get_explicitly_set_fields_by_scope(Scope.content) + xblock.xmodule_runtime = StudioEditModuleRuntime(user) xblock.editor_saved(user, old_metadata, old_content) # Update after the callback so any changes made in the callback will get persisted. @@ -624,6 +673,7 @@ def _duplicate_item(parent_usage_key, duplicate_source_usage_key, user, display_ # Allow an XBlock to do anything fancy it may need to when duplicated from another block. # These blocks may handle their own children or parenting if needed. Let them return booleans to # let us know if we need to handle these or not. + dest_module.xmodule_runtime = StudioEditModuleRuntime(user) children_handled = dest_module.studio_post_duplicate(store, source_item) # Children are not automatically copied over (and not all xblocks have a 'children' attribute). diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index b9d39e417b..f807bf92f4 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -15,7 +15,6 @@ from xmodule.x_module import PREVIEW_VIEWS, STUDENT_VIEW, AUTHOR_VIEW from xmodule.contentstore.django import contentstore from xmodule.error_module import ErrorDescriptor 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 @@ -150,28 +149,6 @@ class PreviewModuleSystem(ModuleSystem): # pylint: disable=abstract-method return result -class StudioPermissionsService(object): - """ - Service that can provide information about a user's permissions. - - Deprecated. To be replaced by a more general authorization service. - - Only used by LibraryContentDescriptor (and library_tools.py). - """ - - def __init__(self, request): - super(StudioPermissionsService, self).__init__() - self._request = request - - def can_read(self, course_key): - """ Does the user have read access to the given course/library? """ - return has_studio_read_access(self._request.user, course_key) - - def can_write(self, course_key): - """ Does the user have read access to the given course/library? """ - return has_studio_write_access(self._request.user, course_key) - - def _preview_module_system(request, descriptor, field_data): """ Returns a ModuleSystem for the specified descriptor that is specialized for @@ -213,8 +190,6 @@ def _preview_module_system(request, descriptor, field_data): # 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( static_url=settings.STATIC_URL, # TODO (cpennington): Do we want to track how instructors are using the preview problems? @@ -241,7 +216,6 @@ def _preview_module_system(request, descriptor, field_data): services={ "field-data": field_data, "i18n": ModuleI18nService, - "library_tools": LibraryToolsService(modulestore()), "settings": SettingsService(), "user": DjangoXBlockUserService(request.user), }, diff --git a/common/lib/xmodule/xmodule/library_content_module.py b/common/lib/xmodule/xmodule/library_content_module.py index 9a5593274e..309dc549d5 100644 --- a/common/lib/xmodule/xmodule/library_content_module.py +++ b/common/lib/xmodule/xmodule/library_content_module.py @@ -573,12 +573,10 @@ class LibraryContentDescriptor(LibraryContentFields, MakoModuleDescriptor, XmlDe """ lib_tools = self.runtime.service(self, 'library_tools') user_perms = self.runtime.service(self, 'studio_user_permissions') - all_libraries = lib_tools.list_available_libraries() - if user_perms: - all_libraries = [ - (key, name) for key, name in all_libraries - if user_perms.can_read(key) or self.source_library_id == unicode(key) - ] + all_libraries = [ + (key, name) for key, name in lib_tools.list_available_libraries() + if user_perms.can_read(key) or self.source_library_id == unicode(key) + ] all_libraries.sort(key=lambda entry: entry[1]) # Sort by name if self.source_library_id and self.source_library_key not in [entry[0] for entry in all_libraries]: all_libraries.append((self.source_library_id, _(u"Invalid Library"))) diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index b349a82747..6dbfadc815 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -1482,8 +1482,9 @@ class DescriptorSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime): """ potential_set = set(super(DescriptorSystem, self).applicable_aside_types(block)) if getattr(block, 'xmodule_runtime', None) is not None: - application_set = set(block.xmodule_runtime.applicable_aside_types(block)) - return list(potential_set.intersection(application_set)) + if hasattr(block.xmodule_runtime, 'applicable_aside_types'): + application_set = set(block.xmodule_runtime.applicable_aside_types(block)) + return list(potential_set.intersection(application_set)) return list(potential_set) def resource_url(self, resource): From d9bb20686876db474ff84ebe09ac5230b1ebddd7 Mon Sep 17 00:00:00 2001 From: cahrens Date: Mon, 2 May 2016 10:27:33 -0400 Subject: [PATCH 2/6] Jasmine-jquery was moved to npm. --- common/static/js/vendor/jasmine-jquery.js | 366 ---------------------- 1 file changed, 366 deletions(-) delete mode 100644 common/static/js/vendor/jasmine-jquery.js diff --git a/common/static/js/vendor/jasmine-jquery.js b/common/static/js/vendor/jasmine-jquery.js deleted file mode 100644 index 6a9a3aba98..0000000000 --- a/common/static/js/vendor/jasmine-jquery.js +++ /dev/null @@ -1,366 +0,0 @@ -var readFixtures = function() { - return jasmine.getFixtures().proxyCallTo_('read', arguments) -} - -var preloadFixtures = function() { - jasmine.getFixtures().proxyCallTo_('preload', arguments) -} - -var loadFixtures = function() { - jasmine.getFixtures().proxyCallTo_('load', arguments) -} - -var appendLoadFixtures = function() { - jasmine.getFixtures().proxyCallTo_('appendLoad', arguments) -} - -var setFixtures = function(html) { - jasmine.getFixtures().proxyCallTo_('set', arguments) -} - -var appendSetFixtures = function() { - jasmine.getFixtures().proxyCallTo_('appendSet', arguments) -} - -var sandbox = function(attributes) { - return jasmine.getFixtures().sandbox(attributes) -} - -var spyOnEvent = function(selector, eventName) { - jasmine.JQuery.events.spyOn(selector, eventName) -} - -jasmine.getFixtures = function() { - return jasmine.currentFixtures_ = jasmine.currentFixtures_ || new jasmine.Fixtures() -} - -jasmine.Fixtures = function() { - this.containerId = 'jasmine-fixtures' - this.fixturesCache_ = {} - this.fixturesPath = 'spec/javascripts/fixtures' -} - -jasmine.Fixtures.prototype.set = function(html) { - this.cleanUp() - this.createContainer_(html) -} - -jasmine.Fixtures.prototype.appendSet= function(html) { - this.addToContainer_(html) -} - -jasmine.Fixtures.prototype.preload = function() { - this.read.apply(this, arguments) -} - -jasmine.Fixtures.prototype.load = function() { - this.cleanUp() - this.createContainer_(this.read.apply(this, arguments)) -} - -jasmine.Fixtures.prototype.appendLoad = function() { - this.addToContainer_(this.read.apply(this, arguments)) -} - -jasmine.Fixtures.prototype.read = function() { - var htmlChunks = [] - - var fixtureUrls = arguments - for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) { - htmlChunks.push(this.getFixtureHtml_(fixtureUrls[urlIndex])) - } - - return htmlChunks.join('') -} - -jasmine.Fixtures.prototype.clearCache = function() { - this.fixturesCache_ = {} -} - -jasmine.Fixtures.prototype.cleanUp = function() { - jQuery('#' + this.containerId).remove() -} - -jasmine.Fixtures.prototype.sandbox = function(attributes) { - var attributesToSet = attributes || {} - return jQuery('
').attr(attributesToSet) -} - -jasmine.Fixtures.prototype.createContainer_ = function(html) { - var container - if(html instanceof jQuery) { - container = jQuery('
') - container.html(html) - } else { - container = '
' + html + '
' - } - jQuery('body').append(container) -} - -jasmine.Fixtures.prototype.addToContainer_ = function(html){ - var container = jQuery('body').find('#'+this.containerId).append(html) - if(!container.length){ - this.createContainer_(html) - } -} - -jasmine.Fixtures.prototype.getFixtureHtml_ = function(url) { - if (typeof this.fixturesCache_[url] === 'undefined') { - this.loadFixtureIntoCache_(url) - } - return this.fixturesCache_[url] -} - -jasmine.Fixtures.prototype.loadFixtureIntoCache_ = function(relativeUrl) { - var url = this.makeFixtureUrl_(relativeUrl) - var request = new XMLHttpRequest() - request.open("GET", url + "?" + new Date().getTime(), false) - request.send(null) - this.fixturesCache_[relativeUrl] = request.responseText -} - -jasmine.Fixtures.prototype.makeFixtureUrl_ = function(relativeUrl){ - return this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl -} - -jasmine.Fixtures.prototype.proxyCallTo_ = function(methodName, passedArguments) { - return this[methodName].apply(this, passedArguments) -} - - -jasmine.JQuery = function() {} - -jasmine.JQuery.browserTagCaseIndependentHtml = function(html) { - return jQuery('
').append(html).html() -} - -jasmine.JQuery.elementToString = function(element) { - var domEl = $(element).get(0) - if (domEl == undefined || domEl.cloneNode) - return jQuery('
').append($(element).clone()).html() - else - return element.toString() -} - -jasmine.JQuery.matchersClass = {}; - -!function(namespace) { - var data = { - spiedEvents: {}, - handlers: [] - } - - namespace.events = { - spyOn: function(selector, eventName) { - var handler = function(e) { - data.spiedEvents[[selector, eventName]] = e - } - jQuery(selector).bind(eventName, handler) - data.handlers.push(handler) - }, - - wasTriggered: function(selector, eventName) { - return !!(data.spiedEvents[[selector, eventName]]) - }, - - wasPrevented: function(selector, eventName) { - return data.spiedEvents[[selector, eventName]].isDefaultPrevented() - }, - - cleanUp: function() { - data.spiedEvents = {} - data.handlers = [] - } - } -}(jasmine.JQuery) - -!function(){ - var jQueryMatchers = { - toHaveClass: function(className) { - return this.actual.hasClass(className) - }, - - toHaveCss: function(css){ - for (var prop in css){ - if (this.actual.css(prop) !== css[prop]) return false - } - return true - }, - - toBeVisible: function() { - return this.actual.is(':visible') - }, - - toBeHidden: function() { - return this.actual.is(':hidden') - }, - - toBeSelected: function() { - return this.actual.is(':selected') - }, - - toBeChecked: function() { - return this.actual.is(':checked') - }, - - toBeEmpty: function() { - return this.actual.is(':empty') - }, - - toExist: function() { - return $(document).find(this.actual).length - }, - - toHaveAttr: function(attributeName, expectedAttributeValue) { - return hasProperty(this.actual.attr(attributeName), expectedAttributeValue) - }, - - toHaveProp: function(propertyName, expectedPropertyValue) { - return hasProperty(this.actual.prop(propertyName), expectedPropertyValue) - }, - - toHaveId: function(id) { - return this.actual.attr('id') == id - }, - - toHaveHtml: function(html) { - return this.actual.html() == jasmine.JQuery.browserTagCaseIndependentHtml(html) - }, - - toContainHtml: function(html){ - var actualHtml = this.actual.html() - var expectedHtml = jasmine.JQuery.browserTagCaseIndependentHtml(html) - return (actualHtml.indexOf(expectedHtml) >= 0) - }, - - toHaveText: function(text) { - var trimmedText = $.trim(this.actual.text()) - if (text && jQuery.isFunction(text.test)) { - return text.test(trimmedText) - } else { - return trimmedText == text - } - }, - - toHaveValue: function(value) { - return this.actual.val() == value - }, - - toHaveData: function(key, expectedValue) { - return hasProperty(this.actual.data(key), expectedValue) - }, - - toBe: function(selector) { - return this.actual.is(selector) - }, - - toContain: function(selector) { - return this.actual.find(selector).length - }, - - toBeDisabled: function(selector){ - return this.actual.is(':disabled') - }, - - toBeFocused: function(selector) { - return this.actual.is(':focus') - }, - - toHandle: function(event) { - - var events = this.actual.data('events') - - if(!events || !event || typeof event !== "string") { - return false - } - - var namespaces = event.split(".") - var eventType = namespaces.shift() - var sortedNamespaces = namespaces.slice(0).sort() - var namespaceRegExp = new RegExp("(^|\\.)" + sortedNamespaces.join("\\.(?:.*\\.)?") + "(\\.|$)") - - if(events[eventType] && namespaces.length) { - for(var i = 0; i < events[eventType].length; i++) { - var namespace = events[eventType][i].namespace - if(namespaceRegExp.test(namespace)) { - return true - } - } - } else { - return events[eventType] && events[eventType].length > 0 - } - }, - - // tests the existence of a specific event binding + handler - toHandleWith: function(eventName, eventHandler) { - var stack = this.actual.data("events")[eventName] - for (var i = 0; i < stack.length; i++) { - if (stack[i].handler == eventHandler) return true - } - return false - } - } - - var hasProperty = function(actualValue, expectedValue) { - if (expectedValue === undefined) return actualValue !== undefined - return actualValue == expectedValue - } - - var bindMatcher = function(methodName) { - var builtInMatcher = jasmine.Matchers.prototype[methodName] - - jasmine.JQuery.matchersClass[methodName] = function() { - if (this.actual - && (this.actual instanceof jQuery - || jasmine.isDomNode(this.actual))) { - this.actual = $(this.actual) - var result = jQueryMatchers[methodName].apply(this, arguments) - var element; - if (this.actual.get && (element = this.actual.get()[0]) && !$.isWindow(element) && element.tagName !== "HTML") - this.actual = jasmine.JQuery.elementToString(this.actual) - return result - } - - if (builtInMatcher) { - return builtInMatcher.apply(this, arguments) - } - - return false - } - } - - for(var methodName in jQueryMatchers) { - bindMatcher(methodName) - } -}() - -beforeEach(function() { - this.addMatchers(jasmine.JQuery.matchersClass) - this.addMatchers({ - toHaveBeenTriggeredOn: function(selector) { - this.message = function() { - return [ - "Expected event " + this.actual + " to have been triggered on " + selector, - "Expected event " + this.actual + " not to have been triggered on " + selector - ] - } - return jasmine.JQuery.events.wasTriggered($(selector), this.actual) - } - }) - this.addMatchers({ - toHaveBeenPreventedOn: function(selector) { - this.message = function() { - return [ - "Expected event " + this.actual + " to have been prevented on " + selector, - "Expected event " + this.actual + " not to have been prevented on " + selector - ] - } - return jasmine.JQuery.events.wasPrevented(selector, this.actual) - } - }) -}) - -afterEach(function() { - jasmine.getFixtures().cleanUp() - jasmine.JQuery.events.cleanUp() -}) From 2c8f3c8046f900b3274c341c130a12912e5248ee Mon Sep 17 00:00:00 2001 From: AlasdairSwan Date: Fri, 29 Apr 2016 15:28:51 -0400 Subject: [PATCH 3/6] Styled footer on pattern library pages --- lms/static/sass/_build-lms-v2.scss | 2 + lms/static/sass/base-v2/_extends.scss | 6 + lms/static/sass/shared-v2/_footer.scss | 196 ++++++++++++++++++++++++ lms/static/sass/shared-v2/_header.scss | 4 +- lms/static/sass/shared/_footer-edx.scss | 4 + lms/static/sass/shared/_footer.scss | 85 ---------- lms/templates/footer.html | 2 +- lms/templates/navigation.html | 2 +- 8 files changed, 212 insertions(+), 89 deletions(-) create mode 100644 lms/static/sass/base-v2/_extends.scss create mode 100644 lms/static/sass/shared-v2/_footer.scss diff --git a/lms/static/sass/_build-lms-v2.scss b/lms/static/sass/_build-lms-v2.scss index b20fe16b6b..470ce959c7 100644 --- a/lms/static/sass/_build-lms-v2.scss +++ b/lms/static/sass/_build-lms-v2.scss @@ -6,8 +6,10 @@ // Configuration @import 'config'; @import 'base/variables'; +@import 'base-v2/extends'; // Extensions @import 'shared-v2/base'; @import 'shared-v2/navigation'; @import 'shared-v2/header'; +@import 'shared-v2/footer'; diff --git a/lms/static/sass/base-v2/_extends.scss b/lms/static/sass/base-v2/_extends.scss new file mode 100644 index 0000000000..e2d6206c75 --- /dev/null +++ b/lms/static/sass/base-v2/_extends.scss @@ -0,0 +1,6 @@ +// Adds a simple extend that indicates that this user interface element should not print +%ui-print-excluded { + @media print { + display:none; + } +} diff --git a/lms/static/sass/shared-v2/_footer.scss b/lms/static/sass/shared-v2/_footer.scss new file mode 100644 index 0000000000..8951da58af --- /dev/null +++ b/lms/static/sass/shared-v2/_footer.scss @@ -0,0 +1,196 @@ +// Open edX: LMS footer +// ==================== + +.wrapper-footer { + @extend %ui-print-excluded; + margin-top: ($baseline*2) + px; + box-shadow: 0 -1px 5px 0 $shadow-l1; + border-top: 1px solid tint(palette(grayscale, light), 50%); + padding: 25px ($baseline/2 + px) ($baseline*1.5 + px) ($baseline/2 + px); + background: $footer-bg; + clear: both; + + footer#footer-openedx { + @include clearfix(); + box-sizing: border-box; + margin: 0 auto; + + p, ol, ul { + font-family: $sans-serif; + + // override needed for poorly scoped font-family styling on p a:link {} + a { + font-family: $sans-serif; + } + } + + a { + @extend %link-text; + border-bottom: none; + + &:hover, + &:focus, + &:active { + border-bottom: 1px dotted $link-color; + } + } + + // colophon + .colophon { + @include span(12); + + @include susy-media($bp-screen-sm) { + @include span(8); + } + + .nav-colophon { + @include clearfix(); + margin: $footer_margin; + + li { + @include float(left); + margin-right: ($baseline*0.75) + px; + + a { + color: tint($black, 20%); + + &:hover, + &:focus, + &:active { + color: $link-color; + } + } + + &:last-child { + @include margin-right(0); + } + } + } + + .colophon-about { + @include clearfix(); + + img { + @include float(left); + width: 68px; + height: 34px; + margin-right: 0; + } + + p { + @include float(left); + @include span(9); + margin-left: $baseline + px; + padding-left: $baseline + px; + font-size: font-size(small); + background: transparent url(/static/images/bg-footer-divider.jpg) 0 0 no-repeat; + } + } + } + + // references + .references { + @include span(4); + margin: -10px 0 0 0; + display: inline-block; + } + + .wrapper-logo { + margin: ($baseline*0.75) + px 0; + + a { + display: inline-block; + + &:hover { + border-bottom: 0; + } + } + } + + .copyright { + @include text-align(left); + margin: -2px 0 8px 0; + font-size: font-size(xx-small); + color: palette(grayscale, dark); + } + + .nav-legal { + @include clearfix(); + @include text-align(left); + + li { + display: inline-block; + font-size: font-size(xx-small); + } + + .nav-legal-02 a { + &:before { + @include margin-right(($baseline/4) + px); + content: "-"; + } + } + } + + .nav-social { + @include text-align(right); + margin: 0; + + li { + display: inline-block; + + &:last-child { + margin-right: 0; + } + + a { + display: block; + + &:hover, + &:focus, + &:active { + border: none; + } + } + + img { + display: block; + } + } + } + + // platform Open edX logo and link + .footer-about-openedx { + @include span(12); + @include text-align(right); + vertical-align: bottom; + + @include susy-media($bp-screen-sm) { + @include span(4); + @include margin-right(0); + } + + + a { + @include float(right); + display: inline-block; + + &:hover { + border-bottom: none; + } + } + } + } +} + +// marketing site design syncing +.view-register, +.view-login, +.view-passwordreset { + .wrapper-footer footer { + width: 960px; + + .colophon-about img { + margin-top: ($baseline*1.5) + px; + } + } +} diff --git a/lms/static/sass/shared-v2/_header.scss b/lms/static/sass/shared-v2/_header.scss index 76a0d3398d..0998ed4c01 100644 --- a/lms/static/sass/shared-v2/_header.scss +++ b/lms/static/sass/shared-v2/_header.scss @@ -2,7 +2,7 @@ .header-global { @extend %ui-depth1; - @include box-sizing(border-box); + box-sizing: border-box; position: relative; width: 100%; border-bottom: 4px solid $courseware-border-bottom-color; @@ -11,7 +11,7 @@ .wrapper-header { @include clearfix(); - @include box-sizing(border-box); + box-sizing: border-box; height: 74px; margin: 0 auto; padding: 10px 10px 0; diff --git a/lms/static/sass/shared/_footer-edx.scss b/lms/static/sass/shared/_footer-edx.scss index c877108b28..f6d6776ecf 100644 --- a/lms/static/sass/shared/_footer-edx.scss +++ b/lms/static/sass/shared/_footer-edx.scss @@ -17,6 +17,10 @@ footer#footer-edx-v3 { background: $edx-footer-bg-color; padding: 20px; border-top: 1px solid $courseware-button-border-color; + + // To match the Pattern Library + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; .footer-content-wrapper { @include outer-container; diff --git a/lms/static/sass/shared/_footer.scss b/lms/static/sass/shared/_footer.scss index 635b59842e..a19c655438 100644 --- a/lms/static/sass/shared/_footer.scss +++ b/lms/static/sass/shared/_footer.scss @@ -191,9 +191,7 @@ // edx theme overrides &.edx-footer { - footer { - .copyright { text-align: right; } @@ -216,86 +214,3 @@ } } } - - -// edX theme: LMS Footer -// ==================== -$edx-footer-spacing: ($baseline*0.75); -$edx-footer-link-color: $link-color; -$edx-footer-bg-color: rgb(252,252,252); - -%edx-footer-reset { - @include box-sizing(border-box); -} - -%edx-footer-section { - @include float(left); - min-height: ($baseline*17.5); - @include margin-right(flex-gutter()); - @include border-right(1px solid rgb(230, 230, 230)); - @include padding-right($baseline*1.5); - - // CASE: last child - &:last-child { - @include margin-right(0); - border: none; - @include padding-right(0); - } -} - -%edx-footer-title { - // TODO: refactor _typography.scss to extend this set of styling - @extend %t-title; - @extend %t-weight4; - @include font-size(15); - @include line-height(15); - text-transform: none; - letter-spacing: inherit; - color: rgb(61, 62, 63); -} - -%edx-footer-link { - @extend %t-copy-sub1; - @include transition(color $tmg-f2 ease-in-out 0); - display: block; - margin-bottom: ($baseline/2); - - // NOTE: resetting poor link styles - border: none; - padding: 0; - color: $edx-footer-link-color; - - .copy { - @include transition(border-color $tmg-f2 ease-in-out 0); - display: inline-block; - border-bottom: 1px solid transparent; - padding: 0 0 ($baseline/20) 0; - color: $edx-footer-link-color; - } - - // STATE: hover + focused - &:hover, &:focus { - color: saturate($edx-footer-link-color, 25%); - - // NOTE: resetting poor link styles - border: none; - - .copy { - border-bottom-color: saturate($edx-footer-link-color, 25%); - } - } - - // CASE: last child - &:last-child { - margin-bottom: 0; - } - - // CASE: has visual emphasis - &.has-emphasis { - @extend %t-weight4; - - .copy { - @extend %t-weight4; - } - } -} diff --git a/lms/templates/footer.html b/lms/templates/footer.html index 1ca055d128..b0e9fa85f2 100644 --- a/lms/templates/footer.html +++ b/lms/templates/footer.html @@ -9,7 +9,7 @@ <%namespace name='static' file='static_content.html'/>