diff --git a/.pylintrc b/.pylintrc index 6690bb7df0..a9f19ca667 100644 --- a/.pylintrc +++ b/.pylintrc @@ -41,7 +41,8 @@ disable= # R0902: Too many instance attributes # R0903: Too few public methods (1/2) # R0904: Too many public methods - W0141,W0142,R0201,R0901,R0902,R0903,R0904 +# R0913: Too many arguments + W0141,W0142,R0201,R0901,R0902,R0903,R0904,R0913 [REPORTS] diff --git a/cms/djangoapps/contentstore/course_info_model.py b/cms/djangoapps/contentstore/course_info_model.py index 8c8aed549d..589db4ac56 100644 --- a/cms/djangoapps/contentstore/course_info_model.py +++ b/cms/djangoapps/contentstore/course_info_model.py @@ -1,13 +1,15 @@ from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore -from lxml import html, etree +from lxml import html import re from django.http import HttpResponseBadRequest import logging +import django.utils -## TODO store as array of { date, content } and override course_info_module.definition_from_xml -## This should be in a class which inherits from XmlDescriptor +# # TODO store as array of { date, content } and override course_info_module.definition_from_xml +# # This should be in a class which inherits from XmlDescriptor +log = logging.getLogger(__name__) def get_course_updates(location): @@ -26,9 +28,11 @@ def get_course_updates(location): # purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break. try: - course_html_parsed = etree.fromstring(course_updates.data) - except etree.XMLSyntaxError: - course_html_parsed = etree.fromstring("
    ") + course_html_parsed = html.fromstring(course_updates.data) + except: + log.error("Cannot parse: " + course_updates.data) + escaped = django.utils.html.escape(course_updates.data) + course_html_parsed = html.fromstring("
    1. " + escaped + "
    ") # Confirm that root is
      , iterate over
    1. , pull out

      subs and then rest of val course_upd_collection = [] @@ -64,9 +68,11 @@ def update_course_updates(location, update, passed_id=None): # purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break. try: - course_html_parsed = etree.fromstring(course_updates.data) - except etree.XMLSyntaxError: - course_html_parsed = etree.fromstring("
        ") + course_html_parsed = html.fromstring(course_updates.data) + except: + log.error("Cannot parse: " + course_updates.data) + escaped = django.utils.html.escape(course_updates.data) + course_html_parsed = html.fromstring("
        1. " + escaped + "
        ") # No try/catch b/c failure generates an error back to client new_html_parsed = html.fromstring('
      1. ' + update['date'] + '

        ' + update['content'] + '
      2. ') @@ -85,12 +91,19 @@ def update_course_updates(location, update, passed_id=None): passed_id = course_updates.location.url() + "/" + str(idx) # update db record - course_updates.data = etree.tostring(course_html_parsed) + course_updates.data = html.tostring(course_html_parsed) modulestore('direct').update_item(location, course_updates.data) - return {"id" : passed_id, - "date" : update['date'], - "content" :update['content']} + if (len(new_html_parsed) == 1): + content = new_html_parsed[0].tail + else: + content = "\n".join([html.tostring(ele) + for ele in new_html_parsed[1:]]) + + return {"id": passed_id, + "date": update['date'], + "content": content} + def delete_course_update(location, update, passed_id): """ @@ -108,9 +121,11 @@ def delete_course_update(location, update, passed_id): # TODO use delete_blank_text parser throughout and cache as a static var in a class # purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break. try: - course_html_parsed = etree.fromstring(course_updates.data) - except etree.XMLSyntaxError: - course_html_parsed = etree.fromstring("
          ") + course_html_parsed = html.fromstring(course_updates.data) + except: + log.error("Cannot parse: " + course_updates.data) + escaped = django.utils.html.escape(course_updates.data) + course_html_parsed = html.fromstring("
          1. " + escaped + "
          ") if course_html_parsed.tag == 'ol': # ??? Should this use the id in the json or in the url or does it matter? @@ -121,7 +136,7 @@ def delete_course_update(location, update, passed_id): course_html_parsed.remove(element_to_delete) # update db record - course_updates.data = etree.tostring(course_html_parsed) + course_updates.data = html.tostring(course_html_parsed) store = modulestore('direct') store.update_item(location, course_updates.data) @@ -132,7 +147,6 @@ def get_idx(passed_id): """ From the url w/ idx appended, get the idx. """ - # TODO compile this regex into a class static and reuse for each call - idx_matcher = re.search(r'.*/(\d+)$', passed_id) + idx_matcher = re.search(r'.*?/?(\d+)$', passed_id) if idx_matcher: return int(idx_matcher.group(1)) diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 2ec0427e1d..820b60123b 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -7,8 +7,6 @@ from selenium.common.exceptions import WebDriverException, StaleElementReference from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By -from terrain.factories import UserFactory, RegistrationFactory, UserProfileFactory -from terrain.factories import CourseFactory, GroupFactory from xmodule.modulestore.django import _MODULESTORES, modulestore from xmodule.templates import update_templates from auth.authz import get_user_by_email @@ -61,7 +59,7 @@ def create_studio_user( email='robot+studio@edx.org', password='test', is_staff=False): - studio_user = UserFactory.build( + studio_user = world.UserFactory.build( username=uname, email=email, password=password, @@ -69,11 +67,11 @@ def create_studio_user( studio_user.set_password(password) studio_user.save() - registration = RegistrationFactory(user=studio_user) + registration = world.RegistrationFactory(user=studio_user) registration.register(studio_user) registration.activate() - user_profile = UserProfileFactory(user=studio_user) + user_profile = world.UserProfileFactory(user=studio_user) def flush_xmodule_store(): @@ -175,11 +173,11 @@ def log_into_studio( def create_a_course(): - c = CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course') + c = world.CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course') # Add the user to the instructor group of the course # so they will have the permissions to see it in studio - g = GroupFactory.create(name='instructor_MITx/999/Robot_Super_Course') + g = world.GroupFactory.create(name='instructor_MITx/999/Robot_Super_Course') u = get_user_by_email('robot+studio@edx.org') u.groups.add(g) u.save() diff --git a/cms/djangoapps/contentstore/features/factories.py b/cms/djangoapps/contentstore/features/factories.py deleted file mode 100644 index 087ceaaa2d..0000000000 --- a/cms/djangoapps/contentstore/features/factories.py +++ /dev/null @@ -1,34 +0,0 @@ -import factory -from student.models import User, UserProfile, Registration -from datetime import datetime -import uuid - - -class UserProfileFactory(factory.Factory): - FACTORY_FOR = UserProfile - - user = None - name = 'Robot Studio' - courseware = 'course.xml' - - -class RegistrationFactory(factory.Factory): - FACTORY_FOR = Registration - - user = None - activation_key = uuid.uuid4().hex - - -class UserFactory(factory.Factory): - FACTORY_FOR = User - - username = 'robot-studio' - email = 'robot+studio@edx.org' - password = 'test' - first_name = 'Robot' - last_name = 'Studio' - is_staff = False - is_active = True - is_superuser = False - last_login = datetime.now() - date_joined = datetime.now() diff --git a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py b/cms/djangoapps/contentstore/features/studio-overview-togglesection.py index 00aa39455d..060d592cfd 100644 --- a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py +++ b/cms/djangoapps/contentstore/features/studio-overview-togglesection.py @@ -1,5 +1,4 @@ from lettuce import world, step -from terrain.factories import * from common import * from nose.tools import assert_true, assert_false, assert_equal @@ -10,15 +9,15 @@ logger = getLogger(__name__) @step(u'I have a course with no sections$') def have_a_course(step): clear_courses() - course = CourseFactory.create() + course = world.CourseFactory.create() @step(u'I have a course with 1 section$') def have_a_course_with_1_section(step): clear_courses() - course = CourseFactory.create() - section = ItemFactory.create(parent_location=course.location) - subsection1 = ItemFactory.create( + course = world.CourseFactory.create() + section = world.ItemFactory.create(parent_location=course.location) + subsection1 = world.ItemFactory.create( parent_location=section.location, template='i4x://edx/templates/sequential/Empty', display_name='Subsection One',) @@ -27,20 +26,20 @@ def have_a_course_with_1_section(step): @step(u'I have a course with multiple sections$') def have_a_course_with_two_sections(step): clear_courses() - course = CourseFactory.create() - section = ItemFactory.create(parent_location=course.location) - subsection1 = ItemFactory.create( + course = world.CourseFactory.create() + section = world.ItemFactory.create(parent_location=course.location) + subsection1 = world.ItemFactory.create( parent_location=section.location, template='i4x://edx/templates/sequential/Empty', display_name='Subsection One',) - section2 = ItemFactory.create( + section2 = world.ItemFactory.create( parent_location=course.location, display_name='Section Two',) - subsection2 = ItemFactory.create( + subsection2 = world.ItemFactory.create( parent_location=section2.location, template='i4x://edx/templates/sequential/Empty', display_name='Subsection Alpha',) - subsection3 = ItemFactory.create( + subsection3 = world.ItemFactory.create( parent_location=section2.location, template='i4x://edx/templates/sequential/Empty', display_name='Subsection Beta',) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index d04e1a6332..615ffb6ed0 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -101,6 +101,20 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): self.assertEqual(reverse_tabs, course_tabs) + def test_import_polls(self): + import_from_xml(modulestore(), 'common/test/data/', ['full']) + + module_store = modulestore('direct') + found = False + + item = None + items = module_store.get_items(['i4x', 'edX', 'full', 'poll_question', None, None]) + found = len(items) > 0 + + self.assertTrue(found) + # check that there's actually content in the 'question' field + self.assertGreater(len(items[0].question),0) + def test_delete(self): import_from_xml(modulestore(), 'common/test/data/', ['full']) diff --git a/cms/djangoapps/contentstore/tests/test_course_updates.py b/cms/djangoapps/contentstore/tests/test_course_updates.py index 6a3a1e21f7..38608ee94d 100644 --- a/cms/djangoapps/contentstore/tests/test_course_updates.py +++ b/cms/djangoapps/contentstore/tests/test_course_updates.py @@ -1,31 +1,135 @@ from contentstore.tests.test_course_settings import CourseTestCase from django.core.urlresolvers import reverse import json +from webob.exc import HTTPServerError +from django.http import HttpResponseBadRequest class CourseUpdateTest(CourseTestCase): def test_course_update(self): # first get the update to force the creation - url = reverse('course_info', kwargs={'org': self.course_location.org, 'course': self.course_location.course, - 'name': self.course_location.name}) + url = reverse('course_info', + kwargs={'org': self.course_location.org, + 'course': self.course_location.course, + 'name': self.course_location.name}) self.client.get(url) - content = '' + init_content = '' payload = {'content': content, 'date': 'January 8, 2013'} - url = reverse('course_info', kwargs={'org': self.course_location.org, 'course': self.course_location.course, - 'provided_id': ''}) + url = reverse('course_info_json', kwargs={'org': self.course_location.org, + 'course': self.course_location.course, + 'provided_id': ''}) resp = self.client.post(url, json.dumps(payload), "application/json") payload = json.loads(resp.content) - self.assertHTMLEqual(content, payload['content'], "single iframe") + self.assertHTMLEqual(payload['content'], content) - url = reverse('course_info', kwargs={'org': self.course_location.org, 'course': self.course_location.course, - 'provided_id': payload['id']}) - content += '
          div

          p

          ' + first_update_url = reverse('course_info_json', + kwargs={'org': self.course_location.org, + 'course': self.course_location.course, + 'provided_id': payload['id']}) + content += '
          div

          p

          ' payload['content'] = content + resp = self.client.post(first_update_url, json.dumps(payload), + "application/json") + + self.assertHTMLEqual(content, json.loads(resp.content)['content'], + "iframe w/ div") + + # now put in an evil update + content = '
            ' + payload = {'content': content, + 'date': 'January 11, 2013'} + url = reverse('course_info_json', + kwargs={'org': self.course_location.org, + 'course': self.course_location.course, + 'provided_id': ''}) + resp = self.client.post(url, json.dumps(payload), "application/json") - self.assertHTMLEqual(content, json.loads(resp.content)['content'], "iframe w/ div") + payload = json.loads(resp.content) + + self.assertHTMLEqual(content, payload['content'], "self closing ol") + + url = reverse('course_info_json', + kwargs={'org': self.course_location.org, + 'course': self.course_location.course, + 'provided_id': ''}) + resp = self.client.get(url) + payload = json.loads(resp.content) + self.assertTrue(len(payload) == 2) + + # can't test non-json paylod b/c expect_json throws error + # try json w/o required fields + self.assertContains( + self.client.post(url, json.dumps({'garbage': 1}), + "application/json"), + 'Failed to save', status_code=400) + + # now try to update a non-existent update + url = reverse('course_info_json', + kwargs={'org': self.course_location.org, + 'course': self.course_location.course, + 'provided_id': '9'}) + content = 'blah blah' + payload = {'content': content, + 'date': 'January 21, 2013'} + self.assertContains( + self.client.post(url, json.dumps(payload), "application/json"), + 'Failed to save', status_code=400) + + # update w/ malformed html + content = 'error' + payload = {'content': content, + 'date': 'January 11, 2013'} + url = reverse('course_info_json', kwargs={'org': self.course_location.org, + 'course': self.course_location.course, + 'provided_id': ''}) + + resp = self.client.post(url, json.dumps(payload), "application/json") + + payload = json.loads(resp.content) + + self.assertContains( + self.client.post(url, json.dumps(payload), "application/json"), + ' @model.save(children: @components()) + update: (event, ui) => + payload = children : @components() + options = success : => @model.unset('children') + @model.save(payload, options) helper: 'clone' opacity: '0.5' placeholder: 'component-placeholder' @@ -109,7 +112,14 @@ class CMS.Views.UnitEdit extends Backbone.View id: $component.data('id') }, => $component.remove() - @model.save(children: @components()) + # b/c we don't vigilantly keep children up to date + # get rid of it before it hurts someone + # sorry for the js, i couldn't figure out the coffee equivalent + `_this.model.save({children: _this.components()}, + {success: function(model) { + model.unset('children'); + }} + );` ) deleteDraft: (event) -> diff --git a/cms/static/js/views/course_info_edit.js b/cms/static/js/views/course_info_edit.js index 8382fb15eb..ce959fd443 100644 --- a/cms/static/js/views/course_info_edit.js +++ b/cms/static/js/views/course_info_edit.js @@ -142,8 +142,11 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ onDelete: function(event) { event.preventDefault(); - // TODO ask for confirmation - // remove the dom element and delete the model + + if (!confirm('Are you sure you want to delete this update? This action cannot be undone.')) { + return; + } + var targetModel = this.eventModel(event); this.modelDom(event).remove(); var cacheThis = this; diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss index a2d46c0510..438b4460ac 100644 --- a/cms/static/sass/_base.scss +++ b/cms/static/sass/_base.scss @@ -1,7 +1,7 @@ -// studio base styling +// studio - base styling // ==================== -// basic reset +// basic setup html { font-size: 62.5%; overflow-y: scroll; @@ -9,7 +9,7 @@ html { body { @include font-size(16); - min-width: 980px; + min-width: $fg-min-width; background: $gray-l5; line-height: 1.6; color: $baseFontColor; @@ -350,10 +350,11 @@ h1 { // layout - grandfathered .main-wrapper { position: relative; - margin: 0 40px; + margin: 40px; } .inner-wrapper { + @include clearfix(); position: relative; max-width: 1280px; margin: auto; @@ -363,6 +364,12 @@ h1 { } } +.main-column { + clear: both; + float: left; + width: 70%; +} + .sidebar { float: right; width: 28%; @@ -378,109 +385,6 @@ h1 { // ==================== -// forms -input[type="text"], -input[type="email"], -input[type="password"], -textarea.text { - padding: 6px 8px 8px; - @include box-sizing(border-box); - border: 1px solid $mediumGrey; - border-radius: 2px; - @include linear-gradient($lightGrey, tint($lightGrey, 90%)); - background-color: $lightGrey; - @include box-shadow(0 1px 2px rgba(0, 0, 0, .1) inset); - font-family: 'Open Sans', sans-serif; - font-size: 11px; - color: $baseFontColor; - outline: 0; - - &::-webkit-input-placeholder, - &:-moz-placeholder, - &:-ms-input-placeholder { - color: #979faf; - } - - &:focus { - @include linear-gradient($paleYellow, tint($paleYellow, 90%)); - outline: 0; - } - - &[disabled] { - border-color: $gray-l4; - color: $gray-l2; - } - - &[readonly] { - border-color: $gray-l4; - color: $gray-l1; - - &:focus { - @include linear-gradient($lightGrey, tint($lightGrey, 90%)); - outline: 0; - } - } -} - -// forms - specific -input.search { - padding: 6px 15px 8px 30px; - @include box-sizing(border-box); - border: 1px solid $darkGrey; - border-radius: 20px; - background: url(../img/search-icon.png) no-repeat 8px 7px #edf1f5; - font-family: 'Open Sans', sans-serif; - color: $baseFontColor; - outline: 0; - - &::-webkit-input-placeholder { - color: #979faf; - } -} - -label { - font-size: 12px; -} - -code { - padding: 0 4px; - border-radius: 3px; - background: #eee; - font-family: Monaco, monospace; -} - -.CodeMirror { - font-size: 13px; - border: 1px solid $darkGrey; - background: #fff; -} - -.text-editor { - width: 100%; - min-height: 80px; - padding: 10px; - @include box-sizing(border-box); - border: 1px solid $mediumGrey; - @include linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.3)); - background-color: #edf1f5; - @include box-shadow(0 1px 2px rgba(0, 0, 0, 0.1) inset); - font-family: Monaco, monospace; -} - -// ==================== - -// UI - chrome -.window { - @include clearfix(); - @include border-radius(3px); - @include box-shadow(0 1px 1px $shadow-l1); - margin-bottom: $baseline; - border: 1px solid $gray-l2; - background: $white; -} - -// ==================== - // UI - actions .new-unit-item, .new-subsection-item, @@ -861,14 +765,4 @@ body.hide-wip { .wip-box { display: none; } -} - -// ==================== - -// needed fudges for now -body.dashboard { - - .my-classes { - margin-top: $baseline; - } } \ No newline at end of file diff --git a/cms/static/sass/_calendar.scss b/cms/static/sass/_calendar.scss deleted file mode 100644 index 4c007bb561..0000000000 --- a/cms/static/sass/_calendar.scss +++ /dev/null @@ -1,367 +0,0 @@ -section.cal { - @include box-sizing(border-box); - @include clearfix; - padding: 20px; - - > header { - display: none; - @include clearfix; - margin-bottom: 10px; - opacity: .4; - @include transition; - text-shadow: 0 1px 0 #fff; - - &:hover { - opacity: 1; - } - - h2 { - @include inline-block(); - text-transform: uppercase; - letter-spacing: 1px; - font-size: 14px; - padding: 6px 6px 6px 0; - font-size: 12px; - margin: 0; - } - - ul { - @include inline-block; - float: right; - margin: 0; - padding: 0; - - &.actions { - float: left; - } - - li { - @include inline-block; - margin-right: 6px; - border-right: 1px solid #ddd; - padding: 0 6px 0 0; - - &:last-child { - border-right: 0; - margin-right: 0; - padding-right: 0; - } - - a { - @include inline-block(); - font-size: 12px; - @include inline-block; - margin: 0 6px; - font-style: italic; - } - - ul { - @include inline-block(); - margin: 0; - - li { - @include inline-block(); - padding: 0; - border-left: 0; - } - } - } - } - } - - ol { - list-style: none; - @include clearfix; - border: 1px solid lighten( $dark-blue , 30% ); - background: #FFF; - width: 100%; - @include box-sizing(border-box); - margin: 0; - padding: 0; - @include box-shadow(0 0 5px lighten($dark-blue, 45%)); - @include border-radius(3px); - overflow: hidden; - - > li { - border-right: 1px solid lighten($dark-blue, 40%); - border-bottom: 1px solid lighten($dark-blue, 40%); - @include box-sizing(border-box); - float: left; - width: flex-grid(3) + ((flex-gutter() * 3) / 4); - background-color: $light-blue; - @include box-shadow(inset 0 0 0 1px lighten($light-blue, 8%)); - - &:hover { - li.create-module { - opacity: 1; - } - } - - &:nth-child(4n) { - border-right: 0; - } - - header { - border-bottom: 1px solid lighten($dark-blue, 40%); - @include box-shadow(0 2px 2px $light-blue); - display: block; - margin-bottom: 2px; - background: #FFF; - - h1 { - font-size: 14px; - text-transform: uppercase; - border-bottom: 1px solid lighten($dark-blue, 60%); - padding: 6px; - color: $bright-blue; - margin: 0; - - a { - color: $bright-blue; - display: block; - padding: 6px; - margin: -6px; - - &:hover { - color: darken($bright-blue, 10%); - background: lighten($yellow, 10%); - } - } - } - - ul { - margin: 0; - padding: 0; - - li { - background: #fff; - color: #888; - border-bottom: 0; - font-size: 12px; - @include box-shadow(none); - } - } - } - - ul { - list-style: none; - margin: 0 0 1px 0; - padding: 0; - - li { - border-bottom: 1px solid darken($light-blue, 6%); - // @include box-shadow(0 1px 0 lighten($light-blue, 4%)); - overflow: hidden; - position: relative; - text-shadow: 0 1px 0 #fff; - - &:hover { - background-color: lighten($yellow, 14%); - - a.draggable { - background-color: lighten($yellow, 14%); - opacity: 1; - } - } - - &.editable { - padding: 3px 6px; - } - - a { - color: lighten($dark-blue, 10%); - display: block; - padding: 6px 35px 6px 6px; - - &:hover { - background-color: lighten($yellow, 10%); - } - - &.draggable { - background-color: $light-blue; - opacity: .3; - padding: 0; - - &:hover { - background-color: lighten($yellow, 10%); - } - } - } - - &.create-module { - position: relative; - opacity: 0; - @include transition(all 3s ease-in-out); - background: darken($light-blue, 2%); - - > div { - background: $dark-blue; - @include box-shadow(0 0 5px darken($light-blue, 60%)); - @include box-sizing(border-box); - display: none; - margin-left: 3%; - padding: 10px; - @include position(absolute, 30px 0 0 0); - width: 90%; - z-index: 99; - - ul { - li { - border-bottom: 0; - background: none; - - input { - @include box-sizing(border-box); - width: 100%; - } - - select { - @include box-sizing(border-box); - width: 100%; - - option { - font-size: 14px; - } - } - - div { - margin-top: 10px; - } - - a { - color: $light-blue; - float: right; - - &:first-child { - float: left; - } - - &:hover { - color: #fff; - } - } - } - } - } - } - } - } - } - } - - section.new-section { - margin: 10px 0 40px; - @include inline-block(); - position: relative; - - > a { - @extend .button; - display: block; - } - - section { - display: none; - @include position(absolute, 30px 0 0 0); - background: rgba(#000, .8); - min-width: 300px; - padding: 10px; - @include box-sizing(border-box); - @include border-radius(3px); - z-index: 99; - - &:before { - content: " "; - display: block; - background: rgba(#000, .8); - width: 10px; - height: 10px; - @include position(absolute, -5px 0 0 20%); - @include transform(rotate(45deg)); - } - - form { - - ul { - list-style: none; - - li { - border-bottom: 0; - background: none; - margin-bottom: 6px; - - input { - width: 100%; - @include box-sizing(border-box); - border-color: #000; - padding: 6px; - } - - select { - width: 100%; - @include box-sizing(border-box); - - option { - font-size: 14px; - } - } - - a { - float: right; - - &:first-child { - float: left; - } - } - } - } - } - } - } -} - -body.content -section.cal { - width: flex-grid(3); - float: left; - overflow: scroll; - @include box-sizing(border-box); - opacity: .4; - @include transition(); - - &:hover { - opacity: 1; - } - - > header { - @include transition; - overflow: hidden; - - > a { - display: none; - } - - ul { - float: none; - display: block; - - li { - - ul { - display: inline; - } - } - } - } - - ol { - li { - @include box-sizing(border-box); - width: 100%; - border-right: 0; - - &.create-module { - display: none; - } - } - } -} diff --git a/cms/static/sass/_cms_mixins.scss b/cms/static/sass/_cms_mixins.scss index b8d9a8ae2e..015a94b762 100644 --- a/cms/static/sass/_cms_mixins.scss +++ b/cms/static/sass/_cms_mixins.scss @@ -1,3 +1,6 @@ +// studio - utilities - mixins and extends +// ==================== + @mixin clearfix { &:after { content: ''; diff --git a/cms/static/sass/_courseware.scss b/cms/static/sass/_courseware.scss deleted file mode 100644 index 45ea111b6f..0000000000 --- a/cms/static/sass/_courseware.scss +++ /dev/null @@ -1,689 +0,0 @@ - -input.courseware-unit-search-input { - float: left; - width: 260px; - background-color: #fff; -} - -.branch { - - .section-item { - @include clearfix(); - - .details { - display: block; - float: left; - margin-bottom: 0; - width: 650px; - } - - .gradable-status { - float: right; - position: relative; - top: -4px; - right: 50px; - width: 145px; - - .status-label { - position: absolute; - top: 2px; - right: -5px; - display: none; - width: 110px; - padding: 5px 40px 5px 10px; - @include border-radius(3px); - color: $lightGrey; - text-align: right; - font-size: 12px; - font-weight: bold; - line-height: 16px; - } - - .menu-toggle { - z-index: 10; - position: absolute; - top: 0; - right: 5px; - padding: 5px; - color: $mediumGrey; - - &:hover, &.is-active { - color: $blue; - } - } - - .menu { - z-index: 1; - display: none; - opacity: 0.0; - position: absolute; - top: -1px; - left: 5px; - margin: 0; - padding: 8px 12px; - background: $white; - border: 1px solid $mediumGrey; - font-size: 12px; - @include border-radius(4px); - @include box-shadow(0 1px 2px rgba(0, 0, 0, .2)); - @include transition(opacity .15s); - - - li { - width: 115px; - margin-bottom: 3px; - padding-bottom: 3px; - border-bottom: 1px solid $lightGrey; - - &:last-child { - margin-bottom: 0; - padding-bottom: 0; - border: none; - - a { - color: $darkGrey; - } - } - } - - a { - color: $blue; - - &.is-selected { - font-weight: bold; - } - } - } - - // dropdown state - &.is-active { - - .menu { - z-index: 1000; - display: block; - opacity: 1.0; - } - - .menu-toggle { - z-index: 10000; - } - } - - // set state - &.is-set { - - .menu-toggle { - color: $blue; - } - - .status-label { - display: block; - color: $blue; - } - } - } - } - } - - -.courseware-section { - position: relative; - background: #fff; - border-radius: 3px; - border: 1px solid $mediumGrey; - margin-top: 15px; - padding-bottom: 12px; - @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.1)); - - &:first-child { - margin-top: 0; - } - - &.collapsed { - padding-bottom: 0; - } - - label { - float: left; - line-height: 29px; - } - - .datepair { - float: left; - margin-left: 10px; - } - - .section-published-date { - position: absolute; - top: 19px; - right: 90px; - padding: 4px 10px; - border-radius: 3px; - background: $lightGrey; - text-align: right; - - .published-status { - font-size: 12px; - margin-right: 15px; - - strong { - font-weight: bold; - } - } - - .schedule-button { - @include blue-button; - } - - .edit-button { - @include blue-button; - } - - .schedule-button, - .edit-button { - font-size: 11px; - padding: 3px 15px 5px; - } - } - - .datepair .date, - .datepair .time { - padding-left: 0; - padding-right: 0; - border: none; - background: none; - @include box-shadow(none); - font-size: 13px; - font-weight: bold; - color: $blue; - cursor: pointer; - } - - .datepair .date { - width: 80px; - } - - .datepair .time { - width: 65px; - } - - &.collapsed .subsection-list, - .collapsed .subsection-list, - .collapsed > ol { - display: none !important; - } - - header { - min-height: 75px; - @include clearfix(); - - .item-details, .section-published-date { - - } - - .item-details { - display: inline-block; - padding: 20px 0 10px 0; - @include clearfix(); - - .section-name { - float: left; - margin-right: 10px; - width: 350px; - font-size: 19px; - font-weight: bold; - color: $blue; - } - - .section-name-span { - cursor: pointer; - @include transition(color .15s); - - &:hover { - color: $orange; - } - } - - .section-name-edit { - position: relative; - width: 400px; - background: $white; - - input { - font-size: 16px; - } - - .save-button { - @include blue-button; - padding: 7px 20px 7px; - margin-right: 5px; - } - - .cancel-button { - @include white-button; - padding: 7px 20px 7px; - } - } - - .section-published-date { - float: right; - width: 265px; - margin-right: 220px; - @include border-radius(3px); - background: $lightGrey; - - .published-status { - font-size: 12px; - margin-right: 15px; - - strong { - font-weight: bold; - } - } - - .schedule-button { - @include blue-button; - } - - .edit-button { - @include blue-button; - } - - .schedule-button, - .edit-button { - font-size: 11px; - padding: 3px 15px 5px; - - } - } - - .gradable-status { - position: absolute; - top: 20px; - right: 70px; - width: 145px; - - .status-label { - position: absolute; - top: 0; - right: 2px; - display: none; - width: 100px; - padding: 10px 35px 10px 10px; - @include border-radius(3px); - background: $lightGrey; - color: $lightGrey; - text-align: right; - font-size: 12px; - font-weight: bold; - line-height: 16px; - } - - .menu-toggle { - z-index: 10; - position: absolute; - top: 2px; - right: 5px; - padding: 5px; - color: $lightGrey; - - &:hover, &.is-active { - color: $blue; - } - } - - .menu { - z-index: 1; - display: none; - opacity: 0.0; - position: absolute; - top: -1px; - left: 2px; - margin: 0; - padding: 8px 12px; - background: $white; - border: 1px solid $mediumGrey; - font-size: 12px; - @include border-radius(4px); - @include box-shadow(0 1px 2px rgba(0, 0, 0, .2)); - @include transition(opacity .15s); - @include transition(display .15s); - - - li { - width: 115px; - margin-bottom: 3px; - padding-bottom: 3px; - border-bottom: 1px solid $lightGrey; - - &:last-child { - margin-bottom: 0; - padding-bottom: 0; - border: none; - - a { - color: $darkGrey; - } - } - } - - a { - - &.is-selected { - font-weight: bold; - } - } - } - - // dropdown state - &.is-active { - - .menu { - z-index: 1000; - display: block; - opacity: 1.0; - } - - - .menu-toggle { - z-index: 10000; - } - } - - // set state - &.is-set { - - .menu-toggle { - color: $blue; - } - - .status-label { - display: block; - color: $blue; - } - } - - float: left; - padding: 21px 0 0; - } - } - - .item-actions { - margin-top: 21px; - margin-right: 12px; - - .edit-button, - .delete-button { - margin-top: -3px; - } - } - - .expand-collapse-icon { - float: left; - margin: 29px 6px 16px 16px; - @include transition(none); - - &.expand { - background-position: 0 0; - } - - &.collapsed { - - } - } - - .drag-handle { - margin-left: 11px; - } - } - - h3 { - font-size: 19px; - font-weight: 700; - color: $blue; - } - - .section-name-span { - cursor: pointer; - @include transition(color .15s); - - &:hover { - color: $orange; - } - } - - .section-name-form { - margin-bottom: 15px; - } - - .section-name-edit { - input { - font-size: 16px; - } - - .save-button { - @include blue-button; - padding: 7px 20px 7px; - margin-right: 5px; - } - - .cancel-button { - @include white-button; - padding: 7px 20px 7px; - } - } - - h4 { - font-size: 12px; - color: #878e9d; - - strong { - font-weight: bold; - } - } - - .list-header { - @include linear-gradient(top, transparent, rgba(0, 0, 0, .1)); - background-color: #ced2db; - border-radius: 3px 3px 0 0; - } - - .subsection-list { - margin: 0 12px; - - > ol { - @include tree-view; - border-top-width: 0; - } - } - - &.new-section { - - header { - height: auto; - @include clearfix(); - } - - .expand-collapse-icon { - visibility: hidden; - } - - .item-details { - padding: 25px 0 0 0; - - .section-name { - float: none; - width: 100%; - } - } - } -} - -.toggle-button-sections { - display: none; - position: relative; - float: right; - margin-top: 10px; - - font-size: 13px; - color: $darkGrey; - - &.is-shown { - display: block; - } - - .ss-icon { - @include border-radius(20px); - position: relative; - top: -1px; - display: inline-block; - margin-right: 2px; - line-height: 5px; - font-size: 11px; - } - - .label { - display: inline-block; - } -} - -.new-section-name, -.new-subsection-name-input { - width: 515px; -} - -.new-section-name-save, -.new-subsection-name-save { - @include blue-button; - padding: 4px 20px 7px; - margin: 0 5px; - color: #fff !important; -} - -.new-section-name-cancel, -.new-subsection-name-cancel { - @include white-button; - padding: 4px 20px 7px; - color: #8891a1 !important; -} - -.dummy-calendar { - display: none; - position: absolute; - top: 55px; - left: 110px; - z-index: 9999; - border: 1px solid #3C3C3C; - @include box-shadow(0 1px 15px rgba(0, 0, 0, .2)); -} - -.unit-name-input { - padding: 20px 40px; - - label { - display: block; - } - - input { - width: 100%; - font-size: 20px; - } -} - -.preview { - background: url(../img/preview.jpg) center top no-repeat; -} - -.edit-subsection-publish-settings { - display: none; - position: fixed; - top: 100px; - left: 50%; - z-index: 99999; - width: 600px; - margin-left: -300px; - background: #fff; - text-align: center; - - .settings { - padding: 40px; - } - - h3 { - font-size: 34px; - font-weight: 300; - } - - .picker { - margin: 30px 0 65px; - } - - .description { - margin-top: 30px; - font-size: 14px; - line-height: 20px; - } - - strong { - font-weight: 700; - } - - .start-date, - .start-time { - font-size: 19px; - } - - .save-button { - @include blue-button; - margin-right: 10px; - } - - .cancel-button { - @include white-button; - } - - .save-button, - .cancel-button { - font-size: 16px; - } -} - -.collapse-all-button { - float: right; - margin-top: 10px; - font-size: 13px; - color: $darkGrey; -} - -// sort/drag and drop -.ui-droppable { - @include transition (padding 0.5s ease-in-out 0s); - min-height: 20px; - padding: 0; - - &.dropover { - padding: 15px 0; - } -} - -.ui-draggable-dragging { - @include box-shadow(0 1px 2px rgba(0, 0, 0, .3)); - border: 1px solid $darkGrey; - opacity : 0.2; - &:hover { - opacity : 1.0; - .section-item { - background: $yellow !important; - } - } - - // hiding unit button - temporary fix until this semantically corrected - .new-unit-item { - display: none; - } -} - -ol.ui-droppable .branch:first-child .section-item { - border-top: none; -} - diff --git a/cms/static/sass/_dashboard.scss b/cms/static/sass/_dashboard.scss deleted file mode 100644 index 0d4d046e57..0000000000 --- a/cms/static/sass/_dashboard.scss +++ /dev/null @@ -1,114 +0,0 @@ -.class-list { - margin-top: 20px; - border-radius: 3px; - border: 1px solid $darkGrey; - background: #fff; - @include box-shadow(0 1px 2px rgba(0, 0, 0, .1)); - - li { - position: relative; - border-bottom: 1px solid $mediumGrey; - - &:last-child { - border-bottom: none; - } - - .class-link { - z-index: 100; - display: block; - padding: 20px 25px; - line-height: 1.3; - - &:hover { - background: $paleYellow; - - + .view-live-button { - opacity: 1.0; - pointer-events: auto; - } - } - } - } - - .class-name { - display: block; - font-size: 19px; - font-weight: 300; - } - - .detail { - font-size: 14px; - font-weight: 400; - margin-right: 20px; - color: #3c3c3c; - } - - // view live button - .view-live-button { - z-index: 10000; - position: absolute; - top: 15px; - right: $baseline; - padding: ($baseline/4) ($baseline/2); - opacity: 0; - pointer-events: none; - - &:hover { - opacity: 1.0; - pointer-events: auto; - } - } -} - -.new-course { - padding: 15px 25px; - margin-top: 20px; - border-radius: 3px; - border: 1px solid $darkGrey; - background: #fff; - box-shadow: 0 1px 2px rgba(0, 0, 0, .1); - @include clearfix; - - .row { - margin-bottom: 15px; - @include clearfix; - } - - .column { - float: left; - width: 48%; - } - - .column:first-child { - margin-right: 4%; - } - - .course-info { - width: 600px; - } - - label { - display: block; - font-size: 13px; - font-weight: 700; - } - - .new-course-org, - .new-course-number, - .new-course-name { - width: 100%; - } - - .new-course-name { - font-size: 19px; - font-weight: 300; - } - - .new-course-save { - @include blue-button; - } - - .new-course-cancel { - @include white-button; - } -} \ No newline at end of file diff --git a/cms/static/sass/_extends.scss b/cms/static/sass/_extends.scss deleted file mode 100644 index 5907481bd1..0000000000 --- a/cms/static/sass/_extends.scss +++ /dev/null @@ -1,78 +0,0 @@ -.faded-hr-divider { - @include background-image(linear-gradient(180deg, rgba(200,200,200, 0) 0%, - rgba(200,200,200, 1) 50%, - rgba(200,200,200, 0))); - height: 1px; - width: 100%; -} - -.faded-hr-divider-medium { - @include background-image(linear-gradient(180deg, rgba(240,240,240, 0) 0%, - rgba(240,240,240, 1) 50%, - rgba(240,240,240, 0))); - height: 1px; - width: 100%; -} - -.faded-hr-divider-light { - @include background-image(linear-gradient(180deg, rgba(255,255,255, 0) 0%, - rgba(255,255,255, 0.8) 50%, - rgba(255,255,255, 0))); - height: 1px; - width: 100%; -} - -.faded-vertical-divider { - @include background-image(linear-gradient(90deg, rgba(200,200,200, 0) 0%, - rgba(200,200,200, 1) 50%, - rgba(200,200,200, 0))); - height: 100%; - width: 1px; -} - -.faded-vertical-divider-light { - @include background-image(linear-gradient(90deg, rgba(255,255,255, 0) 0%, - rgba(255,255,255, 0.6) 50%, - rgba(255,255,255, 0))); - height: 100%; - width: 1px; -} - -.vertical-divider { - @extend .faded-vertical-divider; - position: relative; - - &::after { - @extend .faded-vertical-divider-light; - content: ""; - display: block; - position: absolute; - left: 1px; - } -} - -.horizontal-divider { - border: none; - @extend .faded-hr-divider; - position: relative; - - &::after { - @extend .faded-hr-divider-light; - content: ""; - display: block; - position: absolute; - top: 1px; - } -} - -.fade-right-hr-divider { - @include background-image(linear-gradient(180deg, rgba(200,200,200, 0) 0%, - rgba(200,200,200, 1))); - border: none; -} - -.fade-left-hr-divider { - @include background-image(linear-gradient(180deg, rgba(200,200,200, 1) 0%, - rgba(200,200,200, 0))); - border: none; -} \ No newline at end of file diff --git a/cms/static/sass/_landing.scss b/cms/static/sass/_landing.scss deleted file mode 100644 index 16f1b5b5a7..0000000000 --- a/cms/static/sass/_landing.scss +++ /dev/null @@ -1,126 +0,0 @@ -// This is a temporary page, which will be replaced once we have a more extensive course catalog and marketing site for edX labs. - -.class-landing { - - .main-wrapper { - width: 700px !important; - margin: 100px auto; - } - - .class-info { - padding: 30px 40px 40px; - @extend .window; - - hgroup { - padding-bottom: 26px; - border-bottom: 1px solid $mediumGrey; - } - - h1 { - float: none; - font-size: 30px; - font-weight: 300; - margin: 0; - } - - h2 { - color: #5d6779; - } - - .class-actions { - @include clearfix; - padding: 15px 0; - margin-bottom: 18px; - border-bottom: 1px solid $mediumGrey; - } - - .log-in-form { - @include clearfix; - padding: 15px 0 20px; - margin-bottom: 18px; - border-bottom: 1px solid $mediumGrey; - - .log-in-submit-button { - @include blue-button; - padding: 6px 20px 8px; - margin: 24px 0 0; - } - - .column { - float: left; - width: 41%; - margin-right: 1%; - - &.submit { - width: 16%; - margin-right: 0; - } - - label { - float: left; - } - } - - input { - width: 100%; - font-family: $sans-serif; - font-size: 13px; - } - - .forgot-button { - float: right; - margin-bottom: 6px; - font-size: 12px; - } - } - - .sign-up-button { - @include blue-button; - display: block; - width: 250px; - margin: auto; - } - - .log-in-button { - @include white-button; - float: right; - } - - .sign-up-button, - .log-in-button { - padding: 8px 0 12px; - font-size: 18px; - font-weight: 300; - text-align: center; - } - - .class-description { - margin-top: 30px; - font-size: 14px; - } - - p + p { - margin-top: 22px; - } - } - - .edx-labs-logo-small { - display: block; - width: 124px; - height: 30px; - margin: auto; - background: url(../img/edx-labs-logo-small.png) no-repeat; - text-indent: -9999px; - overflow: hidden; - } - - .edge-logo { - display: block; - width: 143px; - height: 39px; - margin: auto; - background: url(../images/edge-logo-small.png) no-repeat; - text-indent: -9999px; - overflow: hidden; - } -} \ No newline at end of file diff --git a/cms/static/sass/_layout.scss b/cms/static/sass/_layout.scss deleted file mode 100644 index 43308a973c..0000000000 --- a/cms/static/sass/_layout.scss +++ /dev/null @@ -1,125 +0,0 @@ -body { - @include clearfix(); - height: 100%; - font: 14px $body-font-family; - background-color: lighten($dark-blue, 62%); - background-image: url('/static/img/noise.png'); - - > section { - display: table; - table-layout: fixed; - width: 100%; - } - - > header { - background: $dark-blue; - @include background-image(url('/static/img/noise.png'), linear-gradient(lighten($dark-blue, 10%), $dark-blue)); - border-bottom: 1px solid darken($dark-blue, 15%); - @include box-shadow(inset 0 -1px 0 lighten($dark-blue, 10%)); - @include box-sizing(border-box); - color: #fff; - display: block; - float: none; - padding: 0 20px; - text-shadow: 0 -1px 0 darken($dark-blue, 15%); - width: 100%; - - nav { - @include clearfix; - - > a { - @include hide-text; - background: url('/static/img/menu.png') 0 center no-repeat; - border-right: 1px solid darken($dark-blue, 10%); - @include box-shadow(1px 0 0 lighten($dark-blue, 10%)); - display: block; - float: left; - height: 19px; - padding: 8px 10px 8px 0; - width: 14px; - - &:hover, &:focus { - opacity: .7; - } - } - - h2 { - border-right: 1px solid darken($dark-blue, 10%); - @include box-shadow(1px 0 0 lighten($dark-blue, 10%)); - float: left; - font-size: 14px; - margin: 0; - text-transform: uppercase; - -webkit-font-smoothing: antialiased; - - a { - color: #fff; - padding: 8px 20px; - display: block; - - &:hover { - background-color: rgba(darken($dark-blue, 15%), .5); - color: $yellow; - } - } - } - - a { - color: rgba(#fff, .8); - - &:hover { - color: rgba(#fff, .6); - } - } - - ul { - float: left; - margin: 0; - padding: 0; - @include clearfix; - - &.user-nav { - float: right; - border-left: 1px solid darken($dark-blue, 10%); - } - - li { - border-right: 1px solid darken($dark-blue, 10%); - float: left; - @include box-shadow(1px 0 0 lighten($dark-blue, 10%)); - - a { - padding: 8px 20px; - display: block; - - &:hover { - background-color: rgba(darken($dark-blue, 15%), .5); - color: $yellow; - } - - &.new-module { - &:before { - @include inline-block; - content: "+"; - font-weight: bold; - margin-right: 10px; - } - } - } - } - } - } - } - - &.content { - section.main-content { - border-left: 2px solid $dark-blue; - @include box-sizing(border-box); - width: flex-grid(9) + flex-gutter(); - float: left; - @include box-shadow( -2px 0 0 lighten($dark-blue, 55%)); - @include transition(); - background: #FFF; - } - } -} diff --git a/cms/static/sass/_lms.scss b/cms/static/sass/_lms.scss deleted file mode 100644 index 1ddc48edaf..0000000000 --- a/cms/static/sass/_lms.scss +++ /dev/null @@ -1,69 +0,0 @@ -.component { - font-family: 'Open Sans', Verdana, Arial, Helvetica, sans-serif; - font-size: 16px; - line-height: 1.6; - color: #3c3c3c; - - a { - color: #1d9dd9; - text-decoration: none; - } - - p { - font-size: 16px; - line-height: 1.6; - } - - h1 { - float: none; - } - - h2 { - color: #646464; - font-size: 19px; - font-weight: 300; - letter-spacing: 1px; - margin-bottom: 15px; - margin-left: 0; - text-transform: uppercase; - } - - h3 { - font-size: 19px; - font-weight: 400; - } - - h4 { - background: none; - padding: 0; - border: none; - @include box-shadow(none); - font-size: 16px; - font-weight: 400; - } - - code { - margin: 0 2px; - padding: 0px 5px; - border-radius: 3px; - border: 1px solid #eaeaea; - white-space: nowrap; - font-family: Monaco, monospace; - font-size: 14px; - background-color: #f8f8f8; - } - - p + h2, ul + h2, ol + h2, p + h3 { - margin-top: 40px; - } - - p + p, ul + p, ol + p { - margin-top: 20px; - } - - p { - color: #3c3c3c; - font: normal 1em/1.6em; - margin: 0px; - } -} \ No newline at end of file diff --git a/cms/static/sass/_login.scss b/cms/static/sass/_login.scss deleted file mode 100644 index c2bff74638..0000000000 --- a/cms/static/sass/_login.scss +++ /dev/null @@ -1,139 +0,0 @@ -.edx-studio-logo-large { - display: block; - width: 224px; - height: 45px; - margin: 100px auto 30px; - background: url(../img/edx-studio-large.png) no-repeat; -} - -.sign-up-box, -.log-in-box { - width: 500px; - margin: auto; - border-radius: 3px; - - header { - height: 36px; - border-radius: 3px 3px 0 0; - border: 1px solid #2c2e33; - @include linear-gradient(top, #686b76, #54565e); - color: #fff; - @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.2), 0 -1px 0px rgba(255, 255, 255, 0.05) inset, 0 1px 0 rgba(255, 255, 255, .25) inset); - - h1 { - float: none; - margin: 5px 0; - font-size: 15px; - font-weight: 300; - text-align: center; - } - } - - form { - padding: 40px; - border: 1px solid $darkGrey; - border-top-width: 0; - border-radius: 0 0 3px 3px; - background: #fff; - @include box-shadow(0 1px 2px rgba(0, 0, 0, .1)); - } - - label { - display: block; - margin-bottom: 5px; - font-size: 13px; - font-weight: 700; - } - - input[type="text"], - input[type="email"], - input[type="password"] { - width: 100%; - font-size: 20px; - font-weight: 300; - } - - .row { - @include clearfix; - margin-bottom: 24px; - - .split { - float: left; - width: 48%; - - &:first-child { - margin-right: 4%; - } - } - } - - .form-actions { - @include clearfix; - margin-top: 32px; - margin-bottom: 5px; - text-align: center; - } - - .log-in-button, - .create-account-button { - @include blue-button; - padding: 8px 0 10px; - font-family: $sans-serif; - @include transition(all .15s); - } - - .create-account-button { - padding: 10px 40px 12px; - margin-bottom: 10px; - } - - .enrolled { - font-size: 14px; - } - - .sign-up-button { - @include white-button; - padding: 7px 0 9px; - } - - .log-in-button, - .sign-up-button { - @include box-sizing(border-box); - float: left; - width: 45%; - } - - .or { - float: left; - display: inline-block; - width: 10%; - font-size: 15px; - line-height: 36px; - color: $darkGrey; - text-align: center; - } - - .forgot-button { - float: right; - font-size: 11px; - font-weight: 400; - line-height: 21px; - } - - .log-in-extra { - margin-top: 10px; - text-align: right; - font-size: 13px; - } - - #login_error, - #register_error { - display: none; - margin-bottom: 30px; - padding: 5px 10px; - border-radius: 3px; - background: $error-red; - font-size: 14px; - color: #fff; - } -} \ No newline at end of file diff --git a/cms/static/sass/_module-header.scss b/cms/static/sass/_module-header.scss deleted file mode 100644 index e2af263618..0000000000 --- a/cms/static/sass/_module-header.scss +++ /dev/null @@ -1,128 +0,0 @@ -section.video-new, section.video-edit, section.problem-new, section.problem-edit { - position: absolute; - top: 72px; - right: 0; - background: #fff; - width: flex-grid(6); - @include box-shadow(0 0 6px #666); - border: 1px solid #333; - border-right: 0; - z-index: 4; - - > header { - background: #666; - @include clearfix; - color: #fff; - padding: 6px; - border-bottom: 1px solid #333; - -webkit-font-smoothing: antialiased; - - h2 { - float: left; - font-size: 14px; - } - - a { - color: #fff; - - &.save-update { - float: right; - } - - &.cancel { - float: left; - } - } - - } - - > section { - padding: 20px; - - > header { - h1 { - font-size: 24px; - margin: 12px 0; - } - - section { - &.status-settings { - ul { - list-style: none; - @include border-radius(2px); - border: 1px solid #999; - @include inline-block(); - - li { - @include inline-block(); - border-right: 1px solid #999; - padding: 6px; - - &:last-child { - border-right: 0; - } - - &.current { - background: #eee; - } - } - } - - a.settings { - @include inline-block(); - margin: 0 20px; - border: 1px solid #999; - padding: 6px; - } - - select { - float: right; - } - } - - &.meta { - background: #eee; - padding: 10px; - margin: 20px 0; - @include clearfix(); - - div { - float: left; - margin-right: 20px; - - h2 { - font-size: 14px; - @include inline-block(); - } - - p { - @include inline-block(); - } - } - } - } - } - - section.notes { - margin-top: 20px; - padding: 6px; - background: #eee; - border: 1px solid #ccc; - - textarea { - @include box-sizing(border-box); - display: block; - width: 100%; - } - - h2 { - font-size: 14px; - margin-bottom: 6px; - } - - input[type="submit"]{ - margin-top: 10px; - } - } - } -} diff --git a/cms/static/sass/_problem.scss b/cms/static/sass/_problem.scss deleted file mode 100644 index 66acacf65c..0000000000 --- a/cms/static/sass/_problem.scss +++ /dev/null @@ -1,24 +0,0 @@ -section.problem-new, section.problem-edit { - > section { - textarea { - @include box-sizing(border-box); - display: block; - width: 100%; - } - - div.preview { - background: #eee; - @include box-sizing(border-box); - height: 40px; - padding: 10px; - width: 100%; - } - - a.save { - @extend .button; - @include inline-block(); - margin-top: 20px; - } - } -} - diff --git a/cms/static/sass/_reset.scss b/cms/static/sass/_reset.scss index ee03a0fca3..87a6e51232 100644 --- a/cms/static/sass/_reset.scss +++ b/cms/static/sass/_reset.scss @@ -1,3 +1,10 @@ +// studio - utilities - reset +// ==================== + +// * { +// @include box-sizing(border-box); +// } + html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, @@ -18,7 +25,7 @@ time, mark, audio, video { font: inherit; vertical-align: baseline; } -/* HTML5 display-role reset for older browsers */ + article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { display: block; @@ -38,12 +45,6 @@ q:before, q:after { content: none; } -/* remember to define visible focus styles! -:focus { - outline: ?????; -} */ - -/* remember to highlight inserts somehow! */ ins { text-decoration: none; } @@ -56,10 +57,11 @@ table { border-spacing: 0; } -/* Reset styles to remove ui-lightness jquery ui theme -from the tabs component (used in the add component problem tab menu) -*/ +// ==================== +// grandfathered styles + +// reset styles to remove ui-lightness jquery ui theme from the tabs component (used in the add component problem tab menu) .ui-tabs { padding: 0; white-space: normal; @@ -118,10 +120,7 @@ from the tabs component (used in the add component problem tab menu) padding: 0; } -/* reapplying the tab styles from unit.scss after -removing jquery ui ui-lightness styling -*/ - +// reapplying the tab styles from unit.scss after removing jquery ui ui-lightness styling .problem-type-tabs { border:none; list-style-type: none; @@ -146,26 +145,4 @@ removing jquery ui ui-lightness styling border: 0px; } } -/* - li { - float:left; - display:inline-block; - text-align:center; - width: auto; - //@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0)); - //background-color: tint($lightBluishGrey, 20%); - //@include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset); - opacity:.8; - - &:hover { - opacity:1; - } - - &.current { - border: 0px; - //@include active; - opacity:1; - } - } -*/ } \ No newline at end of file diff --git a/cms/static/sass/_section.scss b/cms/static/sass/_section.scss deleted file mode 100644 index 97818326be..0000000000 --- a/cms/static/sass/_section.scss +++ /dev/null @@ -1,239 +0,0 @@ -section#unit-wrapper { - section.filters { - @include clearfix; - display: none; - opacity: .4; - margin-bottom: 10px; - @include transition; - - &:hover { - opacity: 1; - } - - h2 { - @include inline-block(); - text-transform: uppercase; - letter-spacing: 1px; - font-size: 14px; - padding: 6px 6px 6px 0; - font-size: 12px; - margin: 0; - } - - ul { - @include clearfix(); - list-style: none; - margin: 0; - padding: 0; - - li { - @include inline-block; - margin-right: 6px; - border-right: 1px solid #ddd; - padding-right: 6px; - - &.search { - float: right; - border: 0; - } - - a { - &.more { - font-size: 12px; - @include inline-block; - margin: 0 6px; - font-style: italic; - } - } - } - } - } - - div.content { - display: table; - border: 1px solid lighten($dark-blue, 40%); - width: 100%; - @include border-radius(3px); - @include box-shadow(0 0 4px lighten($dark-blue, 50%)); - - section { - header { - background: #fff; - padding: 6px; - border-bottom: 1px solid lighten($dark-blue, 60%); - @include clearfix; - - h2 { - color: $bright-blue; - // float: left; - font-size: 14px; - letter-spacing: 1px; - // line-height: 20px; - text-transform: uppercase; - margin: 0; - } - } - - &.modules { - @include box-sizing(border-box); - display: table-cell; - width: flex-grid(6, 9); - border-right: 1px solid lighten($dark-blue, 40%); - - &.empty { - text-align: center; - vertical-align: middle; - - a { - @extend .button; - @include inline-block(); - margin-top: 10px; - } - } - - ol { - list-style: none; - margin: 0; - padding: 0; - - li { - border-bottom: 1px solid lighten($dark-blue, 60%); - - a { - color: #000; - } - - ol { - list-style: none; - margin: 0; - padding: 0; - - li { - padding: 6px; - position: relative; - - &:last-child { - border-bottom: 0; - } - - &:hover { - background-color: lighten($yellow, 10%); - - a.draggable { - opacity: 1; - } - } - - a.draggable { - float: right; - opacity: .4; - } - - &.group { - padding: 0; - - header { - padding: 6px; - background: none; - - h3 { - font-size: 14px; - margin: 0; - } - } - - ol { - border-left: 4px solid #999; - border-bottom: 0; - margin: 0; - padding: 0; - - li { - &:last-child { - border-bottom: 0; - } - } - } - } - } - } - } - } - } - - &.scratch-pad { - @include box-sizing(border-box); - display: table-cell; - width: flex-grid(3, 9) + flex-gutter(9); - vertical-align: top; - - ol { - list-style: none; - margin: 0; - padding: 0; - - li { - background: $light-blue; - - &:last-child { - border-bottom: 0; - } - - &.new-module a { - background-color: darken($light-blue, 2%); - border-bottom: 1px solid darken($light-blue, 8%); - - &:hover { - background-color: lighten($yellow, 10%); - } - } - - a { - color: $dark-blue; - } - - ul { - list-style: none; - margin: 0; - padding: 0; - - li { - padding: 6px; - border-collapse: collapse; - border-bottom: 1px solid darken($light-blue, 8%); - position: relative; - - &:last-child { - border-bottom: 1px solid darken($light-blue, 8%); - } - - &:hover { - background-color: lighten($yellow, 10%); - - a.draggable { - opacity: 1; - } - } - - - &.empty { - padding: 12px; - - a { - @extend .button; - display: block; - text-align: center; - } - } - - a.draggable { - opacity: .3; - } - } - } - } - } - } - } - } -} diff --git a/cms/static/sass/_subsection.scss b/cms/static/sass/_subsection.scss deleted file mode 100644 index a39c0d757a..0000000000 --- a/cms/static/sass/_subsection.scss +++ /dev/null @@ -1,295 +0,0 @@ -.subsection .main-wrapper { - margin: 40px; -} - -.subsection .inner-wrapper { - @include clearfix(); -} - -.subsection-body { - padding: 32px 40px; - @include clearfix; - - > div { - margin-bottom: 40px; - } - - input { - font-size: 14px; - } - - .unit-subtitle { - display: block; - width: 100%; - } - - .sortable-unit-list { - ol { - @include tree-view; - } - } - - .policy-list { - input[disabled] { - border: none; - @include box-shadow(none); - } - - .policy-list-name { - margin-right: 5px; - margin-bottom: 10px; - } - - .policy-list-value { - width: 320px; - margin-right: 10px; - } - } - - .policy-list-element { - .save-button, - .cancel-button { - display: none; - } - - .edit-icon { - margin-right: 8px; - } - - &.editing, - &.new-policy-list-element { - .policy-list-name, - .policy-list-value { - border: 1px solid #b0b6c2; - @include linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, .3)); - background-color: #edf1f5; - @include box-shadow(0 1px 2px rgba(0, 0, 0, .1) inset); - } - } - } - - .new-policy-list-element { - padding: 10px 10px 0; - margin: 0 -10px 10px; - border-radius: 3px; - background: $mediumGrey; - - .save-button { - @include blue-button; - margin-bottom: 10px; - } - - .cancel-button { - @include white-button; - } - - .edit-icon { - display: none; - } - - .delete-icon { - display: none; - } - } - - .new-policy-item { - margin: 10px 0; - - .plus-icon-small { - position: relative; - top: -1px; - vertical-align: middle; - } - } -} - -.subsection-name-input { - label { - display: block; - } - - input { - width: 100%; - font-size: 20px; - } -} - -.scheduled-date-input, -.due-date-input { - @include clearfix; - - .date-input, - .time-input { - display: inline-block; - width: 100px; - } - - .inherits-check { - label { - font-size: 13px; - } - } - - .notice { - margin-top: 6px; - font-size: 11px; - color: #999; - } -} - -.due-date-input { - label { - display: inline-block !important; - margin-right: 10px; - } - - a { - font-size: 11px; - font-weight: bold; - text-transform: uppercase; - } - - .date-setter { - @include clearfix; - display: none; - } - - .remove-date { - display: block; - } -} - -.row.visibility { - label { - display: inline-block !important; - margin-right: 10px; - line-height: 21px; - } - - a { - display: inline-block; - height: 31px; - margin-right: 8px; - vertical-align: middle; - font-size: 11px; - font-weight: 700; - line-height: 31px; - text-transform: uppercase; - } - - .large-toggle { - width: 41px; - background: url(../img/large-toggles.png) no-repeat; - background-position: 0 -50px; - - .hidden { - background-position: 0 -5px; - } - } -} - -.gradable { - - label { - display: inline-block; - vertical-align: top; - } - - .gradable-status { - position: relative; - top: -4px; - display: inline-block; - margin-left: 10px; - width: 65%; - - .status-label { - margin: 0; - padding: 0; - background: transparent; - color: $blue; - border: none; - font-size: 11px; - font-weight: bold; - text-transform: uppercase; - } - - .menu-toggle { - z-index: 100; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 20px; - background: transparent; - - &:hover, &.is-active { - color: $blue; - } - } - - .menu { - z-index: 1; - position: absolute; - top: -12px; - left: -7px; - display: none; - width: 100%; - margin: 0; - padding: 8px 12px; - opacity: 0.0; - background: $white; - border: 1px solid $mediumGrey; - font-size: 12px; - @include border-radius(4px); - @include box-shadow(0 1px 2px rgba(0, 0, 0, .2)); - @include transition(opacity .15s); - - - li { - margin-bottom: 3px; - padding-bottom: 3px; - border-bottom: 1px solid $lightGrey; - - &:last-child { - margin-bottom: 0; - padding-bottom: 0; - border: none; - } - } - - a { - - &.is-selected { - font-weight: bold; - } - } - } - - // dropdown state - &.is-active { - - .menu { - z-index: 10000; - display: block; - opacity: 1.0; - } - - .menu-toggle { - z-index: 1000; - } - } - - // set state - &.is-set { - - .menu-toggle { - color: $blue; - } - - .status-label { - display: block; - color: $blue; - } - } - } -} \ No newline at end of file diff --git a/cms/static/sass/_unit.scss b/cms/static/sass/_unit.scss deleted file mode 100644 index b7600e4205..0000000000 --- a/cms/static/sass/_unit.scss +++ /dev/null @@ -1,667 +0,0 @@ -.unit .main-wrapper { - @include clearfix(); - margin: 40px; -} - -//Problem Selector tab menu requirements -.js .tabs .tab { - display: none; -} -//end problem selector reqs - -.main-column { - clear: both; - float: left; - width: 70%; -} - -.unit-body.published { - .components > li { - border: none; - - .rendered-component { - padding: 0 20px; - } - } -} - -.unit-body { - .breadcrumbs { - border-radius: 3px 3px 0 0; - border-bottom: 1px solid #cbd1db; - @include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0) 70%); - background-color: #edf1f5; - @include box-shadow(0 1px 0 rgba(255, 255, 255, .7) inset); - @include clearfix; - - li { - float: left; - } - - a, - .current-page { - display: block; - padding: 15px 35px 15px 30px; - font-size: 14px; - background: url(../img/breadcrumb-arrow.png) no-repeat right center; - } - } - - h2 { - margin: 30px 40px 30px 0; - color: #646464; - font-size: 19px; - font-weight: 300; - letter-spacing: 1px; - text-transform: uppercase; - } - - .components { - - > li { - position: relative; - z-index: 10; - margin: 20px 40px; - - - - .title { - margin: 0 0 15px 0; - color: $mediumGrey; - - .value { - } - } - - &.new-component-item { - margin: 20px 0px; - border-top: 1px solid $mediumGrey; - box-shadow: 0 2px 1px rgba(182, 182, 182, 0.75) inset; - background-color: $lightGrey; - margin-bottom: 0px; - padding-bottom: 20px; - - .new-component-button { - display: block; - padding: 20px; - text-align: center; - color: #edf1f5; - } - - h5 { - margin: 20px 0px; - color: #fff; - font-weight: 600; - font-size: 18px; - } - - .rendered-component { - display: none; - background: #fff; - border-radius: 3px 3px 0 0; - } - - .new-component-type { - - a, - li { - display: inline-block; - } - - a { - border: 1px solid $mediumGrey; - width: 100px; - height: 100px; - color: #fff; - margin-right: 15px; - margin-bottom: 20px; - border-radius: 8px; - font-size: 15px; - line-height: 14px; - text-align: center; - @include box-shadow(0 1px 1px rgba(0, 0, 0, .2), 0 1px 0 rgba(255, 255, 255, .4) inset); - - .name { - position: absolute; - bottom: 5px; - left: 0; - width: 100%; - padding: 10px; - @include box-sizing(border-box); - color: #fff; - } - } - } - - .new-component-templates { - display: none; - margin: 20px 40px 20px 40px; - border-radius: 3px; - border: 1px solid $mediumGrey; - background-color: #fff; - @include box-shadow(0 1px 1px rgba(0, 0, 0, .2), 0 1px 0 rgba(255, 255, 255, .4) inset); - @include clearfix; - - .cancel-button { - margin: 20px 0px 10px 10px; - @include white-button; - } - - .problem-type-tabs { - display: none; - } - - // specific menu types - &.new-component-problem { - padding-bottom:10px; - - .ss-icon, .editor-indicator { - display: inline-block; - } - - .problem-type-tabs { - display: inline-block; - } - } - } - - .new-component-type, - .new-component-template { - @include clearfix; - - a { - position: relative; - border: 1px solid $darkGreen; - background: tint($green,20%); - color: #fff; - - &:hover { - background: $brightGreen; - } - } - } - - .problem-type-tabs { - list-style-type: none; - border-radius: 0; - width: 100%; - @include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0)); - background-color: $lightBluishGrey; - @include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset); - - li:first-child { - margin-left: 20px; - } - - li { - float:left; - display:inline-block; - text-align:center; - width: auto; - @include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0)); - background-color: tint($lightBluishGrey, 10%); - @include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset); - opacity:.8; - - &:hover { - opacity:1; - background-color: tint($lightBluishGrey, 20%); - } - - &.ui-state-active { - border: 0px; - @include active; - opacity:1; - } - } - - a{ - display: block; - padding: 15px 25px; - font-size: 15px; - line-height: 16px; - text-align: center; - color: #3c3c3c; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.3); - } - } - - .new-component-template { - - a { - background: #fff; - border: 0px; - color: #3c3c3c; - @include transition (none); - - &:hover { - background: tint($green,30%); - color: #fff; - @include transition(background-color .15s); - } - } - - li { - border:none; - border-bottom: 1px dashed $lightGrey; - color: #fff; - } - - li:first-child { - a { - border-top: 0px; - } - } - - li:nth-child(2) { - a { - border-radius: 0px; - } - } - - a { - @include clearfix(); - display: block; - padding: 7px 20px; - border-bottom: none; - font-weight: 500; - - .name { - float: left; - - .ss-icon { - @include transition(opacity .15s); - display: inline-block; - top: 1px; - margin-right: 5px; - opacity: 0.5; - width: 17; - height: 21px; - vertical-align: middle; - } - } - - .editor-indicator { - @include transition(opacity .15s); - float: right; - position: relative; - top: 3px; - font-size: 12px; - opacity: 0.3; - } - - .ss-icon, .editor-indicator { - display: none; - } - - &:hover { - color: #fff; - - .ss-icon { - opacity: 1.0; - } - - .editor-indicator { - opacity: 1.0; - } - } - } - - // specific editor types - .empty { - - a { - line-height: 1.4; - font-weight: 400; - background: #fff; - color: #3c3c3c; - - - &:hover { - background: tint($green,30%); - color: #fff; - } - } - } - } - - .new-component { - text-align: center; - - h5 { - color: $darkGreen; - } - - } - } - } - } - - .component { - border: 1px solid $lightBluishGrey2; - border-radius: 3px; - background: #fff; - @include transition(none); - - &:hover { - border-color: #6696d7; - - .drag-handle { - background-color: $blue; - border-color: $blue; - } - } - - &.editing { - border: 1px solid $lightBluishGrey2; - z-index: auto; - - .drag-handle, - .component-actions { - display: none; - } - } - - &.component-placeholder { - border-color: #6696d7; - } - - .component-actions { - position: absolute; - top: 7px; - right: 9px; - } - - .drag-handle { - position: absolute; - display: block; - top: -1px; - right: -16px; - z-index: 10; - width: 15px; - height: 100%; - border-radius: 0 3px 3px 0; - border: 1px solid $lightBluishGrey2; - background: url(../img/white-drag-handles.png) center no-repeat $lightBluishGrey2; - cursor: move; - @include transition(none); - } - } - - .xmodule_display { - padding: 40px 20px 20px; - overflow-x: auto; - - h1 { - float: none; - margin-left: 0; - } - } - - .wrapper-component-editor { - z-index: 9999; - position: relative; - background: $lightBluishGrey2; - } - - .component-editor { - @include edit-box; - @include box-shadow(none); - display: none; - padding: 20px; - border-radius: 2px 2px 0 0; - - .metadata_edit { - margin-bottom: 20px; - font-size: 13px; - - li { - margin-bottom: 10px; - } - - label { - display: inline-block; - margin-right: 10px; - } - } - - h3 { - margin-bottom: 10px; - font-size: 18px; - font-weight: 700; - } - - h5 { - margin-bottom: 8px; - color: #fff; - font-weight: 700; - } - - .save-button { - margin-top: 10px; - margin: 15px 8px 0 0; - } - } -} - -.unit-settings { - .window-contents { - padding: 10px 20px; - } - - .unit-actions { - border-bottom: none; - padding-bottom: 0; - } - - .published-alert { - display: none; - padding: 10px; - border: 1px solid #edbd3c; - border-radius: 3px; - background: #fbf6e1; - font-size: 14px; - line-height: 1.4; - - div { - margin-top: 15px; - } - } - - input[type="radio"] { - margin-right: 7px; - } - - .status { - font-size: 12px; - - strong { - font-weight: 700; - } - } - - .preview-button, .view-button { - @include white-button; - margin-bottom: 10px; - } - - .publish-button { - @include orange-button; - } - - .delete-button { - @include blue-button; - } - - .delete-draft { - display: inline-block; - } - - .delete-button, - .preview-button, - .publish-button, - .view-button { - font-size: 11px; - margin-top: 10px; - padding: 6px 15px 8px; - } -} - -.unit-history { - &.collapsed { - h4 { - border-bottom: none; - border-radius: 3px; - } - - .window-contents { - display: none; - } - } - - ol { - border: 1px solid #ced2db; - - li { - display: block; - padding: 6px 8px 8px 10px; - background: #edf1f5; - font-size: 12px; - - &:hover { - background: #fffcf1; - - .item-actions { - display: block; - } - } - - &.checked { - background: #d1dae3; - } - - .item-actions { - display: none; - } - - input[type="radio"] { - margin-right: 7px; - } - } - } -} - -.unit-location { - .url { - width: 100%; - margin-bottom: 10px; - @include box-shadow(none); - } - - .draft-tag, - .hidden-tag, - .private-tag, - .has-new-draft-tag { - font-size: 8px; - } - - .window-contents > ol { - @include tree-view; - - .section-item { - display: inline-block; - width: 100%; - font-size: 11px; - padding: 2px 8px 4px; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - @include box-sizing(border-box); - } - - ol { - .section-item { - padding-left: 20px; - } - - .new-unit-item { - margin-left: 20px; - } - } - - ol ol { - .section-item { - padding-left: 34px; - } - - .new-unit-item { - margin: 0 0 10px 41px; - } - } - } -} - -.edit-state-draft { - .visibility, - - .edit-draft-message, - .view-button { - display: none; - } - - .published-alert { - display: block; - } -} - -.edit-state-public { - .delete-draft, - .component-actions, - .new-component-item, - .editing-draft-alert, - .publish-draft-message, - .preview-button { - display: none; - } - - .published-alert { - display: block; - } - - .drag-handle { - display: none !important; - } -} - -.edit-state-private { - .delete-draft, - .publish-draft, - .editing-draft-alert, - .create-draft, - .view-button { - display: none; - } -} - -// editing units from courseware -body.unit { - - .component { - padding-top: 30px; - - .component-actions { - @include box-sizing(border-box); - position: absolute; - width: 100%; - padding: 15px; - top: 0; - left: 0; - border-bottom: 1px solid $lightBluishGrey2; - background: $lightGrey; - } - - &.editing { - padding-top: 0; - } - } -} diff --git a/cms/static/sass/_variables.scss b/cms/static/sass/_variables.scss index 4d8e26b2f9..c43286de72 100644 --- a/cms/static/sass/_variables.scss +++ b/cms/static/sass/_variables.scss @@ -1,3 +1,6 @@ +// studio - utilities - variables +// ==================== + $baseline: 20px; // grid @@ -12,11 +15,18 @@ $fg-min-width: 900px; // type $sans-serif: 'Open Sans', $verdana; $body-line-height: golden-ratio(.875em, 1); -$error-red: rgb(253, 87, 87); // colors - new for re-org $black: rgb(0,0,0); +$black-t0: rgba(0,0,0,0.125); +$black-t1: rgba(0,0,0,0.25); +$black-t2: rgba(0,0,0,0.50); +$black-t3: rgba(0,0,0,0.75); $white: rgb(255,255,255); +$white-t0: rgba(255,255,255,0.125); +$white-t1: rgba(255,255,255,0.25); +$white-t2: rgba(255,255,255,0.50); +$white-t3: rgba(255,255,255,0.75); $gray: rgb(127,127,127); $gray-l1: tint($gray,20%); @@ -39,6 +49,12 @@ $blue-d1: shade($blue,20%); $blue-d2: shade($blue,40%); $blue-d3: shade($blue,60%); $blue-d4: shade($blue,80%); +$blue-s1: saturate($blue,15%); +$blue-s2: saturate($blue,30%); +$blue-s3: saturate($blue,45%); +$blue-u1: desaturate($blue,15%); +$blue-u2: desaturate($blue,30%); +$blue-u3: desaturate($blue,45%); $pink: rgb(183, 37, 103); $pink-l1: tint($pink,20%); @@ -50,6 +66,29 @@ $pink-d1: shade($pink,20%); $pink-d2: shade($pink,40%); $pink-d3: shade($pink,60%); $pink-d4: shade($pink,80%); +$pink-s1: saturate($pink,15%); +$pink-s2: saturate($pink,30%); +$pink-s3: saturate($pink,45%); +$pink-u1: desaturate($pink,15%); +$pink-u2: desaturate($pink,30%); +$pink-u3: desaturate($pink,45%); + +$red: rgb(178, 6, 16); +$red-l1: tint($red,20%); +$red-l2: tint($red,40%); +$red-l3: tint($red,60%); +$red-l4: tint($red,80%); +$red-l5: tint($red,90%); +$red-d1: shade($red,20%); +$red-d2: shade($red,40%); +$red-d3: shade($red,60%); +$red-d4: shade($red,80%); +$red-s1: saturate($red,15%); +$red-s2: saturate($red,30%); +$red-s3: saturate($red,45%); +$red-u1: desaturate($red,15%); +$red-u2: desaturate($red,30%); +$red-u3: desaturate($red,45%); $green: rgb(37, 184, 90); $green-l1: tint($green,20%); @@ -61,6 +100,12 @@ $green-d1: shade($green,20%); $green-d2: shade($green,40%); $green-d3: shade($green,60%); $green-d4: shade($green,80%); +$green-s1: saturate($green,15%); +$green-s2: saturate($green,30%); +$green-s3: saturate($green,45%); +$green-u1: desaturate($green,15%); +$green-u2: desaturate($green,30%); +$green-u3: desaturate($green,45%); $yellow: rgb(231, 214, 143); $yellow-l1: tint($yellow,20%); @@ -72,6 +117,29 @@ $yellow-d1: shade($yellow,20%); $yellow-d2: shade($yellow,40%); $yellow-d3: shade($yellow,60%); $yellow-d4: shade($yellow,80%); +$yellow-s1: saturate($yellow,15%); +$yellow-s2: saturate($yellow,30%); +$yellow-s3: saturate($yellow,45%); +$yellow-u1: desaturate($yellow,15%); +$yellow-u2: desaturate($yellow,30%); +$yellow-u3: desaturate($yellow,45%); + +$orange: rgb(237, 189, 60); +$orange-l1: tint($orange,20%); +$orange-l2: tint($orange,40%); +$orange-l3: tint($orange,60%); +$orange-l4: tint($orange,80%); +$orange-l5: tint($orange,90%); +$orange-d1: shade($orange,20%); +$orange-d2: shade($orange,40%); +$orange-d3: shade($orange,60%); +$orange-d4: shade($orange,80%); +$orange-s1: saturate($orange,15%); +$orange-s2: saturate($orange,30%); +$orange-s3: saturate($orange,45%); +$orange-u1: desaturate($orange,15%); +$orange-u2: desaturate($orange,30%); +$orange-u3: desaturate($orange,45%); $shadow: rgba(0,0,0,0.2); $shadow-l1: rgba(0,0,0,0.1); @@ -80,8 +148,6 @@ $shadow-d1: rgba(0,0,0,0.4); // colors - inherited $baseFontColor: #3c3c3c; $offBlack: #3c3c3c; -$orange: #edbd3c; -$red: #b20610; $green: #108614; $lightGrey: #edf1f5; $mediumGrey: #b0b6c2; @@ -94,4 +160,5 @@ $brightGreen: rgb(22, 202, 87); $disabledGreen: rgb(124, 206, 153); $darkGreen: rgb(52, 133, 76); $lightBluishGrey: rgb(197, 207, 223); -$lightBluishGrey2: rgb(213, 220, 228); \ No newline at end of file +$lightBluishGrey2: rgb(213, 220, 228); +$error-red: rgb(253, 87, 87); \ No newline at end of file diff --git a/cms/static/sass/_video.scss b/cms/static/sass/_video.scss deleted file mode 100644 index b68176e2db..0000000000 --- a/cms/static/sass/_video.scss +++ /dev/null @@ -1,33 +0,0 @@ -section.video-new, section.video-edit { - > section { - - section.upload { - padding: 6px; - margin-bottom: 10px; - border: 1px solid #ddd; - - a.upload-button { - @extend .button; - @include inline-block(); - } - } - - section.in-use { - h2 { - font-size: 14px; - } - - div { - background: #eee; - text-align: center; - padding: 6px; - } - } - - a.save-update { - @extend .button; - @include inline-block(); - margin-top: 20px; - } - } -} diff --git a/cms/static/sass/_week.scss b/cms/static/sass/_week.scss deleted file mode 100644 index b638a36f5c..0000000000 --- a/cms/static/sass/_week.scss +++ /dev/null @@ -1,256 +0,0 @@ -section.week-edit, -section.week-new, -section.sequence-edit { - - > header { - border-bottom: 2px solid #333; - @include clearfix(); - - div { - @include clearfix(); - padding: 6px 20px; - - h1 { - font-size: 18px; - text-transform: uppercase; - letter-spacing: 1px; - float: left; - } - - p { - float: right; - } - - &.week { - background: #eee; - font-size: 12px; - border-bottom: 1px solid #ccc; - - h2 { - font-size: 12px; - @include inline-block(); - margin-right: 20px; - } - - ul { - list-style: none; - @include inline-block(); - - li { - @include inline-block(); - margin-right: 10px; - - p { - float: none; - } - } - } - } - } - - section.goals { - background: #eee; - padding: 6px 20px; - border-top: 1px solid #ccc; - - ul { - list-style: none; - color: #999; - - li { - margin-bottom: 6px; - - &:last-child { - margin-bottom: 0; - } - } - } - } - } - - > section.content { - @include box-sizing(border-box); - padding: 20px; - - section.filters { - @include clearfix; - margin-bottom: 10px; - background: #efefef; - border: 1px solid #ddd; - - ul { - @include clearfix(); - list-style: none; - padding: 6px; - - li { - @include inline-block(); - - &.advanced { - float: right; - } - } - } - } - - > div { - display: table; - border: 1px solid; - width: 100%; - - section { - header { - background: #eee; - padding: 6px; - border-bottom: 1px solid #ccc; - @include clearfix; - - h2 { - text-transform: uppercase; - letter-spacing: 1px; - font-size: 12px; - float: left; - } - } - - &.modules { - @include box-sizing(border-box); - display: table-cell; - width: flex-grid(6, 9); - border-right: 1px solid #333; - - &.empty { - text-align: center; - vertical-align: middle; - - a { - @extend .button; - @include inline-block(); - margin-top: 10px; - } - } - - ol { - list-style: none; - border-bottom: 1px solid #333; - - li { - border-bottom: 1px solid #333; - - &:last-child{ - border-bottom: 0; - } - - a { - color: #000; - } - - ol { - list-style: none; - - li { - padding: 6px; - - &:hover { - a.draggable { - opacity: 1; - } - } - - a.draggable { - float: right; - opacity: .5; - } - - &.group { - padding: 0; - - header { - padding: 6px; - background: none; - - h3 { - font-size: 14px; - } - } - - - ol { - border-left: 4px solid #999; - border-bottom: 0; - - li { - &:last-child { - border-bottom: 0; - } - } - } - } - } - } - } - } - } - - &.scratch-pad { - @include box-sizing(border-box); - display: table-cell; - width: flex-grid(3, 9) + flex-gutter(9); - vertical-align: top; - - ol { - list-style: none; - border-bottom: 1px solid #999; - - li { - border-bottom: 1px solid #999; - background: #f9f9f9; - - &:last-child { - border-bottom: 0; - } - - ul { - list-style: none; - - li { - padding: 6px; - - &:last-child { - border-bottom: 0; - } - - &:hover { - a.draggable { - opacity: 1; - } - } - - &.empty { - padding: 12px; - - a { - @extend .button; - display: block; - text-align: center; - } - } - - a.draggable { - float: right; - opacity: .3; - } - - a { - color: #000; - } - } - } - - } - } - } - } - } - } -} diff --git a/cms/static/sass/_content-types.scss b/cms/static/sass/assets/_content-types.scss similarity index 100% rename from cms/static/sass/_content-types.scss rename to cms/static/sass/assets/_content-types.scss diff --git a/cms/static/sass/_fonts.scss b/cms/static/sass/assets/_fonts.scss similarity index 100% rename from cms/static/sass/_fonts.scss rename to cms/static/sass/assets/_fonts.scss diff --git a/cms/static/sass/_graphics.scss b/cms/static/sass/assets/_graphics.scss similarity index 100% rename from cms/static/sass/_graphics.scss rename to cms/static/sass/assets/_graphics.scss diff --git a/cms/static/sass/_keyframes.scss b/cms/static/sass/assets/_keyframes.scss similarity index 100% rename from cms/static/sass/_keyframes.scss rename to cms/static/sass/assets/_keyframes.scss diff --git a/cms/static/sass/base-style.scss b/cms/static/sass/base-style.scss index dceac4233d..c7ec38e756 100644 --- a/cms/static/sass/base-style.scss +++ b/cms/static/sass/base-style.scss @@ -1,39 +1,51 @@ +// studio - css architecture +// ==================== + +// bourbon libs and resets @import 'bourbon/bourbon'; @import 'bourbon/addons/button'; @import 'vendor/normalize'; -@import 'keyframes'; - @import 'reset'; + +// utilities +@import 'variables'; @import 'mixins'; +@import 'cms_mixins'; -@import "fonts"; -@import "variables"; -@import "cms_mixins"; -@import "extends"; -@import "base"; -@import "header"; -@import "footer"; -@import "dashboard"; -@import "courseware"; -@import "subsection"; -@import "unit"; -@import "assets"; -@import "static-pages"; -@import "users"; -@import "import"; -@import "export"; -@import "settings"; -@import "course-info"; -@import "landing"; -@import "graphics"; -@import "modal"; -@import "alerts"; -@import "login"; -@import "account"; -@import "index"; -@import 'jquery-ui-calendar'; +// assets +@import 'assets/fonts'; +@import 'assets/graphics'; +@import 'assets/keyframes'; -@import 'content-types'; +// base +@import 'base'; +// elements +@import 'elements/header'; +@import 'elements/footer'; +@import 'elements/navigation'; +@import 'elements/forms'; +@import 'elements/modal'; +@import 'elements/alerts'; +@import 'elements/jquery-ui-calendar'; + +// specific views +@import 'views/account'; +@import 'views/assets'; +@import 'views/updates'; +@import 'views/dashboard'; +@import 'views/export'; +@import 'views/index'; +@import 'views/import'; +@import 'views/outline'; +@import 'views/settings'; +@import 'views/static-pages'; +@import 'views/subsection'; +@import 'views/unit'; +@import 'views/users'; + +@import 'assets/content-types'; + +// xblock-related @import 'module/module-styles.scss'; @import 'descriptor/module-styles.scss'; diff --git a/cms/static/sass/_alerts.scss b/cms/static/sass/elements/_alerts.scss similarity index 96% rename from cms/static/sass/_alerts.scss rename to cms/static/sass/elements/_alerts.scss index bd7f687f67..9c15f811e0 100644 --- a/cms/static/sass/_alerts.scss +++ b/cms/static/sass/elements/_alerts.scss @@ -1,3 +1,6 @@ +// studio - elements - alerts, notifications, prompts +// ==================== + // notifications .wrapper-notification { @include clearfix(); diff --git a/cms/static/sass/_footer.scss b/cms/static/sass/elements/_footer.scss similarity index 93% rename from cms/static/sass/_footer.scss rename to cms/static/sass/elements/_footer.scss index 66a9ce0e95..b1c0f57bb2 100644 --- a/cms/static/sass/_footer.scss +++ b/cms/static/sass/elements/_footer.scss @@ -1,4 +1,6 @@ -//studio global footer +// studio - elements - global footer +// ==================== + .wrapper-footer { margin: ($baseline*1.5) 0 $baseline 0; padding: $baseline; diff --git a/cms/static/sass/elements/_forms.scss b/cms/static/sass/elements/_forms.scss new file mode 100644 index 0000000000..384ffc0509 --- /dev/null +++ b/cms/static/sass/elements/_forms.scss @@ -0,0 +1,76 @@ +// studio - elements - forms +// ==================== + +// forms - general +input[type="text"], +input[type="email"], +input[type="password"], +textarea.text { + padding: 6px 8px 8px; + @include box-sizing(border-box); + border: 1px solid $mediumGrey; + border-radius: 2px; + @include linear-gradient($lightGrey, tint($lightGrey, 90%)); + background-color: $lightGrey; + @include box-shadow(0 1px 2px rgba(0, 0, 0, .1) inset); + font-family: 'Open Sans', sans-serif; + font-size: 11px; + color: $baseFontColor; + outline: 0; + + &::-webkit-input-placeholder, + &:-moz-placeholder, + &:-ms-input-placeholder { + color: #979faf; + } + + &:focus { + @include linear-gradient($paleYellow, tint($paleYellow, 90%)); + outline: 0; + } +} + +// forms - specific +input.search { + padding: 6px 15px 8px 30px; + @include box-sizing(border-box); + border: 1px solid $darkGrey; + border-radius: 20px; + background: url(../img/search-icon.png) no-repeat 8px 7px #edf1f5; + font-family: 'Open Sans', sans-serif; + color: $baseFontColor; + outline: 0; + + &::-webkit-input-placeholder { + color: #979faf; + } +} + +label { + font-size: 12px; +} + +code { + padding: 0 4px; + border-radius: 3px; + background: #eee; + font-family: Monaco, monospace; +} + +.CodeMirror { + font-size: 13px; + border: 1px solid $darkGrey; + background: #fff; +} + +.text-editor { + width: 100%; + min-height: 80px; + padding: 10px; + @include box-sizing(border-box); + border: 1px solid $mediumGrey; + @include linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.3)); + background-color: #edf1f5; + @include box-shadow(0 1px 2px rgba(0, 0, 0, 0.1) inset); + font-family: Monaco, monospace; +} \ No newline at end of file diff --git a/cms/static/sass/_header.scss b/cms/static/sass/elements/_header.scss similarity index 99% rename from cms/static/sass/_header.scss rename to cms/static/sass/elements/_header.scss index ca1092f44b..e8df37f57f 100644 --- a/cms/static/sass/_header.scss +++ b/cms/static/sass/elements/_header.scss @@ -1,4 +1,4 @@ -// studio global header and navigation +// studio - elements - global header // ==================== .wrapper-header { diff --git a/cms/static/sass/_jquery-ui-calendar.scss b/cms/static/sass/elements/_jquery-ui-calendar.scss similarity index 94% rename from cms/static/sass/_jquery-ui-calendar.scss rename to cms/static/sass/elements/_jquery-ui-calendar.scss index 96cffc059f..3d20bde642 100644 --- a/cms/static/sass/_jquery-ui-calendar.scss +++ b/cms/static/sass/elements/_jquery-ui-calendar.scss @@ -1,3 +1,6 @@ +// studio - elements - JQUI calendar +// ==================== + .ui-datepicker { border-color: $darkGrey; border-radius: 2px; diff --git a/cms/static/sass/_modal.scss b/cms/static/sass/elements/_modal.scss similarity index 94% rename from cms/static/sass/_modal.scss rename to cms/static/sass/elements/_modal.scss index f9fbf81a8f..b81baf4565 100644 --- a/cms/static/sass/_modal.scss +++ b/cms/static/sass/elements/_modal.scss @@ -1,3 +1,6 @@ +// studio - elements - modal windows +// ==================== + .modal-cover { display: none; position: fixed; diff --git a/cms/static/sass/elements/_navigation.scss b/cms/static/sass/elements/_navigation.scss new file mode 100644 index 0000000000..066c47298b --- /dev/null +++ b/cms/static/sass/elements/_navigation.scss @@ -0,0 +1,24 @@ +// studio - elements - navigation +// ==================== + +// common + +// ==================== + +// primary + +// ==================== + +// right hand side + +// ==================== + +// tabs + +// ==================== + +// dropdown + +// ==================== + +// \ No newline at end of file diff --git a/cms/static/sass/_account.scss b/cms/static/sass/views/_account.scss similarity index 99% rename from cms/static/sass/_account.scss rename to cms/static/sass/views/_account.scss index 650743979f..1206db5e76 100644 --- a/cms/static/sass/_account.scss +++ b/cms/static/sass/views/_account.scss @@ -1,5 +1,6 @@ -// Studio - Sign In/Up +// studio - views - sign up/in // ==================== + body.signup, body.signin { .wrapper-content { diff --git a/cms/static/sass/_assets.scss b/cms/static/sass/views/_assets.scss similarity index 97% rename from cms/static/sass/_assets.scss rename to cms/static/sass/views/_assets.scss index d9b215faec..779dc56684 100644 --- a/cms/static/sass/_assets.scss +++ b/cms/static/sass/views/_assets.scss @@ -1,4 +1,8 @@ -.uploads { +// studio - views - assets +// ==================== + +body.course.uploads { + input.asset-search-input { float: left; width: 260px; diff --git a/cms/static/sass/views/_dashboard.scss b/cms/static/sass/views/_dashboard.scss new file mode 100644 index 0000000000..a02c4e0c29 --- /dev/null +++ b/cms/static/sass/views/_dashboard.scss @@ -0,0 +1,124 @@ +// studio - views - user dashboard +// ==================== + +body.dashboard { + + .my-classes { + margin-top: $baseline; + } + + .class-list { + margin-top: 20px; + border-radius: 3px; + border: 1px solid $darkGrey; + background: #fff; + @include box-shadow(0 1px 2px rgba(0, 0, 0, .1)); + + li { + position: relative; + border-bottom: 1px solid $mediumGrey; + + &:last-child { + border-bottom: none; + } + + .class-link { + z-index: 100; + display: block; + padding: 20px 25px; + line-height: 1.3; + + &:hover { + background: $paleYellow; + + + .view-live-button { + opacity: 1.0; + pointer-events: auto; + } + } + } + } + + .class-name { + display: block; + font-size: 19px; + font-weight: 300; + } + + .detail { + font-size: 14px; + font-weight: 400; + margin-right: 20px; + color: #3c3c3c; + } + + // view live button + .view-live-button { + z-index: 10000; + position: absolute; + top: 15px; + right: $baseline; + padding: ($baseline/4) ($baseline/2); + opacity: 0; + pointer-events: none; + + &:hover { + opacity: 1.0; + pointer-events: auto; + } + } + } + + .new-course { + padding: 15px 25px; + margin-top: 20px; + border-radius: 3px; + border: 1px solid $darkGrey; + background: #fff; + box-shadow: 0 1px 2px rgba(0, 0, 0, .1); + @include clearfix; + + .row { + margin-bottom: 15px; + @include clearfix; + } + + .column { + float: left; + width: 48%; + } + + .column:first-child { + margin-right: 4%; + } + + .course-info { + width: 600px; + } + + label { + display: block; + font-size: 13px; + font-weight: 700; + } + + .new-course-org, + .new-course-number, + .new-course-name { + width: 100%; + } + + .new-course-name { + font-size: 19px; + font-weight: 300; + } + + .new-course-save { + @include blue-button; + } + + .new-course-cancel { + @include white-button; + } + } +} \ No newline at end of file diff --git a/cms/static/sass/_export.scss b/cms/static/sass/views/_export.scss similarity index 96% rename from cms/static/sass/_export.scss rename to cms/static/sass/views/_export.scss index e1ab7eb605..933bb50252 100644 --- a/cms/static/sass/_export.scss +++ b/cms/static/sass/views/_export.scss @@ -1,4 +1,8 @@ -.export { +// studio - views - course export +// ==================== + +body.course.export { + .export-overview { @extend .window; @include clearfix; @@ -118,6 +122,4 @@ } } } - - } \ No newline at end of file diff --git a/cms/static/sass/_import.scss b/cms/static/sass/views/_import.scss similarity index 95% rename from cms/static/sass/_import.scss rename to cms/static/sass/views/_import.scss index a0a1f5e512..e5fb955348 100644 --- a/cms/static/sass/_import.scss +++ b/cms/static/sass/views/_import.scss @@ -1,4 +1,8 @@ -.import { +// studio - views - course import +// ==================== + +body.course.import { + .import-overview { @extend .window; @include clearfix; diff --git a/cms/static/sass/_index.scss b/cms/static/sass/views/_index.scss similarity index 99% rename from cms/static/sass/_index.scss rename to cms/static/sass/views/_index.scss index e0f6d0f2b7..f4087a8605 100644 --- a/cms/static/sass/_index.scss +++ b/cms/static/sass/views/_index.scss @@ -1,5 +1,7 @@ -// how it works/not signed in index -.index { +// studio - views - how it works +// ==================== + +body.index { &.not-signedin { diff --git a/cms/static/sass/views/_outline.scss b/cms/static/sass/views/_outline.scss new file mode 100644 index 0000000000..0d72e2d2bf --- /dev/null +++ b/cms/static/sass/views/_outline.scss @@ -0,0 +1,680 @@ +// studio - views - course outline +// ==================== + +body.course.outline { + + input.courseware-unit-search-input { + float: left; + width: 260px; + background-color: #fff; + } + + .branch { + + .section-item { + @include clearfix(); + + .details { + display: block; + float: left; + margin-bottom: 0; + width: 650px; + } + + .gradable-status { + float: right; + position: relative; + top: -4px; + right: 50px; + width: 145px; + + .status-label { + position: absolute; + top: 2px; + right: -5px; + display: none; + width: 110px; + padding: 5px 40px 5px 10px; + @include border-radius(3px); + color: $lightGrey; + text-align: right; + font-size: 12px; + font-weight: bold; + line-height: 16px; + } + + .menu-toggle { + z-index: 10; + position: absolute; + top: 0; + right: 5px; + padding: 5px; + color: $mediumGrey; + + &:hover, &.is-active { + color: $blue; + } + } + + .menu { + z-index: 1; + display: none; + opacity: 0.0; + position: absolute; + top: -1px; + left: 5px; + margin: 0; + padding: 8px 12px; + background: $white; + border: 1px solid $mediumGrey; + font-size: 12px; + @include border-radius(4px); + @include box-shadow(0 1px 2px rgba(0, 0, 0, .2)); + @include transition(opacity .15s); + + + li { + width: 115px; + margin-bottom: 3px; + padding-bottom: 3px; + border-bottom: 1px solid $lightGrey; + + &:last-child { + margin-bottom: 0; + padding-bottom: 0; + border: none; + + a { + color: $darkGrey; + } + } + } + + a { + color: $blue; + + &.is-selected { + font-weight: bold; + } + } + } + + // dropdown state + &.is-active { + + .menu { + z-index: 1000; + display: block; + opacity: 1.0; + } + + .menu-toggle { + z-index: 10000; + } + } + + // set state + &.is-set { + + .menu-toggle { + color: $blue; + } + + .status-label { + display: block; + color: $blue; + } + } + } + } + } + + + .courseware-section { + position: relative; + background: #fff; + border-radius: 3px; + border: 1px solid $mediumGrey; + margin-top: 15px; + padding-bottom: 12px; + @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.1)); + + &:first-child { + margin-top: 0; + } + + &.collapsed { + padding-bottom: 0; + } + + label { + float: left; + line-height: 29px; + } + + .datepair { + float: left; + margin-left: 10px; + } + + .section-published-date { + position: absolute; + top: 19px; + right: 90px; + padding: 4px 10px; + border-radius: 3px; + background: $lightGrey; + text-align: right; + + .published-status { + font-size: 12px; + margin-right: 15px; + + strong { + font-weight: bold; + } + } + + .schedule-button { + @include blue-button; + } + + .edit-button { + @include blue-button; + } + + .schedule-button, + .edit-button { + font-size: 11px; + padding: 3px 15px 5px; + } + } + + .datepair .date, + .datepair .time { + padding-left: 0; + padding-right: 0; + border: none; + background: none; + @include box-shadow(none); + font-size: 13px; + font-weight: bold; + color: $blue; + cursor: pointer; + } + + .datepair .date { + width: 80px; + } + + .datepair .time { + width: 65px; + } + + &.collapsed .subsection-list, + .collapsed .subsection-list, + .collapsed > ol { + display: none !important; + } + + header { + min-height: 75px; + @include clearfix(); + + .item-details, .section-published-date { + + } + + .item-details { + display: inline-block; + padding: 20px 0 10px 0; + @include clearfix(); + + .section-name { + float: left; + margin-right: 10px; + width: 350px; + font-size: 19px; + font-weight: bold; + color: $blue; + } + + .section-name-span { + cursor: pointer; + @include transition(color .15s); + + &:hover { + color: $orange; + } + } + + .section-name-edit { + position: relative; + width: 400px; + background: $white; + + input { + font-size: 16px; + } + + .save-button { + @include blue-button; + padding: 7px 20px 7px; + margin-right: 5px; + } + + .cancel-button { + @include white-button; + padding: 7px 20px 7px; + } + } + + .section-published-date { + float: right; + width: 265px; + margin-right: 220px; + @include border-radius(3px); + background: $lightGrey; + + .published-status { + font-size: 12px; + margin-right: 15px; + + strong { + font-weight: bold; + } + } + + .schedule-button { + @include blue-button; + } + + .edit-button { + @include blue-button; + } + + .schedule-button, + .edit-button { + font-size: 11px; + padding: 3px 15px 5px; + + } + } + + .gradable-status { + position: absolute; + top: 20px; + right: 70px; + width: 145px; + + .status-label { + position: absolute; + top: 0; + right: 2px; + display: none; + width: 100px; + padding: 10px 35px 10px 10px; + @include border-radius(3px); + background: $lightGrey; + color: $lightGrey; + text-align: right; + font-size: 12px; + font-weight: bold; + line-height: 16px; + } + + .menu-toggle { + z-index: 10; + position: absolute; + top: 2px; + right: 5px; + padding: 5px; + color: $lightGrey; + + &:hover, &.is-active { + color: $blue; + } + } + + .menu { + z-index: 1; + display: none; + opacity: 0.0; + position: absolute; + top: -1px; + left: 2px; + margin: 0; + padding: 8px 12px; + background: $white; + border: 1px solid $mediumGrey; + font-size: 12px; + @include border-radius(4px); + @include box-shadow(0 1px 2px rgba(0, 0, 0, .2)); + @include transition(opacity .15s); + @include transition(display .15s); + + + li { + width: 115px; + margin-bottom: 3px; + padding-bottom: 3px; + border-bottom: 1px solid $lightGrey; + + &:last-child { + margin-bottom: 0; + padding-bottom: 0; + border: none; + + a { + color: $darkGrey; + } + } + } + + a { + + &.is-selected { + font-weight: bold; + } + } + } + + // dropdown state + &.is-active { + + .menu { + z-index: 1000; + display: block; + opacity: 1.0; + } + + + .menu-toggle { + z-index: 10000; + } + } + + // set state + &.is-set { + + .menu-toggle { + color: $blue; + } + + .status-label { + display: block; + color: $blue; + } + } + + float: left; + padding: 21px 0 0; + } + } + + .item-actions { + margin-top: 21px; + margin-right: 12px; + + .edit-button, + .delete-button { + margin-top: -3px; + } + } + + .expand-collapse-icon { + float: left; + margin: 29px 6px 16px 16px; + @include transition(none); + + &.expand { + background-position: 0 0; + } + + &.collapsed { + + } + } + + .drag-handle { + margin-left: 11px; + } + } + + h3 { + font-size: 19px; + font-weight: 700; + color: $blue; + } + + .section-name-span { + cursor: pointer; + @include transition(color .15s); + + &:hover { + color: $orange; + } + } + + .section-name-form { + margin-bottom: 15px; + } + + .section-name-edit { + input { + font-size: 16px; + } + + .save-button { + @include blue-button; + padding: 7px 20px 7px; + margin-right: 5px; + } + + .cancel-button { + @include white-button; + padding: 7px 20px 7px; + } + } + + h4 { + font-size: 12px; + color: #878e9d; + + strong { + font-weight: bold; + } + } + + .list-header { + @include linear-gradient(top, transparent, rgba(0, 0, 0, .1)); + background-color: #ced2db; + border-radius: 3px 3px 0 0; + } + + .subsection-list { + margin: 0 12px; + + > ol { + @include tree-view; + border-top-width: 0; + } + } + + &.new-section { + + header { + height: auto; + @include clearfix(); + } + + .expand-collapse-icon { + visibility: hidden; + } + + .item-details { + padding: 25px 0 0 0; + + .section-name { + float: none; + width: 100%; + } + } + } + } + + .toggle-button-sections { + display: none; + position: relative; + float: right; + margin-top: 10px; + + font-size: 13px; + color: $darkGrey; + + &.is-shown { + display: block; + } + + .ss-icon { + @include border-radius(20px); + position: relative; + top: -1px; + display: inline-block; + margin-right: 2px; + line-height: 5px; + font-size: 11px; + } + + .label { + display: inline-block; + } + } + + .new-section-name, + .new-subsection-name-input { + width: 515px; + } + + .new-section-name-save, + .new-subsection-name-save { + @include blue-button; + padding: 4px 20px 7px; + margin: 0 5px; + color: #fff !important; + } + + .new-section-name-cancel, + .new-subsection-name-cancel { + @include white-button; + padding: 4px 20px 7px; + color: #8891a1 !important; + } + + .dummy-calendar { + display: none; + position: absolute; + top: 55px; + left: 110px; + z-index: 9999; + border: 1px solid #3C3C3C; + @include box-shadow(0 1px 15px rgba(0, 0, 0, .2)); + } + + .preview { + background: url(../img/preview.jpg) center top no-repeat; + } + + .edit-subsection-publish-settings { + display: none; + position: fixed; + top: 100px; + left: 50%; + z-index: 99999; + width: 600px; + margin-left: -300px; + background: #fff; + text-align: center; + + .settings { + padding: 40px; + } + + h3 { + font-size: 34px; + font-weight: 300; + } + + .picker { + margin: 30px 0 65px; + } + + .description { + margin-top: 30px; + font-size: 14px; + line-height: 20px; + } + + strong { + font-weight: 700; + } + + .start-date, + .start-time { + font-size: 19px; + } + + .save-button { + @include blue-button; + margin-right: 10px; + } + + .cancel-button { + @include white-button; + } + + .save-button, + .cancel-button { + font-size: 16px; + } + } + + .collapse-all-button { + float: right; + margin-top: 10px; + font-size: 13px; + color: $darkGrey; + } + + // sort/drag and drop + .ui-droppable { + @include transition (padding 0.5s ease-in-out 0s); + min-height: 20px; + padding: 0; + + &.dropover { + padding: 15px 0; + } + } + + .ui-draggable-dragging { + @include box-shadow(0 1px 2px rgba(0, 0, 0, .3)); + border: 1px solid $darkGrey; + opacity : 0.2; + &:hover { + opacity : 1.0; + .section-item { + background: $yellow !important; + } + } + + // hiding unit button - temporary fix until this semantically corrected + .new-unit-item { + display: none; + } + } + + ol.ui-droppable .branch:first-child .section-item { + border-top: none; + } +} \ No newline at end of file diff --git a/cms/static/sass/_settings.scss b/cms/static/sass/views/_settings.scss similarity index 99% rename from cms/static/sass/_settings.scss rename to cms/static/sass/views/_settings.scss index a42ff80bc2..307ebad0a8 100644 --- a/cms/static/sass/_settings.scss +++ b/cms/static/sass/views/_settings.scss @@ -1,5 +1,6 @@ -// Studio - Course Settings +// studio - views - course settings // ==================== + body.course.settings { .content-primary, .content-supplementary { diff --git a/cms/static/sass/_static-pages.scss b/cms/static/sass/views/_static-pages.scss similarity index 72% rename from cms/static/sass/_static-pages.scss rename to cms/static/sass/views/_static-pages.scss index 138e817769..759b03cfc7 100644 --- a/cms/static/sass/_static-pages.scss +++ b/cms/static/sass/views/_static-pages.scss @@ -1,4 +1,8 @@ -.static-pages { +// studio - views - course static pages +// ==================== + +body.course.static-pages { + .new-static-page-button { @include grey-button; display: block; @@ -16,6 +20,51 @@ margin: 0 0 5px 0; } } + + .wrapper-component-editor { + z-index: 9999; + position: relative; + background: $lightBluishGrey2; + } + + .component-editor { + @include edit-box; + @include box-shadow(none); + display: none; + padding: 20px; + border-radius: 2px 2px 0 0; + + .metadata_edit { + margin-bottom: 20px; + font-size: 13px; + + li { + margin-bottom: 10px; + } + + label { + display: inline-block; + margin-right: 10px; + } + } + + h3 { + margin-bottom: 10px; + font-size: 18px; + font-weight: 700; + } + + h5 { + margin-bottom: 8px; + color: #fff; + font-weight: 700; + } + + .save-button { + margin-top: 10px; + margin: 15px 8px 0 0; + } + } } .component-editor { @@ -35,6 +84,7 @@ } .component { + position: relative; border: 1px solid $mediumGrey; border-top: none; @@ -56,10 +106,13 @@ } .drag-handle { + position: absolute; + display: block; top: 0; right: 0; z-index: 11; width: 35px; + height: 100%; border: none; background: url(../img/drag-handles.png) center no-repeat #fff; @@ -69,6 +122,7 @@ } .component-actions { + position: absolute; top: 26px; right: 44px; } diff --git a/cms/static/sass/views/_subsection.scss b/cms/static/sass/views/_subsection.scss new file mode 100644 index 0000000000..3c6bfa9f11 --- /dev/null +++ b/cms/static/sass/views/_subsection.scss @@ -0,0 +1,450 @@ +// studio - views - course subsection +// ==================== + +body.course.subsection { + + .unit-settings { + .window-contents { + padding: 10px 20px; + } + + .unit-actions { + border-bottom: none; + padding-bottom: 0; + } + + .published-alert { + display: none; + padding: 10px; + border: 1px solid #edbd3c; + border-radius: 3px; + background: #fbf6e1; + font-size: 14px; + line-height: 1.4; + + div { + margin-top: 15px; + } + } + + input[type="radio"] { + margin-right: 7px; + } + + .status { + font-size: 12px; + + strong { + font-weight: 700; + } + } + + .preview-button, .view-button { + @include white-button; + margin-bottom: 10px; + } + + .publish-button { + @include orange-button; + } + + .delete-button { + @include blue-button; + } + + .delete-draft { + display: inline-block; + } + + .delete-button, + .preview-button, + .publish-button, + .view-button { + font-size: 11px; + margin-top: 10px; + padding: 6px 15px 8px; + } + } + + .unit-history { + &.collapsed { + h4 { + border-bottom: none; + border-radius: 3px; + } + + .window-contents { + display: none; + } + } + + ol { + border: 1px solid #ced2db; + + li { + display: block; + padding: 6px 8px 8px 10px; + background: #edf1f5; + font-size: 12px; + + &:hover { + background: #fffcf1; + + .item-actions { + display: block; + } + } + + &.checked { + background: #d1dae3; + } + + .item-actions { + display: none; + } + + input[type="radio"] { + margin-right: 7px; + } + } + } + } + + .unit-location { + .url { + width: 100%; + margin-bottom: 10px; + @include box-shadow(none); + } + + .draft-tag, + .hidden-tag, + .private-tag, + .has-new-draft-tag { + font-size: 8px; + } + + .window-contents > ol { + @include tree-view; + + .section-item { + display: inline-block; + width: 100%; + font-size: 11px; + padding: 2px 8px 4px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + @include box-sizing(border-box); + } + + ol { + .section-item { + padding-left: 20px; + } + + .new-unit-item { + margin-left: 20px; + } + } + + ol ol { + .section-item { + padding-left: 34px; + } + + .new-unit-item { + margin: 0 0 10px 41px; + } + } + } + } + + .subsection-body { + padding: 32px 40px; + @include clearfix; + + > div { + margin-bottom: 40px; + } + + input { + font-size: 14px; + } + + .unit-subtitle { + display: block; + width: 100%; + } + + .sortable-unit-list { + ol { + @include tree-view; + } + } + + .policy-list { + input[disabled] { + border: none; + @include box-shadow(none); + } + + .policy-list-name { + margin-right: 5px; + margin-bottom: 10px; + } + + .policy-list-value { + width: 320px; + margin-right: 10px; + } + } + + .policy-list-element { + .save-button, + .cancel-button { + display: none; + } + + .edit-icon { + margin-right: 8px; + } + + &.editing, + &.new-policy-list-element { + .policy-list-name, + .policy-list-value { + border: 1px solid #b0b6c2; + @include linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, .3)); + background-color: #edf1f5; + @include box-shadow(0 1px 2px rgba(0, 0, 0, .1) inset); + } + } + } + + .new-policy-list-element { + padding: 10px 10px 0; + margin: 0 -10px 10px; + border-radius: 3px; + background: $mediumGrey; + + .save-button { + @include blue-button; + margin-bottom: 10px; + } + + .cancel-button { + @include white-button; + } + + .edit-icon { + display: none; + } + + .delete-icon { + display: none; + } + } + + .new-policy-item { + margin: 10px 0; + + .plus-icon-small { + position: relative; + top: -1px; + vertical-align: middle; + } + } + } + + .subsection-name-input { + label { + display: block; + } + + input { + width: 100%; + font-size: 20px; + } + } + + .scheduled-date-input, + .due-date-input { + @include clearfix; + + .date-input, + .time-input { + display: inline-block; + width: 100px; + } + + .inherits-check { + label { + font-size: 13px; + } + } + + .notice { + margin-top: 6px; + font-size: 11px; + color: #999; + } + } + + .due-date-input { + label { + display: inline-block !important; + margin-right: 10px; + } + + a { + font-size: 11px; + font-weight: bold; + text-transform: uppercase; + } + + .date-setter { + @include clearfix; + display: none; + } + + .remove-date { + display: block; + } + } + + .row.visibility { + label { + display: inline-block !important; + margin-right: 10px; + line-height: 21px; + } + + a { + display: inline-block; + height: 31px; + margin-right: 8px; + vertical-align: middle; + font-size: 11px; + font-weight: 700; + line-height: 31px; + text-transform: uppercase; + } + + .large-toggle { + width: 41px; + background: url(../img/large-toggles.png) no-repeat; + background-position: 0 -50px; + + .hidden { + background-position: 0 -5px; + } + } + } + + .gradable { + + label { + display: inline-block; + vertical-align: top; + } + + .gradable-status { + position: relative; + top: -4px; + display: inline-block; + margin-left: 10px; + width: 65%; + + .status-label { + margin: 0; + padding: 0; + background: transparent; + color: $blue; + border: none; + font-size: 11px; + font-weight: bold; + text-transform: uppercase; + } + + .menu-toggle { + z-index: 100; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 20px; + background: transparent; + + &:hover, &.is-active { + color: $blue; + } + } + + .menu { + z-index: 1; + position: absolute; + top: -12px; + left: -7px; + display: none; + width: 100%; + margin: 0; + padding: 8px 12px; + opacity: 0.0; + background: $white; + border: 1px solid $mediumGrey; + font-size: 12px; + @include border-radius(4px); + @include box-shadow(0 1px 2px rgba(0, 0, 0, .2)); + @include transition(opacity .15s); + + + li { + margin-bottom: 3px; + padding-bottom: 3px; + border-bottom: 1px solid $lightGrey; + + &:last-child { + margin-bottom: 0; + padding-bottom: 0; + border: none; + } + } + + a { + + &.is-selected { + font-weight: bold; + } + } + } + + // dropdown state + &.is-active { + + .menu { + z-index: 10000; + display: block; + opacity: 1.0; + } + + .menu-toggle { + z-index: 1000; + } + } + + // set state + &.is-set { + + .menu-toggle { + color: $blue; + } + + .status-label { + display: block; + color: $blue; + } + } + } + } +} \ No newline at end of file diff --git a/cms/static/sass/views/_unit.scss b/cms/static/sass/views/_unit.scss new file mode 100644 index 0000000000..e74690d9ec --- /dev/null +++ b/cms/static/sass/views/_unit.scss @@ -0,0 +1,681 @@ +// studio - views - unit +// ==================== + +body.course.unit { + + .unit .main-wrapper { + @include clearfix(); + margin: 40px; + } + + //Problem Selector tab menu requirements + .js .tabs .tab { + display: none; + } + //end problem selector reqs + + .main-column { + clear: both; + float: left; + width: 70%; + } + + .unit-body.published { + .components > li { + border: none; + + .rendered-component { + padding: 0 20px; + } + } + } + + .unit-body { + + .unit-name-input { + padding: 20px 40px; + + label { + display: block; + } + + input { + width: 100%; + font-size: 20px; + } + } + + .breadcrumbs { + border-radius: 3px 3px 0 0; + border-bottom: 1px solid #cbd1db; + @include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0) 70%); + background-color: #edf1f5; + @include box-shadow(0 1px 0 rgba(255, 255, 255, .7) inset); + @include clearfix; + + li { + float: left; + } + + a, + .current-page { + display: block; + padding: 15px 35px 15px 30px; + font-size: 14px; + background: url(../img/breadcrumb-arrow.png) no-repeat right center; + } + } + + h2 { + margin: 30px 40px 30px 0; + color: #646464; + font-size: 19px; + font-weight: 300; + letter-spacing: 1px; + text-transform: uppercase; + } + + .components { + + > li { + position: relative; + z-index: 10; + margin: 20px 40px; + + + + .title { + margin: 0 0 15px 0; + color: $mediumGrey; + + .value { + } + } + + &.new-component-item { + margin: 20px 0px; + border-top: 1px solid $mediumGrey; + box-shadow: 0 2px 1px rgba(182, 182, 182, 0.75) inset; + background-color: $lightGrey; + margin-bottom: 0px; + padding-bottom: 20px; + + .new-component-button { + display: block; + padding: 20px; + text-align: center; + color: #edf1f5; + } + + h5 { + margin: 20px 0px; + color: #fff; + font-weight: 600; + font-size: 18px; + } + + .rendered-component { + display: none; + background: #fff; + border-radius: 3px 3px 0 0; + } + + .new-component-type { + + a, + li { + display: inline-block; + } + + a { + border: 1px solid $mediumGrey; + width: 100px; + height: 100px; + color: #fff; + margin-right: 15px; + margin-bottom: 20px; + border-radius: 8px; + font-size: 15px; + line-height: 14px; + text-align: center; + @include box-shadow(0 1px 1px rgba(0, 0, 0, .2), 0 1px 0 rgba(255, 255, 255, .4) inset); + + .name { + position: absolute; + bottom: 5px; + left: 0; + width: 100%; + padding: 10px; + @include box-sizing(border-box); + color: #fff; + } + } + } + + .new-component-templates { + display: none; + margin: 20px 40px 20px 40px; + border-radius: 3px; + border: 1px solid $mediumGrey; + background-color: #fff; + @include box-shadow(0 1px 1px rgba(0, 0, 0, .2), 0 1px 0 rgba(255, 255, 255, .4) inset); + @include clearfix; + + .cancel-button { + margin: 20px 0px 10px 10px; + @include white-button; + } + + .problem-type-tabs { + display: none; + } + + // specific menu types + &.new-component-problem { + padding-bottom:10px; + + .ss-icon, .editor-indicator { + display: inline-block; + } + + .problem-type-tabs { + display: inline-block; + } + } + } + + .new-component-type, + .new-component-template { + @include clearfix; + + a { + position: relative; + border: 1px solid $darkGreen; + background: tint($green,20%); + color: #fff; + + &:hover { + background: $brightGreen; + } + } + } + + .problem-type-tabs { + list-style-type: none; + border-radius: 0; + width: 100%; + @include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0)); + background-color: $lightBluishGrey; + @include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset); + + li:first-child { + margin-left: 20px; + } + + li { + float:left; + display:inline-block; + text-align:center; + width: auto; + @include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0)); + background-color: tint($lightBluishGrey, 10%); + @include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset); + opacity:.8; + + &:hover { + opacity:1; + background-color: tint($lightBluishGrey, 20%); + } + + &.ui-state-active { + border: 0px; + @include active; + opacity:1; + } + } + + a{ + display: block; + padding: 15px 25px; + font-size: 15px; + line-height: 16px; + text-align: center; + color: #3c3c3c; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.3); + } + } + + .new-component-template { + + a { + background: #fff; + border: 0px; + color: #3c3c3c; + @include transition (none); + + &:hover { + background: tint($green,30%); + color: #fff; + @include transition(background-color .15s); + } + } + + li { + border:none; + border-bottom: 1px dashed $lightGrey; + color: #fff; + } + + li:first-child { + a { + border-top: 0px; + } + } + + li:nth-child(2) { + a { + border-radius: 0px; + } + } + + a { + @include clearfix(); + display: block; + padding: 7px 20px; + border-bottom: none; + font-weight: 500; + + .name { + float: left; + + .ss-icon { + @include transition(opacity .15s); + display: inline-block; + top: 1px; + margin-right: 5px; + opacity: 0.5; + width: 17; + height: 21px; + vertical-align: middle; + } + } + + .editor-indicator { + @include transition(opacity .15s); + float: right; + position: relative; + top: 3px; + font-size: 12px; + opacity: 0.3; + } + + .ss-icon, .editor-indicator { + display: none; + } + + &:hover { + color: #fff; + + .ss-icon { + opacity: 1.0; + } + + .editor-indicator { + opacity: 1.0; + } + } + } + + // specific editor types + .empty { + + a { + line-height: 1.4; + font-weight: 400; + background: #fff; + color: #3c3c3c; + + + &:hover { + background: tint($green,30%); + color: #fff; + } + } + } + } + + .new-component { + text-align: center; + + h5 { + color: $darkGreen; + } + + } + } + } + } + + .component { + border: 1px solid $lightBluishGrey2; + border-radius: 3px; + background: #fff; + @include transition(none); + + &:hover { + border-color: #6696d7; + + .drag-handle { + background-color: $blue; + border-color: $blue; + } + } + + &.editing { + border: 1px solid $lightBluishGrey2; + z-index: auto; + + .drag-handle, + .component-actions { + display: none; + } + } + + &.component-placeholder { + border-color: #6696d7; + } + + .drag-handle { + position: absolute; + display: block; + top: -1px; + right: -16px; + z-index: 10; + width: 15px; + height: 100%; + border-radius: 0 3px 3px 0; + border: 1px solid $lightBluishGrey2; + background: url(../img/white-drag-handles.png) center no-repeat $lightBluishGrey2; + cursor: move; + @include transition(none); + } + } + + .xmodule_display { + padding: 40px 20px 20px; + overflow-x: auto; + + h1 { + float: none; + margin-left: 0; + } + } + + .wrapper-component-editor { + z-index: 9999; + position: relative; + background: $lightBluishGrey2; + } + + .component-editor { + @include edit-box; + @include box-shadow(none); + display: none; + padding: 20px; + border-radius: 2px 2px 0 0; + + .metadata_edit { + margin-bottom: 20px; + font-size: 13px; + + li { + margin-bottom: 10px; + } + + label { + display: inline-block; + margin-right: 10px; + } + } + + h3 { + margin-bottom: 10px; + font-size: 18px; + font-weight: 700; + } + + h5 { + margin-bottom: 8px; + color: #fff; + font-weight: 700; + } + + .save-button { + margin-top: 10px; + margin: 15px 8px 0 0; + } + } + } + + .unit-settings { + .window-contents { + padding: 10px 20px; + } + + .unit-actions { + border-bottom: none; + padding-bottom: 0; + } + + .published-alert { + display: none; + padding: 10px; + border: 1px solid #edbd3c; + border-radius: 3px; + background: #fbf6e1; + font-size: 14px; + line-height: 1.4; + + div { + margin-top: 15px; + } + } + + input[type="radio"] { + margin-right: 7px; + } + + .status { + font-size: 12px; + + strong { + font-weight: 700; + } + } + + .preview-button, .view-button { + @include white-button; + margin-bottom: 10px; + } + + .publish-button { + @include orange-button; + } + + .delete-button { + @include blue-button; + } + + .delete-draft { + display: inline-block; + } + + .delete-button, + .preview-button, + .publish-button, + .view-button { + font-size: 11px; + margin-top: 10px; + padding: 6px 15px 8px; + } + } + + .unit-history { + &.collapsed { + h4 { + border-bottom: none; + border-radius: 3px; + } + + .window-contents { + display: none; + } + } + + ol { + border: 1px solid #ced2db; + + li { + display: block; + padding: 6px 8px 8px 10px; + background: #edf1f5; + font-size: 12px; + + &:hover { + background: #fffcf1; + + .item-actions { + display: block; + } + } + + &.checked { + background: #d1dae3; + } + + .item-actions { + display: none; + } + + input[type="radio"] { + margin-right: 7px; + } + } + } + } + + .unit-location { + .url { + width: 100%; + margin-bottom: 10px; + @include box-shadow(none); + } + + .draft-tag, + .hidden-tag, + .private-tag, + .has-new-draft-tag { + font-size: 8px; + } + + .window-contents > ol { + @include tree-view; + + .section-item { + display: inline-block; + width: 100%; + font-size: 11px; + padding: 2px 8px 4px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + @include box-sizing(border-box); + } + + ol { + .section-item { + padding-left: 20px; + } + + .new-unit-item { + margin-left: 20px; + } + } + + ol ol { + .section-item { + padding-left: 34px; + } + + .new-unit-item { + margin: 0 0 10px 41px; + } + } + } + } + + .edit-state-draft { + .visibility, + + .edit-draft-message, + .view-button { + display: none; + } + + .published-alert { + display: block; + } + } + + .edit-state-public { + .delete-draft, + .component-actions, + .new-component-item, + .editing-draft-alert, + .publish-draft-message, + .preview-button { + display: none; + } + + .published-alert { + display: block; + } + + .drag-handle { + display: none !important; + } + } + + .edit-state-private { + .delete-draft, + .publish-draft, + .editing-draft-alert, + .create-draft, + .view-button { + display: none; + } + } +} + +// editing units from courseware +body.unit { + + .component { + padding-top: 30px; + + .component-actions { + @include box-sizing(border-box); + position: absolute; + width: 100%; + padding: 15px; + top: 0; + left: 0; + border-bottom: 1px solid $lightBluishGrey2; + background: $lightGrey; + } + + &.editing { + padding-top: 0; + } + } +} \ No newline at end of file diff --git a/cms/static/sass/_course-info.scss b/cms/static/sass/views/_updates.scss similarity index 97% rename from cms/static/sass/_course-info.scss rename to cms/static/sass/views/_updates.scss index 5a2a5a9432..8d92c9d860 100644 --- a/cms/static/sass/_course-info.scss +++ b/cms/static/sass/views/_updates.scss @@ -1,4 +1,8 @@ -.course-info { +// studio - views - course updates +// ==================== + +body.course.updates { + h2 { margin-bottom: 24px; font-size: 22px; diff --git a/cms/static/sass/_users.scss b/cms/static/sass/views/_users.scss similarity index 94% rename from cms/static/sass/_users.scss rename to cms/static/sass/views/_users.scss index e107bdbb6d..ecaa319707 100644 --- a/cms/static/sass/_users.scss +++ b/cms/static/sass/views/_users.scss @@ -1,4 +1,8 @@ -.users { +// studio - views - course users +// ==================== + +body.course.users { + .new-user-form { display: none; padding: 15px 20px; diff --git a/cms/templates/404.html b/cms/templates/404.html new file mode 100644 index 0000000000..a45a223bad --- /dev/null +++ b/cms/templates/404.html @@ -0,0 +1,14 @@ +<%inherit file="base.html" /> +<%block name="title">Page Not Found + +<%block name="content"> + +
            +
            + +

            Page not found

            +

            The page that you were looking for was not found. Go back to the homepage or let us know about any pages that may have been moved at technical@edx.org.

            +
            +
            + + \ No newline at end of file diff --git a/cms/templates/500.html b/cms/templates/500.html new file mode 100644 index 0000000000..2645b0067b --- /dev/null +++ b/cms/templates/500.html @@ -0,0 +1,13 @@ +<%inherit file="base.html" /> +<%block name="title">Server Error + +<%block name="content"> + +
            +
            +

            Currently the edX servers are down

            +

            Our staff is currently working to get the site back up as soon as possible. Please email us at technical@edx.org to report any problems or downtime.

            +
            +
            + + \ No newline at end of file diff --git a/cms/urls.py b/cms/urls.py index d43b9bc44c..4d46325a68 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -43,7 +43,7 @@ urlpatterns = ('', url(r'^(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)/remove_user$', 'contentstore.views.remove_user', name='remove_user'), url(r'^(?P[^/]+)/(?P[^/]+)/info/(?P[^/]+)$', 'contentstore.views.course_info', name='course_info'), - url(r'^(?P[^/]+)/(?P[^/]+)/course_info/updates/(?P.*)$', 'contentstore.views.course_info_updates', name='course_info'), + url(r'^(?P[^/]+)/(?P[^/]+)/course_info/updates/(?P.*)$', 'contentstore.views.course_info_updates', name='course_info_json'), url(r'^(?P[^/]+)/(?P[^/]+)/settings-details/(?P[^/]+)$', 'contentstore.views.get_course_settings', name='course_settings'), url(r'^(?P[^/]+)/(?P[^/]+)/settings-grading/(?P[^/]+)$', 'contentstore.views.course_config_graders_page', name='course_settings'), url(r'^(?P[^/]+)/(?P[^/]+)/settings-details/(?P[^/]+)/section/(?P
            [^/]+).*$', 'contentstore.views.course_settings_updates', name='course_settings'), @@ -100,7 +100,13 @@ urlpatterns += ( ) if settings.ENABLE_JASMINE: - ## Jasmine + # # Jasmine urlpatterns = urlpatterns + (url(r'^_jasmine/', include('django_jasmine.urls')),) urlpatterns = patterns(*urlpatterns) + +# Custom error pages +handler404 = 'contentstore.views.render_404' +handler500 = 'contentstore.views.render_500' + + diff --git a/common/djangoapps/course_groups/cohorts.py b/common/djangoapps/course_groups/cohorts.py index c362ed4e89..7924012bfe 100644 --- a/common/djangoapps/course_groups/cohorts.py +++ b/common/djangoapps/course_groups/cohorts.py @@ -15,6 +15,24 @@ from .models import CourseUserGroup log = logging.getLogger(__name__) +# tl;dr: global state is bad. capa reseeds random every time a problem is loaded. Even +# if and when that's fixed, it's a good idea to have a local generator to avoid any other +# code that messes with the global random module. +_local_random = None + +def local_random(): + """ + Get the local random number generator. In a function so that we don't run + random.Random() at import time. + """ + # ironic, isn't it? + global _local_random + + if _local_random is None: + _local_random = random.Random() + + return _local_random + def is_course_cohorted(course_id): """ Given a course id, return a boolean for whether or not the course is @@ -129,13 +147,7 @@ def get_cohort(user, course_id): return None # Put user in a random group, creating it if needed - choice = random.randrange(0, n) - group_name = choices[choice] - - # Victor: we are seeing very strange behavior on prod, where almost all users - # end up in the same group. Log at INFO to try to figure out what's going on. - log.info("DEBUG: adding user {0} to cohort {1}. choice={2}".format( - user, group_name,choice)) + group_name = local_random().choice(choices) group, created = CourseUserGroup.objects.get_or_create( course_id=course_id, diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 54bdd77297..56b1293c2d 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -75,10 +75,15 @@ class UserProfile(models.Model): GENDER_CHOICES = (('m', 'Male'), ('f', 'Female'), ('o', 'Other')) gender = models.CharField(blank=True, null=True, max_length=6, db_index=True, choices=GENDER_CHOICES) - LEVEL_OF_EDUCATION_CHOICES = (('p_se', 'Doctorate in science or engineering'), - ('p_oth', 'Doctorate in another field'), + + # [03/21/2013] removed these, but leaving comment since there'll still be + # p_se and p_oth in the existing data in db. + # ('p_se', 'Doctorate in science or engineering'), + # ('p_oth', 'Doctorate in another field'), + LEVEL_OF_EDUCATION_CHOICES = (('p', 'Doctorate'), ('m', "Master's or professional degree"), ('b', "Bachelor's degree"), + ('a', "Associate's degree"), ('hs', "Secondary/high school"), ('jhs', "Junior secondary/junior high/middle school"), ('el', "Elementary/primary school"), diff --git a/common/djangoapps/student/tests/__init__.py b/common/djangoapps/student/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/djangoapps/student/tests/factories.py b/common/djangoapps/student/tests/factories.py new file mode 100644 index 0000000000..f74188725a --- /dev/null +++ b/common/djangoapps/student/tests/factories.py @@ -0,0 +1,59 @@ +from student.models import (User, UserProfile, Registration, + CourseEnrollmentAllowed, CourseEnrollment) +from django.contrib.auth.models import Group +from datetime import datetime +from factory import Factory, SubFactory +from uuid import uuid4 + + +class GroupFactory(Factory): + FACTORY_FOR = Group + + name = 'staff_MITx/999/Robot_Super_Course' + + +class UserProfileFactory(Factory): + FACTORY_FOR = UserProfile + + user = None + name = 'Robot Test' + level_of_education = None + gender = 'm' + mailing_address = None + goals = 'World domination' + + +class RegistrationFactory(Factory): + FACTORY_FOR = Registration + + user = None + activation_key = uuid4().hex + + +class UserFactory(Factory): + FACTORY_FOR = User + + username = 'robot' + email = 'robot+test@edx.org' + password = 'test' + first_name = 'Robot' + last_name = 'Test' + is_staff = False + is_active = True + is_superuser = False + last_login = datetime(2012, 1, 1) + date_joined = datetime(2011, 1, 1) + + +class CourseEnrollmentFactory(Factory): + FACTORY_FOR = CourseEnrollment + + user = SubFactory(UserFactory) + course_id = 'edX/toy/2012_Fall' + + +class CourseEnrollmentAllowedFactory(Factory): + FACTORY_FOR = CourseEnrollmentAllowed + + email = 'test@edx.org' + course_id = 'edX/test/2012_Fall' diff --git a/common/djangoapps/student/tests.py b/common/djangoapps/student/tests/tests.py similarity index 97% rename from common/djangoapps/student/tests.py rename to common/djangoapps/student/tests/tests.py index 6a2d75e3d8..4638da44b2 100644 --- a/common/djangoapps/student/tests.py +++ b/common/djangoapps/student/tests/tests.py @@ -9,8 +9,8 @@ import logging from django.test import TestCase from mock import Mock -from .models import unique_id_for_user -from .views import process_survey_link, _cert_info +from student.models import unique_id_for_user +from student.views import process_survey_link, _cert_info COURSE_1 = 'edX/toy/2012_Fall' COURSE_2 = 'edx/full/6.002_Spring_2012' diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 902ec82677..5dbaf5d2c2 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -311,7 +311,7 @@ def change_enrollment(request): course = course_from_id(course_id) except ItemNotFoundError: log.warning("User {0} tried to enroll in non-existent course {1}" - .format(user.username, enrollment.course_id)) + .format(user.username, course_id)) return {'success': False, 'error': 'The course requested does not exist.'} if not has_access(user, course, 'enroll'): diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 0881d86124..c8cc0c9e4b 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -1,7 +1,11 @@ from lettuce import before, after, world from splinter.browser import Browser from logging import getLogger -import time + +# Let the LMS and CMS do their one-time setup +# For example, setting up mongo caches +from lms import one_time_startup +from cms import one_time_startup logger = getLogger(__name__) logger.info("Loading the lettuce acceptance testing terrain file...") @@ -11,6 +15,9 @@ from django.core.management import call_command @before.harvest def initial_setup(server): + ''' + Launch the browser once before executing the tests + ''' # Launch the browser app (choose one of these below) world.browser = Browser('chrome') # world.browser = Browser('phantomjs') @@ -19,14 +26,18 @@ def initial_setup(server): @before.each_scenario def reset_data(scenario): - # Clean out the django test database defined in the - # envs/acceptance.py file: mitx_all/db/test_mitx.db + ''' + Clean out the django test database defined in the + envs/acceptance.py file: mitx_all/db/test_mitx.db + ''' logger.debug("Flushing the test database...") call_command('flush', interactive=False) @after.all def teardown_browser(total): - # Quit firefox + ''' + Quit the browser after executing the tests + ''' world.browser.quit() pass diff --git a/common/djangoapps/terrain/factories.py b/common/djangoapps/terrain/factories.py index a531f4fd26..768c51b25e 100644 --- a/common/djangoapps/terrain/factories.py +++ b/common/djangoapps/terrain/factories.py @@ -1,163 +1,64 @@ -from student.models import User, UserProfile, Registration -from django.contrib.auth.models import Group -from datetime import datetime -from factory import Factory -from xmodule.modulestore import Location -from xmodule.modulestore.django import modulestore -from time import gmtime -from uuid import uuid4 -from xmodule.timeparse import stringify_time -from xmodule.modulestore.inheritance import own_metadata +''' +Factories are defined in other modules and absorbed here into the +lettuce world so that they can be used by both unit tests +and integration / BDD tests. +''' +import student.tests.factories as sf +import xmodule.modulestore.tests.factories as xf +from lettuce import world -class GroupFactory(Factory): - FACTORY_FOR = Group - - name = 'staff_MITx/999/Robot_Super_Course' - - -class UserProfileFactory(Factory): - FACTORY_FOR = UserProfile - - user = None - name = 'Robot Test' - level_of_education = None - gender = 'm' - mailing_address = None - goals = 'World domination' - - -class RegistrationFactory(Factory): - FACTORY_FOR = Registration - - user = None - activation_key = uuid4().hex - - -class UserFactory(Factory): - FACTORY_FOR = User - - username = 'robot' - email = 'robot+test@edx.org' - password = 'test' - first_name = 'Robot' - last_name = 'Test' - is_staff = False - is_active = True - is_superuser = False - last_login = datetime(2012, 1, 1) - date_joined = datetime(2011, 1, 1) - - -def XMODULE_COURSE_CREATION(class_to_create, **kwargs): - return XModuleCourseFactory._create(class_to_create, **kwargs) - - -def XMODULE_ITEM_CREATION(class_to_create, **kwargs): - return XModuleItemFactory._create(class_to_create, **kwargs) - - -class XModuleCourseFactory(Factory): +@world.absorb +class UserFactory(sf.UserFactory): """ - Factory for XModule courses. + User account for lms / cms """ - - ABSTRACT_FACTORY = True - _creation_function = (XMODULE_COURSE_CREATION,) - - @classmethod - def _create(cls, target_class, *args, **kwargs): - - template = Location('i4x', 'edx', 'templates', 'course', 'Empty') - org = kwargs.get('org') - number = kwargs.get('number') - display_name = kwargs.get('display_name') - location = Location('i4x', org, number, - 'course', Location.clean(display_name)) - - store = modulestore('direct') - - # Write the data to the mongo datastore - new_course = store.clone_item(template, location) - - # This metadata code was copied from cms/djangoapps/contentstore/views.py - if display_name is not None: - new_course.display_name = display_name - - new_course.lms.start = gmtime() - new_course.tabs = [{"type": "courseware"}, - {"type": "course_info", "name": "Course Info"}, - {"type": "discussion", "name": "Discussion"}, - {"type": "wiki", "name": "Wiki"}, - {"type": "progress", "name": "Progress"}] - - # Update the data in the mongo datastore - store.update_metadata(new_course.location.url(), own_metadata(new_course)) - - return new_course - - -class Course: pass -class CourseFactory(XModuleCourseFactory): - FACTORY_FOR = Course - - template = 'i4x://edx/templates/course/Empty' - org = 'MITx' - number = '999' - display_name = 'Robot Super Course' - - -class XModuleItemFactory(Factory): +@world.absorb +class UserProfileFactory(sf.UserProfileFactory): """ - Factory for XModule items. + Demographics etc for the User """ - - ABSTRACT_FACTORY = True - _creation_function = (XMODULE_ITEM_CREATION,) - - @classmethod - def _create(cls, target_class, *args, **kwargs): - """ - kwargs must include parent_location, template. Can contain display_name - target_class is ignored - """ - - DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info'] - - parent_location = Location(kwargs.get('parent_location')) - template = Location(kwargs.get('template')) - display_name = kwargs.get('display_name') - - store = modulestore('direct') - - # This code was based off that in cms/djangoapps/contentstore/views.py - parent = store.get_item(parent_location) - dest_location = parent_location._replace(category=template.category, name=uuid4().hex) - - new_item = store.clone_item(template, dest_location) - - # replace the display name with an optional parameter passed in from the caller - if display_name is not None: - new_item.display_name = display_name - - store.update_metadata(new_item.location.url(), own_metadata(new_item)) - - if new_item.location.category not in DETACHED_CATEGORIES: - store.update_children(parent_location, parent.children + [new_item.location.url()]) - - return new_item - - -class Item: pass -class ItemFactory(XModuleItemFactory): - FACTORY_FOR = Item +@world.absorb +class RegistrationFactory(sf.RegistrationFactory): + """ + Activation key for registering the user account + """ + pass - parent_location = 'i4x://MITx/999/course/Robot_Super_Course' - template = 'i4x://edx/templates/chapter/Empty' - display_name = 'Section One' + +@world.absorb +class GroupFactory(sf.GroupFactory): + """ + Groups for user permissions for courses + """ + pass + + +@world.absorb +class CourseEnrollmentAllowedFactory(sf.CourseEnrollmentAllowed): + """ + Users allowed to enroll in the course outside of the usual window + """ + pass + + +@world.absorb +class CourseFactory(xf.CourseFactory): + """ + Courseware courses + """ + pass + + +@world.absorb +class ItemFactory(xf.ItemFactory): + """ + Everything included inside a course + """ + pass diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index 3dcef9b1ed..890d5fe450 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -1,7 +1,12 @@ from lettuce import world, step from .factories import * from lettuce.django import django_url +from django.conf import settings +from django.http import HttpRequest from django.contrib.auth.models import User +from django.contrib.auth import authenticate, login +from django.contrib.auth.middleware import AuthenticationMiddleware +from django.contrib.sessions.middleware import SessionMiddleware from student.models import CourseEnrollment from urllib import quote_plus from nose.tools import assert_equals @@ -9,6 +14,7 @@ from bs4 import BeautifulSoup import time import re import os.path +from selenium.common.exceptions import WebDriverException from logging import getLogger logger = getLogger(__name__) @@ -69,10 +75,15 @@ def the_page_title_should_be(step, title): assert_equals(world.browser.title, title) +@step(u'the page title should contain "([^"]*)"$') +def the_page_title_should_contain(step, title): + assert(title in world.browser.title) + + @step('I am a logged in user$') def i_am_logged_in_user(step): create_user('robot') - log_in('robot@edx.org', 'test') + log_in('robot', 'test') @step('I am not logged in$') @@ -80,18 +91,6 @@ def i_am_not_logged_in(step): world.browser.cookies.delete() -@step('I am registered for a course$') -def i_am_registered_for_a_course(step): - create_user('robot') - u = User.objects.get(username='robot') - CourseEnrollment.objects.get_or_create(user=u, course_id='MITx/6.002x/2012_Fall') - - -@step('I am registered for course "([^"]*)"$') -def i_am_registered_for_course_by_id(step, course_id): - register_by_course_id(course_id) - - @step('I am staff for course "([^"]*)"$') def i_am_staff_for_course_by_id(step, course_id): register_by_course_id(course_id, True) @@ -99,7 +98,7 @@ def i_am_staff_for_course_by_id(step, course_id): @step('I log in$') def i_log_in(step): - log_in('robot@edx.org', 'test') + log_in('robot', 'test') @step(u'I am an edX user$') @@ -108,6 +107,7 @@ def i_am_an_edx_user(step): #### helper functions + @world.absorb def scroll_to_bottom(): # Maximize the browser @@ -116,30 +116,55 @@ def scroll_to_bottom(): @world.absorb def create_user(uname): + + # If the user already exists, don't try to create it again + if len(User.objects.filter(username=uname)) > 0: + return + portal_user = UserFactory.build(username=uname, email=uname + '@edx.org') portal_user.set_password('test') portal_user.save() - registration = RegistrationFactory(user=portal_user) + registration = world.RegistrationFactory(user=portal_user) registration.register(portal_user) registration.activate() - user_profile = UserProfileFactory(user=portal_user) + user_profile = world.UserProfileFactory(user=portal_user) @world.absorb -def log_in(email, password): - world.browser.cookies.delete() - world.browser.visit(django_url('/')) - world.browser.is_element_present_by_css('header.global', 10) - world.browser.click_link_by_href('#login-modal') - login_form = world.browser.find_by_css('form#login_form') - login_form.find_by_name('email').fill(email) - login_form.find_by_name('password').fill(password) - login_form.find_by_name('submit').click() +def log_in(username, password): + ''' + Log the user in programatically + ''' - # wait for the page to redraw - assert world.browser.is_element_present_by_css('.content-wrapper', 10) + # Authenticate the user + user = authenticate(username=username, password=password) + assert(user is not None and user.is_active) + + # Send a fake HttpRequest to log the user in + # We need to process the request using + # Session middleware and Authentication middleware + # to ensure that session state can be stored + request = HttpRequest() + SessionMiddleware().process_request(request) + AuthenticationMiddleware().process_request(request) + login(request, user) + + # Save the session + request.session.save() + + # Retrieve the sessionid and add it to the browser's cookies + cookie_dict = {settings.SESSION_COOKIE_NAME: request.session.session_key} + try: + world.browser.cookies.add(cookie_dict) + + # WebDriver has an issue where we cannot set cookies + # before we make a GET request, so if we get an error, + # we load the '/' page and try again + except: + world.browser.visit(django_url('/')) + world.browser.cookies.add(cookie_dict) @world.absorb @@ -196,6 +221,7 @@ def save_the_course_content(path='/tmp'): u = world.browser.url section_url = u[u.find('courseware/') + 11:] + if not os.path.exists(path): os.makedirs(path) @@ -203,3 +229,15 @@ def save_the_course_content(path='/tmp'): f = open('%s/%s' % (path, filename), 'w') f.write(output) f.close + +@world.absorb +def css_click(css_selector): + try: + world.browser.find_by_css(css_selector).click() + + except WebDriverException: + # Occassionally, MathJax or other JavaScript can cover up + # an element temporarily. + # If this happens, wait a second, then try again + time.sleep(1) + world.browser.find_by_css(css_selector).click() diff --git a/common/lib/capa/capa/tests/response_xml_factory.py b/common/lib/capa/capa/tests/response_xml_factory.py index 7aa299d20d..aa401b70cd 100644 --- a/common/lib/capa/capa/tests/response_xml_factory.py +++ b/common/lib/capa/capa/tests/response_xml_factory.py @@ -1,6 +1,7 @@ from lxml import etree from abc import ABCMeta, abstractmethod + class ResponseXMLFactory(object): """ Abstract base class for capa response XML factories. Subclasses override create_response_element and @@ -13,7 +14,7 @@ class ResponseXMLFactory(object): """ Subclasses override to return an etree element representing the capa response XML (e.g. ). - + The tree should NOT contain any input elements (such as ) as these will be added later.""" return None @@ -25,7 +26,7 @@ class ResponseXMLFactory(object): return None def build_xml(self, **kwargs): - """ Construct an XML string for a capa response + """ Construct an XML string for a capa response based on **kwargs. **kwargs is a dictionary that will be passed @@ -37,7 +38,7 @@ class ResponseXMLFactory(object): *question_text*: The text of the question to display, wrapped in

            tags. - + *explanation_text*: The detailed explanation that will be shown if the user answers incorrectly. @@ -75,7 +76,7 @@ class ResponseXMLFactory(object): for i in range(0, int(num_responses)): response_element = self.create_response_element(**kwargs) root.append(response_element) - + # Add input elements for j in range(0, int(num_inputs)): input_element = self.create_input_element(**kwargs) @@ -135,7 +136,7 @@ class ResponseXMLFactory(object): # Names of group elements group_element_names = {'checkbox': 'checkboxgroup', 'radio': 'radiogroup', - 'multiple': 'choicegroup' } + 'multiple': 'choicegroup'} # Retrieve **kwargs choices = kwargs.get('choices', [True]) @@ -151,13 +152,11 @@ class ResponseXMLFactory(object): choice_element = etree.SubElement(group_element, "choice") choice_element.set("correct", "true" if correct_val else "false") - # Add some text describing the choice - etree.SubElement(choice_element, "startouttext") - etree.text = "Choice description" - etree.SubElement(choice_element, "endouttext") - # Add a name identifying the choice, if one exists + # For simplicity, we use the same string as both the + # name attribute and the text of the element if name: + choice_element.text = str(name) choice_element.set("name", str(name)) return group_element @@ -217,7 +216,7 @@ class CustomResponseXMLFactory(ResponseXMLFactory): *answer*: Inline script that calculates the answer """ - + # Retrieve **kwargs cfn = kwargs.get('cfn', None) expect = kwargs.get('expect', None) @@ -247,7 +246,7 @@ class SchematicResponseXMLFactory(ResponseXMLFactory): def create_response_element(self, **kwargs): """ Create the XML element. - + Uses *kwargs*: *answer*: The Python script used to evaluate the answer. @@ -274,6 +273,7 @@ class SchematicResponseXMLFactory(ResponseXMLFactory): For testing, we create a bare-bones version of .""" return etree.Element("schematic") + class CodeResponseXMLFactory(ResponseXMLFactory): """ Factory for creating XML trees """ @@ -286,9 +286,9 @@ class CodeResponseXMLFactory(ResponseXMLFactory): def create_response_element(self, **kwargs): """ Create a XML element: - + Uses **kwargs: - + *initial_display*: The code that initially appears in the textbox [DEFAULT: "Enter code here"] *answer_display*: The answer to display to the student @@ -328,6 +328,7 @@ class CodeResponseXMLFactory(ResponseXMLFactory): # return None here return None + class ChoiceResponseXMLFactory(ResponseXMLFactory): """ Factory for creating XML trees """ @@ -356,13 +357,13 @@ class FormulaResponseXMLFactory(ResponseXMLFactory): *num_samples*: The number of times to sample the student's answer to numerically compare it to the correct answer. - + *tolerance*: The tolerance within which answers will be accepted - [DEFAULT: 0.01] + [DEFAULT: 0.01] *answer*: The answer to the problem. Can be a formula string - or a Python variable defined in a script - (e.g. "$calculated_answer" for a Python variable + or a Python variable defined in a script + (e.g. "$calculated_answer" for a Python variable called calculated_answer) [REQUIRED] @@ -387,7 +388,7 @@ class FormulaResponseXMLFactory(ResponseXMLFactory): # Set the sample information sample_str = self._sample_str(sample_dict, num_samples, tolerance) response_element.set("samples", sample_str) - + # Set the tolerance responseparam_element = etree.SubElement(response_element, "responseparam") @@ -408,7 +409,7 @@ class FormulaResponseXMLFactory(ResponseXMLFactory): # We could sample a different range, but for simplicity, # we use the same sample string for the hints - # that we used previously. + # that we used previously. formulahint_element.set("samples", sample_str) formulahint_element.set("answer", str(hint_prompt)) @@ -436,10 +437,11 @@ class FormulaResponseXMLFactory(ResponseXMLFactory): high_range_vals = [str(f[1]) for f in sample_dict.values()] sample_str = (",".join(sample_dict.keys()) + "@" + ",".join(low_range_vals) + ":" + - ",".join(high_range_vals) + + ",".join(high_range_vals) + "#" + str(num_samples)) return sample_str + class ImageResponseXMLFactory(ResponseXMLFactory): """ Factory for producing XML """ @@ -450,9 +452,9 @@ class ImageResponseXMLFactory(ResponseXMLFactory): def create_input_element(self, **kwargs): """ Create the element. - + Uses **kwargs: - + *src*: URL for the image file [DEFAULT: "/static/image.jpg"] *width*: Width of the image [DEFAULT: 100] @@ -490,7 +492,7 @@ class ImageResponseXMLFactory(ResponseXMLFactory): input_element.set("src", str(src)) input_element.set("width", str(width)) input_element.set("height", str(height)) - + if rectangle: input_element.set("rectangle", rectangle) @@ -499,6 +501,7 @@ class ImageResponseXMLFactory(ResponseXMLFactory): return input_element + class JavascriptResponseXMLFactory(ResponseXMLFactory): """ Factory for producing XML """ @@ -522,7 +525,7 @@ class JavascriptResponseXMLFactory(ResponseXMLFactory): # Both display_src and display_class given, # or neither given - assert((display_src and display_class) or + assert((display_src and display_class) or (not display_src and not display_class)) # Create the element @@ -552,6 +555,7 @@ class JavascriptResponseXMLFactory(ResponseXMLFactory): """ Create the element """ return etree.Element("javascriptinput") + class MultipleChoiceResponseXMLFactory(ResponseXMLFactory): """ Factory for producing XML """ @@ -564,6 +568,7 @@ class MultipleChoiceResponseXMLFactory(ResponseXMLFactory): kwargs['choice_type'] = 'multiple' return ResponseXMLFactory.choicegroup_input_xml(**kwargs) + class TrueFalseResponseXMLFactory(ResponseXMLFactory): """ Factory for producing XML """ @@ -576,6 +581,7 @@ class TrueFalseResponseXMLFactory(ResponseXMLFactory): kwargs['choice_type'] = 'multiple' return ResponseXMLFactory.choicegroup_input_xml(**kwargs) + class OptionResponseXMLFactory(ResponseXMLFactory): """ Factory for producing XML""" @@ -620,7 +626,7 @@ class StringResponseXMLFactory(ResponseXMLFactory): def create_response_element(self, **kwargs): """ Create a XML element. - + Uses **kwargs: *answer*: The correct answer (a string) [REQUIRED] @@ -642,7 +648,7 @@ class StringResponseXMLFactory(ResponseXMLFactory): # Create the element response_element = etree.Element("stringresponse") - # Set the answer attribute + # Set the answer attribute response_element.set("answer", str(answer)) # Set the case sensitivity @@ -667,6 +673,7 @@ class StringResponseXMLFactory(ResponseXMLFactory): def create_input_element(self, **kwargs): return ResponseXMLFactory.textline_input_xml(**kwargs) + class AnnotationResponseXMLFactory(ResponseXMLFactory): """ Factory for creating XML trees """ def create_response_element(self, **kwargs): @@ -679,17 +686,17 @@ class AnnotationResponseXMLFactory(ResponseXMLFactory): input_element = etree.Element("annotationinput") text_children = [ - {'tag': 'title', 'text': kwargs.get('title', 'super cool annotation') }, - {'tag': 'text', 'text': kwargs.get('text', 'texty text') }, - {'tag': 'comment', 'text':kwargs.get('comment', 'blah blah erudite comment blah blah') }, - {'tag': 'comment_prompt', 'text': kwargs.get('comment_prompt', 'type a commentary below') }, - {'tag': 'tag_prompt', 'text': kwargs.get('tag_prompt', 'select one tag') } + {'tag': 'title', 'text': kwargs.get('title', 'super cool annotation')}, + {'tag': 'text', 'text': kwargs.get('text', 'texty text')}, + {'tag': 'comment', 'text':kwargs.get('comment', 'blah blah erudite comment blah blah')}, + {'tag': 'comment_prompt', 'text': kwargs.get('comment_prompt', 'type a commentary below')}, + {'tag': 'tag_prompt', 'text': kwargs.get('tag_prompt', 'select one tag')} ] for child in text_children: etree.SubElement(input_element, child['tag']).text = child['text'] - default_options = [('green', 'correct'),('eggs', 'incorrect'),('ham', 'partially-correct')] + default_options = [('green', 'correct'),('eggs', 'incorrect'), ('ham', 'partially-correct')] options = kwargs.get('options', default_options) options_element = etree.SubElement(input_element, 'options') @@ -698,4 +705,3 @@ class AnnotationResponseXMLFactory(ResponseXMLFactory): option_element.text = description return input_element - diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index f05f419a03..48fbfcced1 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -8,41 +8,66 @@ from xmodule.raw_module import RawDescriptor from .x_module import XModule from xblock.core import Integer, Scope, BlockScope, ModelType, String, Boolean, Object, Float, List from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor +from collections import namedtuple log = logging.getLogger("mitx.courseware") - V1_SETTINGS_ATTRIBUTES = ["display_name", "attempts", "is_graded", "accept_file_upload", - "skip_spelling_checks", "due", "graceperiod", "max_score"] + "skip_spelling_checks", "due", "graceperiod", "max_score"] V1_STUDENT_ATTRIBUTES = ["current_task_number", "task_states", "state", - "student_attempts", "ready_to_reset"] + "student_attempts", "ready_to_reset"] V1_ATTRIBUTES = V1_SETTINGS_ATTRIBUTES + V1_STUDENT_ATTRIBUTES -VERSION_TUPLES = ( - ('1', CombinedOpenEndedV1Descriptor, CombinedOpenEndedV1Module, V1_SETTINGS_ATTRIBUTES, V1_STUDENT_ATTRIBUTES), -) +VersionTuple = namedtuple('VersionTuple', ['descriptor', 'module', 'settings_attributes', 'student_attributes']) +VERSION_TUPLES = { + 1: VersionTuple(CombinedOpenEndedV1Descriptor, CombinedOpenEndedV1Module, V1_SETTINGS_ATTRIBUTES, + V1_STUDENT_ATTRIBUTES), +} DEFAULT_VERSION = 1 -DEFAULT_VERSION = str(DEFAULT_VERSION) + + +class VersionInteger(Integer): + """ + A model type that converts from strings to integers when reading from json. + Also does error checking to see if version is correct or not. + """ + + def from_json(self, value): + try: + value = int(value) + if value not in VERSION_TUPLES: + version_error_string = "Could not find version {0}, using version {1} instead" + log.error(version_error_string.format(value, DEFAULT_VERSION)) + value = DEFAULT_VERSION + except: + value = DEFAULT_VERSION + return value class CombinedOpenEndedFields(object): display_name = String(help="Display name for this module", default="Open Ended Grading", scope=Scope.settings) current_task_number = Integer(help="Current task that the student is on.", default=0, scope=Scope.student_state) task_states = List(help="List of state dictionaries of each task within this module.", scope=Scope.student_state) - state = String(help="Which step within the current task that the student is on.", default="initial", scope=Scope.student_state) - student_attempts = Integer(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.student_state) - ready_to_reset = Boolean(help="If the problem is ready to be reset or not.", default=False, scope=Scope.student_state) + state = String(help="Which step within the current task that the student is on.", default="initial", + scope=Scope.student_state) + student_attempts = Integer(help="Number of attempts taken by the student on this problem", default=0, + scope=Scope.student_state) + ready_to_reset = Boolean(help="If the problem is ready to be reset or not.", default=False, + scope=Scope.student_state) attempts = Integer(help="Maximum number of attempts that a student is allowed.", default=1, scope=Scope.settings) - is_graded = Boolean(help="Whether or not the problem is graded.", default=False, scope=Scope.settings) - accept_file_upload = Boolean(help="Whether or not the problem accepts file uploads.", default=False, scope=Scope.settings) - skip_spelling_checks = Boolean(help="Whether or not to skip initial spelling checks.", default=True, scope=Scope.settings) + is_graded = Boolean(help="Whether or not the problem is graded.", default=False, scope=Scope.settings) + accept_file_upload = Boolean(help="Whether or not the problem accepts file uploads.", default=False, + scope=Scope.settings) + skip_spelling_checks = Boolean(help="Whether or not to skip initial spelling checks.", default=True, + scope=Scope.settings) due = String(help="Date that this problem is due by", default=None, scope=Scope.settings) - graceperiod = String(help="Amount of time after the due date that submissions will be accepted", default=None, scope=Scope.settings) + graceperiod = String(help="Amount of time after the due date that submissions will be accepted", default=None, + scope=Scope.settings) max_score = Integer(help="Maximum score for the problem.", default=1, scope=Scope.settings) - version = Integer(help="Current version number", default=DEFAULT_VERSION, scope=Scope.settings) + version = VersionInteger(help="Current version number", default=DEFAULT_VERSION, scope=Scope.settings) data = String(help="XML data for the problem", scope=Scope.content) @@ -130,23 +155,10 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule): if self.task_states is None: self.task_states = [] - versions = [i[0] for i in VERSION_TUPLES] - descriptors = [i[1] for i in VERSION_TUPLES] - modules = [i[2] for i in VERSION_TUPLES] - settings_attributes = [i[3] for i in VERSION_TUPLES] - student_attributes = [i[4] for i in VERSION_TUPLES] - version_error_string = "Could not find version {0}, using version {1} instead" + version_tuple = VERSION_TUPLES[self.version] - try: - version_index = versions.index(self.version) - except: - #This is a dev_facing_error - log.error(version_error_string.format(self.version, DEFAULT_VERSION)) - self.version = DEFAULT_VERSION - version_index = versions.index(self.version) - - self.student_attributes = student_attributes[version_index] - self.settings_attributes = settings_attributes[version_index] + self.student_attributes = version_tuple.student_attributes + self.settings_attributes = version_tuple.settings_attributes attributes = self.student_attributes + self.settings_attributes @@ -154,10 +166,11 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule): 'rewrite_content_links': self.rewrite_content_links, } instance_state = {k: getattr(self, k) for k in attributes} - self.child_descriptor = descriptors[version_index](self.system) - self.child_definition = descriptors[version_index].definition_from_xml(etree.fromstring(self.data), self.system) - self.child_module = modules[version_index](self.system, location, self.child_definition, self.child_descriptor, - instance_state=instance_state, static_data=static_data, attributes=attributes) + self.child_descriptor = version_tuple.descriptor(self.system) + self.child_definition = version_tuple.descriptor.definition_from_xml(etree.fromstring(self.data), self.system) + self.child_module = version_tuple.module(self.system, location, self.child_definition, self.child_descriptor, + instance_state=instance_state, static_data=static_data, + attributes=attributes) self.save_instance_data() def get_html(self): diff --git a/common/lib/xmodule/xmodule/css/poll/display.scss b/common/lib/xmodule/xmodule/css/poll/display.scss index cfc03bcf91..82c018a3a0 100644 --- a/common/lib/xmodule/xmodule/css/poll/display.scss +++ b/common/lib/xmodule/xmodule/css/poll/display.scss @@ -131,6 +131,7 @@ section.poll_question { box-shadow: rgb(97, 184, 225) 0px 1px 0px 0px inset; color: rgb(255, 255, 255); text-shadow: rgb(7, 103, 148) 0px 1px 0px; + background-image: none; } .text { diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py index c5e5bbfdf8..f6fa98fc28 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -8,7 +8,7 @@ from collections import namedtuple from fs.osfs import OSFS from itertools import repeat from path import path -from datetime import datetime, timedelta +from datetime import datetime from importlib import import_module from xmodule.errortracker import null_error_tracker, exc_info_to_str @@ -246,6 +246,7 @@ class MongoModuleStore(ModuleStoreBase): self.fs_root = path(fs_root) self.error_tracker = error_tracker self.render_template = render_template + self.ignore_write_events_on_courses = [] def get_metadata_inheritance_tree(self, location): ''' @@ -303,6 +304,7 @@ class MongoModuleStore(ModuleStoreBase): # this is likely a leaf node, so let's record what metadata we need to inherit metadata_to_inherit[child] = my_metadata + if root is not None: _compute_inherited_metadata(root) @@ -329,8 +331,13 @@ class MongoModuleStore(ModuleStoreBase): return tree + def refresh_cached_metadata_inheritance_tree(self, location): + pseudo_course_id = '/'.join([location.org, location.course]) + if pseudo_course_id not in self.ignore_write_events_on_courses: + self.get_cached_metadata_inheritance_tree(location, force_refresh = True) + def clear_cached_metadata_inheritance_tree(self, location): - key_name = '{0}/{1}'.format(location.org, location.course) + key_name = '{0}/{1}'.format(location.org, location.course) if self.metadata_inheritance_cache is not None: self.metadata_inheritance_cache.delete(key_name) @@ -375,7 +382,7 @@ class MongoModuleStore(ModuleStoreBase): return data - def _load_item(self, item, data_cache): + def _load_item(self, item, data_cache, should_apply_metadata_inheritence=True): """ Load an XModuleDescriptor from item, using the children stored in data_cache """ @@ -389,9 +396,7 @@ class MongoModuleStore(ModuleStoreBase): metadata_inheritance_tree = None - # if we are loading a course object, there is no parent to inherit the metadata from - # so don't bother getting it - if item['location']['category'] != 'course': + if should_apply_metadata_inheritence: metadata_inheritance_tree = self.get_cached_metadata_inheritance_tree(Location(item['location'])) # TODO (cdodge): When the 'split module store' work has been completed, we should remove @@ -414,7 +419,10 @@ class MongoModuleStore(ModuleStoreBase): """ data_cache = self._cache_children(items, depth) - return [self._load_item(item, data_cache) for item in items] + # if we are loading a course object, if we're not prefetching children (depth != 0) then don't + # bother with the metadata inheritence + return [self._load_item(item, data_cache, + should_apply_metadata_inheritence=(item['location']['category'] != 'course' or depth != 0)) for item in items] def get_courses(self): ''' @@ -497,7 +505,12 @@ class MongoModuleStore(ModuleStoreBase): try: source_item = self.collection.find_one(location_to_query(source)) source_item['_id'] = Location(location).dict() - self.collection.insert(source_item) + self.collection.insert( + source_item, + # Must include this to avoid the django debug toolbar (which defines the deprecated "safe=False") + # from overriding our default value set in the init method. + safe=self.collection.safe + ) item = self._load_items([source_item])[0] # VS[compat] cdodge: This is a hack because static_tabs also have references from the course module, so @@ -519,7 +532,7 @@ class MongoModuleStore(ModuleStoreBase): raise DuplicateItemError(location) # recompute (and update) the metadata inheritance tree which is cached - self.get_cached_metadata_inheritance_tree(Location(location), force_refresh = True) + self.refresh_cached_metadata_inheritance_tree(Location(location)) def get_course_for_item(self, location, depth=0): ''' @@ -560,6 +573,9 @@ class MongoModuleStore(ModuleStoreBase): {'$set': update}, multi=False, upsert=True, + # Must include this to avoid the django debug toolbar (which defines the deprecated "safe=False") + # from overriding our default value set in the init method. + safe=self.collection.safe ) if result['n'] == 0: raise ItemNotFoundError(location) @@ -586,7 +602,7 @@ class MongoModuleStore(ModuleStoreBase): self._update_single_item(location, {'definition.children': children}) # recompute (and update) the metadata inheritance tree which is cached - self.get_cached_metadata_inheritance_tree(Location(location), force_refresh = True) + self.refresh_cached_metadata_inheritance_tree(Location(location)) def update_metadata(self, location, metadata): """ @@ -612,7 +628,7 @@ class MongoModuleStore(ModuleStoreBase): self._update_single_item(location, {'metadata': metadata}) # recompute (and update) the metadata inheritance tree which is cached - self.get_cached_metadata_inheritance_tree(loc, force_refresh = True) + self.refresh_cached_metadata_inheritance_tree(loc) def delete_item(self, location): """ @@ -630,10 +646,12 @@ class MongoModuleStore(ModuleStoreBase): course.tabs = [tab for tab in existing_tabs if tab.get('url_slug') != location.name] self.update_metadata(course.location, own_metadata(course)) - self.collection.remove({'_id': Location(location).dict()}) + self.collection.remove({'_id': Location(location).dict()}, + # Must include this to avoid the django debug toolbar (which defines the deprecated "safe=False") + # from overriding our default value set in the init method. + safe=self.collection.safe) # recompute (and update) the metadata inheritance tree which is cached - self.get_cached_metadata_inheritance_tree(Location(location), force_refresh = True) - + self.refresh_cached_metadata_inheritance_tree(Location(location)) def get_parent_locations(self, location, course_id): '''Find all locations that are the parents of this location in this diff --git a/common/lib/xmodule/xmodule/modulestore/tests/factories.py b/common/lib/xmodule/xmodule/modulestore/tests/factories.py index b842ffe9dd..1a82e1b708 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/factories.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/factories.py @@ -25,8 +25,7 @@ class XModuleCourseFactory(Factory): @classmethod def _create(cls, target_class, *args, **kwargs): - # This logic was taken from the create_new_course method in - # cms/djangoapps/contentstore/views.py + template = Location('i4x', 'edx', 'templates', 'course', 'Empty') org = kwargs.get('org') number = kwargs.get('number') @@ -43,8 +42,7 @@ class XModuleCourseFactory(Factory): if display_name is not None: new_course.display_name = display_name - new_course.start = gmtime() - + new_course.lms.start = gmtime() new_course.tabs = [{"type": "courseware"}, {"type": "course_info", "name": "Course Info"}, {"type": "discussion", "name": "Discussion"}, @@ -81,21 +79,41 @@ class XModuleItemFactory(Factory): @classmethod def _create(cls, target_class, *args, **kwargs): """ - kwargs must include parent_location, template. Can contain display_name - target_class is ignored + Uses *kwargs*: + + *parent_location* (required): the location of the parent module + (e.g. the parent course or section) + + *template* (required): the template to create the item from + (e.g. i4x://templates/section/Empty) + + *data* (optional): the data for the item + (e.g. XML problem definition for a problem item) + + *display_name* (optional): the display name of the item + + *metadata* (optional): dictionary of metadata attributes + + *target_class* is ignored """ DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info'] parent_location = Location(kwargs.get('parent_location')) template = Location(kwargs.get('template')) + data = kwargs.get('data') display_name = kwargs.get('display_name') + metadata = kwargs.get('metadata', {}) store = modulestore('direct') # This code was based off that in cms/djangoapps/contentstore/views.py parent = store.get_item(parent_location) - dest_location = parent_location._replace(category=template.category, name=uuid4().hex) + + # If a display name is set, use that + dest_name = display_name.replace(" ", "_") if display_name is not None else uuid4().hex + dest_location = parent_location._replace(category=template.category, + name=dest_name) new_item = store.clone_item(template, dest_location) @@ -103,7 +121,14 @@ class XModuleItemFactory(Factory): if display_name is not None: new_item.display_name = display_name - store.update_metadata(new_item.location.url(), own_metadata(new_item)) + # Add additional metadata or override current metadata + item_metadata = own_metadata(new_item) + item_metadata.update(metadata) + store.update_metadata(new_item.location.url(), item_metadata) + + # replace the data with the optional *data* parameter + if data is not None: + store.update_item(new_item.location, data) if new_item.location.category not in DETACHED_CATEGORIES: store.update_children(parent_location, parent.children + [new_item.location.url()]) diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index fa232596f2..6a4ce5131b 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -4,6 +4,8 @@ import mimetypes from lxml.html import rewrite_links as lxml_rewrite_links from path import path +from xblock.core import Scope + from .xml import XMLModuleStore from .exceptions import DuplicateItemError from xmodule.modulestore import Location @@ -201,100 +203,127 @@ def import_from_xml(store, data_dir, course_dirs=None, course_items = [] for course_id in module_store.modules.keys(): - course_data_path = None - course_location = None + if target_location_namespace is not None: + pseudo_course_id = '/'.join([target_location_namespace.org, target_location_namespace.course]) + else: + course_id_components = course_id.split('/') + pseudo_course_id = '/'.join([course_id_components[0], course_id_components[1]]) - if verbose: - log.debug("Scanning {0} for course module...".format(course_id)) + try: + # turn off all write signalling while importing as this is a high volume operation + if pseudo_course_id not in store.ignore_write_events_on_courses: + store.ignore_write_events_on_courses.append(pseudo_course_id) - # Quick scan to get course module as we need some info from there. Also we need to make sure that the - # course module is committed first into the store - for module in module_store.modules[course_id].itervalues(): - if module.category == 'course': - course_data_path = path(data_dir) / module.data_dir - course_location = module.location - - module = remap_namespace(module, target_location_namespace) - - # cdodge: more hacks (what else). Seems like we have a problem when importing a course (like 6.002) which - # does not have any tabs defined in the policy file. The import goes fine and then displays fine in LMS, - # but if someone tries to add a new tab in the CMS, then the LMS barfs because it expects that - - # if there is *any* tabs - then there at least needs to be some predefined ones - if module.tabs is None or len(module.tabs) == 0: - module.tabs = [{"type": "courseware"}, - {"type": "course_info", "name": "Course Info"}, - {"type": "discussion", "name": "Discussion"}, - {"type": "wiki", "name": "Wiki"}] # note, add 'progress' when we can support it on Edge - - - if hasattr(module, 'data'): - store.update_item(module.location, module.data) - store.update_children(module.location, module.children) - store.update_metadata(module.location, dict(own_metadata(module))) - - # a bit of a hack, but typically the "course image" which is shown on marketing pages is hard coded to /images/course_image.jpg - # so let's make sure we import in case there are no other references to it in the modules - verify_content_links(module, course_data_path, static_content_store, '/static/images/course_image.jpg') - - course_items.append(module) - - - # then import all the static content - if static_content_store is not None: - _namespace_rename = target_location_namespace if target_location_namespace is not None else course_location - - # first pass to find everything in /static/ - import_static_content(module_store.modules[course_id], course_location, course_data_path, static_content_store, - _namespace_rename, subpath='static', verbose=verbose) - - # finally loop through all the modules - for module in module_store.modules[course_id].itervalues(): - - if module.category == 'course': - # we've already saved the course module up at the top of the loop - # so just skip over it in the inner loop - continue - - # remap module to the new namespace - if target_location_namespace is not None: - module = remap_namespace(module, target_location_namespace) + course_data_path = None + course_location = None if verbose: - log.debug('importing module location {0}'.format(module.location)) + log.debug("Scanning {0} for course module...".format(course_id)) - if hasattr(module, 'data'): - module_data = module.data + # Quick scan to get course module as we need some info from there. Also we need to make sure that the + # course module is committed first into the store + for module in module_store.modules[course_id].itervalues(): + if module.category == 'course': + course_data_path = path(data_dir) / module.data_dir + course_location = module.location - # cdodge: now go through any link references to '/static/' and make sure we've imported - # it as a StaticContent asset - try: - remap_dict = {} + module = remap_namespace(module, target_location_namespace) - # use the rewrite_links as a utility means to enumerate through all links - # in the module data. We use that to load that reference into our asset store - # IMPORTANT: There appears to be a bug in lxml.rewrite_link which makes us not be able to - # do the rewrites natively in that code. - # For example, what I'm seeing is -> - # Note the dropped element closing tag. This causes the LMS to fail when rendering modules - that's - # no good, so we have to do this kludge - if isinstance(module_data, str) or isinstance(module_data, unicode): # some module 'data' fields are non strings which blows up the link traversal code - lxml_rewrite_links(module_data, lambda link: verify_content_links(module, course_data_path, - static_content_store, link, remap_dict)) + # cdodge: more hacks (what else). Seems like we have a problem when importing a course (like 6.002) which + # does not have any tabs defined in the policy file. The import goes fine and then displays fine in LMS, + # but if someone tries to add a new tab in the CMS, then the LMS barfs because it expects that - + # if there is *any* tabs - then there at least needs to be some predefined ones + if module.tabs is None or len(module.tabs) == 0: + module.tabs = [{"type": "courseware"}, + {"type": "course_info", "name": "Course Info"}, + {"type": "discussion", "name": "Discussion"}, + {"type": "wiki", "name": "Wiki"}] # note, add 'progress' when we can support it on Edge - for key in remap_dict.keys(): - module_data = module_data.replace(key, remap_dict[key]) - except Exception, e: - logging.exception("failed to rewrite links on {0}. Continuing...".format(module.location)) + if hasattr(module, 'data'): + store.update_item(module.location, module.data) + store.update_children(module.location, module.children) + store.update_metadata(module.location, dict(own_metadata(module))) - store.update_item(module.location, module_data) + # a bit of a hack, but typically the "course image" which is shown on marketing pages is hard coded to /images/course_image.jpg + # so let's make sure we import in case there are no other references to it in the modules + verify_content_links(module, course_data_path, static_content_store, '/static/images/course_image.jpg') - if hasattr(module, 'children') and module.children != []: - store.update_children(module.location, module.children) + course_items.append(module) - # NOTE: It's important to use own_metadata here to avoid writing - # inherited metadata everywhere. - store.update_metadata(module.location, dict(own_metadata(module))) + + # then import all the static content + if static_content_store is not None: + _namespace_rename = target_location_namespace if target_location_namespace is not None else course_location + + # first pass to find everything in /static/ + import_static_content(module_store.modules[course_id], course_location, course_data_path, static_content_store, + _namespace_rename, subpath='static', verbose=verbose) + + # finally loop through all the modules + for module in module_store.modules[course_id].itervalues(): + + if module.category == 'course': + # we've already saved the course module up at the top of the loop + # so just skip over it in the inner loop + continue + + # remap module to the new namespace + if target_location_namespace is not None: + module = remap_namespace(module, target_location_namespace) + + if verbose: + log.debug('importing module location {0}'.format(module.location)) + + content = {} + for field in module.fields: + if field.scope != Scope.content: + continue + try: + content[field.name] = module._model_data[field.name] + except KeyError: + # Ignore any missing keys in _model_data + pass + + if 'data' in content: + module_data = content['data'] + + # cdodge: now go through any link references to '/static/' and make sure we've imported + # it as a StaticContent asset + try: + remap_dict = {} + + # use the rewrite_links as a utility means to enumerate through all links + # in the module data. We use that to load that reference into our asset store + # IMPORTANT: There appears to be a bug in lxml.rewrite_link which makes us not be able to + # do the rewrites natively in that code. + # For example, what I'm seeing is -> + # Note the dropped element closing tag. This causes the LMS to fail when rendering modules - that's + # no good, so we have to do this kludge + if isinstance(module_data, str) or isinstance(module_data, unicode): # some module 'data' fields are non strings which blows up the link traversal code + lxml_rewrite_links(module_data, lambda link: verify_content_links(module, course_data_path, + static_content_store, link, remap_dict)) + + for key in remap_dict.keys(): + module_data = module_data.replace(key, remap_dict[key]) + + except Exception, e: + logging.exception("failed to rewrite links on {0}. Continuing...".format(module.location)) + + store.update_item(module.location, content) + + if hasattr(module, 'children') and module.children != []: + store.update_children(module.location, module.children) + + # NOTE: It's important to use own_metadata here to avoid writing + # inherited metadata everywhere. + store.update_metadata(module.location, dict(own_metadata(module))) + finally: + # turn back on all write signalling + if pseudo_course_id in store.ignore_write_events_on_courses: + store.ignore_write_events_on_courses.remove(pseudo_course_id) + store.refresh_cached_metadata_inheritance_tree(target_location_namespace if + target_location_namespace is not None else course_location) return module_store, course_items diff --git a/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee b/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee index a5a1deac10..56525af347 100644 --- a/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee +++ b/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee @@ -128,7 +128,9 @@ if Backbone? type: "POST" success: (response, textStatus) => if textStatus == 'success' - @model.set('pinned', true) + @model.set('pinned', true) + error: => + $('.admin-pin').text("Pinning not currently available") unPin: -> url = @model.urlFor("unPinThread") diff --git a/common/static/sass/_mixins.scss b/common/static/sass/_mixins.scss index 76d52ed930..c1dd5b7f2d 100644 --- a/common/static/sass/_mixins.scss +++ b/common/static/sass/_mixins.scss @@ -1,9 +1,12 @@ +// studio - utilities - mixins and extends +// ==================== + // font-sizing @function em($pxval, $base: 16) { @return #{$pxval / $base}em; } -@mixin font-size($sizeValue: 1.6){ +@mixin font-size($sizeValue: 16){ font-size: $sizeValue + px; font-size: ($sizeValue/10) + rem; } @@ -64,4 +67,106 @@ :-ms-input-placeholder { color: $color; } +} + +// ==================== + +// extends - visual +.faded-hr-divider { + @include background-image(linear-gradient(180deg, rgba(200,200,200, 0) 0%, + rgba(200,200,200, 1) 50%, + rgba(200,200,200, 0))); + height: 1px; + width: 100%; +} + +.faded-hr-divider-medium { + @include background-image(linear-gradient(180deg, rgba(240,240,240, 0) 0%, + rgba(240,240,240, 1) 50%, + rgba(240,240,240, 0))); + height: 1px; + width: 100%; +} + +.faded-hr-divider-light { + @include background-image(linear-gradient(180deg, rgba(255,255,255, 0) 0%, + rgba(255,255,255, 0.8) 50%, + rgba(255,255,255, 0))); + height: 1px; + width: 100%; +} + +.faded-vertical-divider { + @include background-image(linear-gradient(90deg, rgba(200,200,200, 0) 0%, + rgba(200,200,200, 1) 50%, + rgba(200,200,200, 0))); + height: 100%; + width: 1px; +} + +.faded-vertical-divider-light { + @include background-image(linear-gradient(90deg, rgba(255,255,255, 0) 0%, + rgba(255,255,255, 0.6) 50%, + rgba(255,255,255, 0))); + height: 100%; + width: 1px; +} + +.vertical-divider { + @extend .faded-vertical-divider; + position: relative; + + &::after { + @extend .faded-vertical-divider-light; + content: ""; + display: block; + position: absolute; + left: 1px; + } +} + +.horizontal-divider { + border: none; + @extend .faded-hr-divider; + position: relative; + + &::after { + @extend .faded-hr-divider-light; + content: ""; + display: block; + position: absolute; + top: 1px; + } +} + +.fade-right-hr-divider { + @include background-image(linear-gradient(180deg, rgba(200,200,200, 0) 0%, + rgba(200,200,200, 1))); + border: none; +} + +.fade-left-hr-divider { + @include background-image(linear-gradient(180deg, rgba(200,200,200, 1) 0%, + rgba(200,200,200, 0))); + border: none; +} + +// extends - ui +.window { + @include clearfix(); + @include border-radius(3px); + @include box-shadow(0 1px 1px $shadow-l1); + margin-bottom: $baseline; + border: 1px solid $gray-l2; + background: $white; +} + +.elem-d1 { + @include clearfix(); + @include box-sizing(border-box); +} + +.elem-d2 { + @include clearfix(); + @include box-sizing(border-box); } \ No newline at end of file diff --git a/common/templates/jasmine/base.html b/common/templates/jasmine/base.html index 96507bdebf..9a1b3bed92 100644 --- a/common/templates/jasmine/base.html +++ b/common/templates/jasmine/base.html @@ -13,14 +13,19 @@ + {% load compressed %} + {# static files #} + {% for url in suite.static_files %} + + {% endfor %} + + {% compressed_js 'js-test-source' %} + {# source files #} {% for url in suite.js_files %} {% endfor %} - {% load compressed %} - {# static files #} - {% compressed_js 'js-test-source' %} {# spec files #} {% compressed_js 'spec' %} diff --git a/common/test/data/full/vertical/vertical_89.xml b/common/test/data/full/vertical/vertical_89.xml index c2b68b6bc2..cf2dd23462 100644 --- a/common/test/data/full/vertical/vertical_89.xml +++ b/common/test/data/full/vertical/vertical_89.xml @@ -7,4 +7,9 @@ + +

            Have you changed your mind?

            + Yes + No + diff --git a/doc/public/internal_data_formats/sql_schema.rst b/doc/public/internal_data_formats/sql_schema.rst index 409ec1c065..92c5c4fa0e 100644 --- a/doc/public/internal_data_formats/sql_schema.rst +++ b/doc/public/internal_data_formats/sql_schema.rst @@ -313,14 +313,18 @@ There is an important split in demographic data gathered for the students who si - This student signed up before this information was collected * - `''` (blank) - User did not specify level of education. + * - `'p'` + - Doctorate * - `'p_se'` - - Doctorate in science or engineering + - Doctorate in science or engineering (no longer used) * - `'p_oth'` - - Doctorate in another field + - Doctorate in another field (no longer used) * - `'m'` - Master's or professional degree * - `'b'` - Bachelor's degree + * - `'a'` + - Associate's degree * - `'hs'` - Secondary/high school * - `'jhs'` @@ -624,4 +628,4 @@ The generatedcertificate table tracks certificate state for students who have be `grade` ------- - The grade of the student recorded at the time the certificate was generated. This may be different than the current grade since grading is only done once for a course when it ends. \ No newline at end of file + The grade of the student recorded at the time the certificate was generated. This may be different than the current grade since grading is only done once for a course when it ends. diff --git a/lms/djangoapps/courseware/features/common.py b/lms/djangoapps/courseware/features/common.py index 2e19696ad4..7d41637c8e 100644 --- a/lms/djangoapps/courseware/features/common.py +++ b/lms/djangoapps/courseware/features/common.py @@ -1,10 +1,11 @@ from lettuce import world, step -from django.core.management import call_command from nose.tools import assert_equals, assert_in from lettuce.django import django_url -from django.conf import settings from django.contrib.auth.models import User from student.models import CourseEnrollment +from xmodule.modulestore import Location +from xmodule.modulestore.django import _MODULESTORES, modulestore +from xmodule.templates import update_templates import time from logging import getLogger @@ -73,7 +74,8 @@ def should_see_in_the_page(step, text): @step('I am logged in$') def i_am_logged_in(step): world.create_user('robot') - world.log_in('robot@edx.org', 'test') + world.log_in('robot', 'test') + world.browser.visit(django_url('/')) @step('I am not logged in$') @@ -81,12 +83,56 @@ def i_am_not_logged_in(step): world.browser.cookies.delete() -@step(u'I am registered for a course$') -def i_am_registered_for_a_course(step): +TEST_COURSE_ORG = 'edx' +TEST_COURSE_NAME = 'Test Course' +TEST_SECTION_NAME = "Problem" + + +@step(u'The course "([^"]*)" exists$') +def create_course(step, course): + + # First clear the modulestore so we don't try to recreate + # the same course twice + # This also ensures that the necessary templates are loaded + flush_xmodule_store() + + # Create the course + # We always use the same org and display name, + # but vary the course identifier (e.g. 600x or 191x) + course = world.CourseFactory.create(org=TEST_COURSE_ORG, + number=course, + display_name=TEST_COURSE_NAME) + + # Add a section to the course to contain problems + section = world.ItemFactory.create(parent_location=course.location, + display_name=TEST_SECTION_NAME) + + problem_section = world.ItemFactory.create(parent_location=section.location, + template='i4x://edx/templates/sequential/Empty', + display_name=TEST_SECTION_NAME) + + +@step(u'I am registered for the course "([^"]*)"$') +def i_am_registered_for_the_course(step, course): + # Create the course + create_course(step, course) + + # Create the user world.create_user('robot') u = User.objects.get(username='robot') - CourseEnrollment.objects.create(user=u, course_id='MITx/6.002x/2012_Fall') - world.log_in('robot@edx.org', 'test') + + # If the user is not already enrolled, enroll the user. + # TODO: change to factory + CourseEnrollment.objects.get_or_create(user=u, course_id=course_id(course)) + + world.log_in('robot', 'test') + + +@step(u'The course "([^"]*)" has extra tab "([^"]*)"$') +def add_tab_to_course(step, course, extra_tab_name): + section_item = world.ItemFactory.create(parent_location=course_location(course), + template="i4x://edx/templates/static_tab/Empty", + display_name=str(extra_tab_name)) @step(u'I am an edX user$') @@ -97,3 +143,37 @@ def i_am_an_edx_user(step): @step(u'User "([^"]*)" is an edX user$') def registered_edx_user(step, uname): world.create_user(uname) + + +def flush_xmodule_store(): + # Flush and initialize the module store + # It needs the templates because it creates new records + # by cloning from the template. + # Note that if your test module gets in some weird state + # (though it shouldn't), do this manually + # from the bash shell to drop it: + # $ mongo test_xmodule --eval "db.dropDatabase()" + _MODULESTORES = {} + modulestore().collection.drop() + update_templates() + + +def course_id(course_num): + return "%s/%s/%s" % (TEST_COURSE_ORG, course_num, + TEST_COURSE_NAME.replace(" ", "_")) + + +def course_location(course_num): + return Location(loc_or_tag="i4x", + org=TEST_COURSE_ORG, + course=course_num, + category='course', + name=TEST_COURSE_NAME.replace(" ", "_")) + + +def section_location(course_num): + return Location(loc_or_tag="i4x", + org=TEST_COURSE_ORG, + course=course_num, + category='sequential', + name=TEST_SECTION_NAME.replace(" ", "_")) diff --git a/lms/djangoapps/courseware/features/courses.py b/lms/djangoapps/courseware/features/courses.py index eb5143b782..c99fb58b85 100644 --- a/lms/djangoapps/courseware/features/courses.py +++ b/lms/djangoapps/courseware/features/courses.py @@ -9,6 +9,7 @@ logger = getLogger(__name__) ## support functions + def get_courses(): ''' Returns dict of lists of courses available, keyed by course.org (ie university). @@ -82,13 +83,13 @@ def get_courseware_with_tabs(course_id): course = get_course_by_id(course_id) chapters = [chapter for chapter in course.get_children() if not chapter.lms.hide_from_toc] courseware = [{'chapter_name': c.display_name_with_default, - 'sections': [{'section_name': s.display_name_with_default, + 'sections': [{'section_name': s.display_name_with_default, 'clickable_tab_count': len(s.get_children()) if (type(s) == seq_module.SequenceDescriptor) else 0, 'tabs': [{'children_count': len(t.get_children()) if (type(t) == vertical_module.VerticalDescriptor) else 0, - 'class': t.__class__.__name__} - for t in s.get_children()]} + 'class': t.__class__.__name__} + for t in s.get_children()]} for s in c.get_children() if not s.lms.hide_from_toc]} - for c in chapters] + for c in chapters] return courseware @@ -167,7 +168,6 @@ def process_section(element, num_tabs=0): assert False, "Class for element not recognized!!" - def process_problem(element, problem_id): ''' Process problem attempts to diff --git a/lms/djangoapps/courseware/features/courseware.feature b/lms/djangoapps/courseware/features/courseware.feature deleted file mode 100644 index 279e5732c9..0000000000 --- a/lms/djangoapps/courseware/features/courseware.feature +++ /dev/null @@ -1,11 +0,0 @@ -Feature: View the Courseware Tab - As a student in an edX course - In order to work on the course - I want to view the info on the courseware tab - - Scenario: I can get to the courseware tab when logged in - Given I am registered for a course - And I log in - And I click on View Courseware - When I click on the "Courseware" tab - Then the "Courseware" tab is active diff --git a/lms/djangoapps/courseware/features/high-level-tabs.feature b/lms/djangoapps/courseware/features/high-level-tabs.feature index 2e9c4f1886..473f3f1572 100644 --- a/lms/djangoapps/courseware/features/high-level-tabs.feature +++ b/lms/djangoapps/courseware/features/high-level-tabs.feature @@ -3,21 +3,18 @@ Feature: All the high level tabs should work As a student I want to navigate through the high level tabs -# Note this didn't work as a scenario outline because -# before each scenario was not flushing the database -# TODO: break this apart so that if one fails the others -# will still run - Scenario: A student can see all tabs of the course - Given I am registered for a course - And I log in - And I click on View Courseware - When I click on the "Courseware" tab - Then the page title should be "6.002x Courseware" - When I click on the "Course Info" tab - Then the page title should be "6.002x Course Info" - When I click on the "Textbook" tab - Then the page title should be "6.002x Textbook" - When I click on the "Wiki" tab - Then the page title should be "6.002x | edX Wiki" - When I click on the "Progress" tab - Then the page title should be "6.002x Progress" +Scenario: I can navigate to all high -level tabs in a course + Given: I am registered for the course "6.002x" + And The course "6.002x" has extra tab "Custom Tab" + And I am logged in + And I click on View Courseware + When I click on the "" tab + Then the page title should contain "" + + Examples: + | TabName | PageTitle | + | Courseware | 6.002x Courseware | + | Course Info | 6.002x Course Info | + | Custom Tab | 6.002x Custom Tab | + | Wiki | edX Wiki | + | Progress | 6.002x Progress | diff --git a/lms/djangoapps/courseware/features/homepage.feature b/lms/djangoapps/courseware/features/homepage.feature index 06a45c4bfa..c0c1c32f02 100644 --- a/lms/djangoapps/courseware/features/homepage.feature +++ b/lms/djangoapps/courseware/features/homepage.feature @@ -39,9 +39,9 @@ Feature: Homepage for web users | MITx | | HarvardX | | BerkeleyX | - | UTx | + | UTx | | WellesleyX | - | GeorgetownX | + | GeorgetownX | # # TODO: Add scenario that tests the courses available # # using a policy or a configuration file diff --git a/lms/djangoapps/courseware/features/login.py b/lms/djangoapps/courseware/features/login.py index ca7d710c61..094db078ca 100644 --- a/lms/djangoapps/courseware/features/login.py +++ b/lms/djangoapps/courseware/features/login.py @@ -34,6 +34,7 @@ def click_the_dropdown(step): #### helper functions + def user_is_an_unactivated_user(uname): u = User.objects.get(username=uname) u.is_active = False diff --git a/lms/djangoapps/courseware/features/openended.feature b/lms/djangoapps/courseware/features/openended.feature index cc9f6e1c5f..1ab496144f 100644 --- a/lms/djangoapps/courseware/features/openended.feature +++ b/lms/djangoapps/courseware/features/openended.feature @@ -3,10 +3,10 @@ Feature: Open ended grading In order to complete the courseware questions I want the machine learning grading to be functional - # Commenting these all out right now until we can + # Commenting these all out right now until we can # make a reference implementation for a course with # an open ended grading problem that is always available - # + # # Scenario: An answer that is too short is rejected # Given I navigate to an openended question # And I enter the answer "z" diff --git a/lms/djangoapps/courseware/features/problems.feature b/lms/djangoapps/courseware/features/problems.feature new file mode 100644 index 0000000000..efeb338c45 --- /dev/null +++ b/lms/djangoapps/courseware/features/problems.feature @@ -0,0 +1,77 @@ +Feature: Answer problems + As a student in an edX course + In order to test my understanding of the material + I want to answer problems + + Scenario: I can answer a problem correctly + Given External graders respond "correct" + And I am viewing a "" problem + When I answer a "" problem "correctly" + Then My "" answer is marked "correct" + + Examples: + | ProblemType | + | drop down | + | multiple choice | + | checkbox | + | string | + | numerical | + | formula | + | script | + | code | + + Scenario: I can answer a problem incorrectly + Given External graders respond "incorrect" + And I am viewing a "" problem + When I answer a "" problem "incorrectly" + Then My "" answer is marked "incorrect" + + Examples: + | ProblemType | + | drop down | + | multiple choice | + | checkbox | + | string | + | numerical | + | formula | + | script | + | code | + + Scenario: I can submit a blank answer + Given I am viewing a "" problem + When I check a problem + Then My "" answer is marked "incorrect" + + Examples: + | ProblemType | + | drop down | + | multiple choice | + | checkbox | + | string | + | numerical | + | formula | + | script | + + + Scenario: I can reset a problem + Given I am viewing a "" problem + And I answer a "" problem "ly" + When I reset the problem + Then My "" answer is marked "unanswered" + + Examples: + | ProblemType | Correctness | + | drop down | correct | + | drop down | incorrect | + | multiple choice | correct | + | multiple choice | incorrect | + | checkbox | correct | + | checkbox | incorrect | + | string | correct | + | string | incorrect | + | numerical | correct | + | numerical | incorrect | + | formula | correct | + | formula | incorrect | + | script | correct | + | script | incorrect | diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py new file mode 100644 index 0000000000..6b2239c38b --- /dev/null +++ b/lms/djangoapps/courseware/features/problems.py @@ -0,0 +1,296 @@ +from lettuce import world, step +from lettuce.django import django_url +import random +import textwrap +import time +from common import i_am_registered_for_the_course, TEST_SECTION_NAME, section_location +from capa.tests.response_xml_factory import OptionResponseXMLFactory, \ + ChoiceResponseXMLFactory, MultipleChoiceResponseXMLFactory, \ + StringResponseXMLFactory, NumericalResponseXMLFactory, \ + FormulaResponseXMLFactory, CustomResponseXMLFactory, \ + CodeResponseXMLFactory + +# Factories from capa.tests.response_xml_factory that we will use +# to generate the problem XML, with the keyword args used to configure +# the output. +PROBLEM_FACTORY_DICT = { + 'drop down': { + 'factory': OptionResponseXMLFactory(), + 'kwargs': { + 'question_text': 'The correct answer is Option 2', + 'options': ['Option 1', 'Option 2', 'Option 3', 'Option 4'], + 'correct_option': 'Option 2'}}, + + 'multiple choice': { + 'factory': MultipleChoiceResponseXMLFactory(), + 'kwargs': { + 'question_text': 'The correct answer is Choice 3', + 'choices': [False, False, True, False], + 'choice_names': ['choice_1', 'choice_2', 'choice_3', 'choice_4']}}, + + 'checkbox': { + 'factory': ChoiceResponseXMLFactory(), + 'kwargs': { + 'question_text': 'The correct answer is Choices 1 and 3', + 'choice_type': 'checkbox', + 'choices': [True, False, True, False, False], + 'choice_names': ['Choice 1', 'Choice 2', 'Choice 3', 'Choice 4']}}, + + 'string': { + 'factory': StringResponseXMLFactory(), + 'kwargs': { + 'question_text': 'The answer is "correct string"', + 'case_sensitive': False, + 'answer': 'correct string'}}, + + 'numerical': { + 'factory': NumericalResponseXMLFactory(), + 'kwargs': { + 'question_text': 'The answer is pi + 1', + 'answer': '4.14159', + 'tolerance': '0.00001', + 'math_display': True}}, + + 'formula': { + 'factory': FormulaResponseXMLFactory(), + 'kwargs': { + 'question_text': 'The solution is [mathjax]x^2+2x+y[/mathjax]', + 'sample_dict': {'x': (-100, 100), 'y': (-100, 100)}, + 'num_samples': 10, + 'tolerance': 0.00001, + 'math_display': True, + 'answer': 'x^2+2*x+y'}}, + + 'script': { + 'factory': CustomResponseXMLFactory(), + 'kwargs': { + 'question_text': 'Enter two integers that sum to 10.', + 'cfn': 'test_add_to_ten', + 'expect': '10', + 'num_inputs': 2, + 'script': textwrap.dedent(""" + def test_add_to_ten(expect,ans): + try: + a1=int(ans[0]) + a2=int(ans[1]) + except ValueError: + a1=0 + a2=0 + return (a1+a2)==int(expect) + """)}}, + 'code': { + 'factory': CodeResponseXMLFactory(), + 'kwargs': { + 'question_text': 'Submit code to an external grader', + 'initial_display': 'print "Hello world!"', + 'grader_payload': '{"grader": "ps1/Spring2013/test_grader.py"}', }}, + } + + +def add_problem_to_course(course, problem_type): + + assert(problem_type in PROBLEM_FACTORY_DICT) + + # Generate the problem XML using capa.tests.response_xml_factory + factory_dict = PROBLEM_FACTORY_DICT[problem_type] + problem_xml = factory_dict['factory'].build_xml(**factory_dict['kwargs']) + + # Create a problem item using our generated XML + # We set rerandomize=always in the metadata so that the "Reset" button + # will appear. + problem_item = world.ItemFactory.create(parent_location=section_location(course), + template="i4x://edx/templates/problem/Blank_Common_Problem", + display_name=str(problem_type), + data=problem_xml, + metadata={'rerandomize': 'always'}) + + +@step(u'I am viewing a "([^"]*)" problem') +def view_problem(step, problem_type): + i_am_registered_for_the_course(step, 'model_course') + + # Ensure that the course has this problem type + add_problem_to_course('model_course', problem_type) + + # Go to the one section in the factory-created course + # which should be loaded with the correct problem + chapter_name = TEST_SECTION_NAME.replace(" ", "_") + section_name = chapter_name + url = django_url('/courses/edx/model_course/Test_Course/courseware/%s/%s' % + (chapter_name, section_name)) + + world.browser.visit(url) + + +@step(u'External graders respond "([^"]*)"') +def set_external_grader_response(step, correctness): + assert(correctness in ['correct', 'incorrect']) + + response_dict = {'correct': True if correctness == 'correct' else False, + 'score': 1 if correctness == 'correct' else 0, + 'msg': 'Your problem was graded %s' % correctness} + + # Set the fake xqueue server to always respond + # correct/incorrect when asked to grade a problem + world.xqueue_server.set_grade_response(response_dict) + + +@step(u'I answer a "([^"]*)" problem "([^"]*)ly"') +def answer_problem(step, problem_type, correctness): + """ Mark a given problem type correct or incorrect, then submit it. + + *problem_type* is a string representing the type of problem (e.g. 'drop down') + *correctness* is in ['correct', 'incorrect'] + """ + + assert(correctness in ['correct', 'incorrect']) + + if problem_type == "drop down": + select_name = "input_i4x-edx-model_course-problem-drop_down_2_1" + option_text = 'Option 2' if correctness == 'correct' else 'Option 3' + world.browser.select(select_name, option_text) + + elif problem_type == "multiple choice": + if correctness == 'correct': + inputfield('multiple choice', choice='choice_3').check() + else: + inputfield('multiple choice', choice='choice_2').check() + + elif problem_type == "checkbox": + if correctness == 'correct': + inputfield('checkbox', choice='choice_0').check() + inputfield('checkbox', choice='choice_2').check() + else: + inputfield('checkbox', choice='choice_3').check() + + elif problem_type == 'string': + textvalue = 'correct string' if correctness == 'correct' else 'incorrect' + inputfield('string').fill(textvalue) + + elif problem_type == 'numerical': + textvalue = "pi + 1" if correctness == 'correct' else str(random.randint(-2, 2)) + inputfield('numerical').fill(textvalue) + + elif problem_type == 'formula': + textvalue = "x^2+2*x+y" if correctness == 'correct' else 'x^2' + inputfield('formula').fill(textvalue) + + elif problem_type == 'script': + # Correct answer is any two integers that sum to 10 + first_addend = random.randint(-100, 100) + second_addend = 10 - first_addend + + # If we want an incorrect answer, then change + # the second addend so they no longer sum to 10 + if correctness == 'incorrect': + second_addend += random.randint(1, 10) + + inputfield('script', input_num=1).fill(str(first_addend)) + inputfield('script', input_num=2).fill(str(second_addend)) + + elif problem_type == 'code': + # The fake xqueue server is configured to respond + # correct / incorrect no matter what we submit. + # Furthermore, since the inline code response uses + # JavaScript to make the code display nicely, it's difficult + # to programatically input text + # (there's not