From 630607d34554aef03f4e74adbdf26f0dd3b01496 Mon Sep 17 00:00:00 2001 From: ichuang Date: Tue, 5 Feb 2013 00:21:04 +0000 Subject: [PATCH 001/436] fix forum management commands (assign_default_role moved); add reload_forum_users --- .../commands/assign_roles_for_course.py | 3 +- .../commands/create_roles_for_existing.py | 3 +- .../management/commands/reload_forum_users.py | 29 +++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 lms/djangoapps/django_comment_client/management/commands/reload_forum_users.py diff --git a/lms/djangoapps/django_comment_client/management/commands/assign_roles_for_course.py b/lms/djangoapps/django_comment_client/management/commands/assign_roles_for_course.py index 82f2290bc7..64378108b6 100644 --- a/lms/djangoapps/django_comment_client/management/commands/assign_roles_for_course.py +++ b/lms/djangoapps/django_comment_client/management/commands/assign_roles_for_course.py @@ -6,7 +6,8 @@ Enrollments. """ from django.core.management.base import BaseCommand, CommandError -from student.models import CourseEnrollment, assign_default_role +from student.models import CourseEnrollment +from django_comment_client.models import assign_default_role class Command(BaseCommand): args = 'course_id' diff --git a/lms/djangoapps/django_comment_client/management/commands/create_roles_for_existing.py b/lms/djangoapps/django_comment_client/management/commands/create_roles_for_existing.py index d1244a6690..3ec2d0646e 100644 --- a/lms/djangoapps/django_comment_client/management/commands/create_roles_for_existing.py +++ b/lms/djangoapps/django_comment_client/management/commands/create_roles_for_existing.py @@ -6,7 +6,8 @@ Enrollments. """ from django.core.management.base import BaseCommand, CommandError -from student.models import CourseEnrollment, assign_default_role +from student.models import CourseEnrollment +from django_comment_client.models import assign_default_role class Command(BaseCommand): args = 'course_id' diff --git a/lms/djangoapps/django_comment_client/management/commands/reload_forum_users.py b/lms/djangoapps/django_comment_client/management/commands/reload_forum_users.py new file mode 100644 index 0000000000..5e7e268270 --- /dev/null +++ b/lms/djangoapps/django_comment_client/management/commands/reload_forum_users.py @@ -0,0 +1,29 @@ +""" +Reload forum (comment client) users from existing users. +""" +from django.core.management.base import BaseCommand, CommandError + +from django.contrib.auth.models import User +import comment_client as cc + +class Command(BaseCommand): + help = 'Reload forum (comment client) users from existing users' + + def adduser(self,user): + print user + try: + cc_user = cc.User.from_django_user(user) + cc_user.save() + except Exception as err: + print "update user info to discussion failed for user with id: %s" % user + + def handle(self, *args, **options): + if len(args) != 0: + uset = [User.objects.get(username=x) for x in args] + else: + uset = User.objects.all() + + for user in uset: + self.adduser(user) + + \ No newline at end of file From 66657db0bee2e26a3966e6ca04b2483f88918754 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Mon, 4 Mar 2013 09:49:41 -0700 Subject: [PATCH 002/436] Added support for superscripts in variables and fixed bug with normal subscripted variables raised to powers --- lms/lib/symmath/formula.py | 60 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/lms/lib/symmath/formula.py b/lms/lib/symmath/formula.py index c34156da52..db74d5b271 100644 --- a/lms/lib/symmath/formula.py +++ b/lms/lib/symmath/formula.py @@ -247,6 +247,65 @@ class formula(object): fix_hat(k) fix_hat(xml) + def flatten_pmathml(xml): + ''' + Give the text version of PMathML elements + ''' + tag = gettag(xml) + if tag == 'mn': return xml.text + elif tag == 'mi': return xml.text + # elif tag == 'msub': return '_'.join([flatten_pmathml(y) for y in xml]) + # elif tag == 'msup': return '^'.join([flatten_pmathml(y) for y in xml]) + elif tag == 'mrow': return ''.join([flatten_pmathml(y) for y in xml]) + raise Exception, '[flatten_pmathml] unknown tag %s' % tag + + # find "tagged" superscripts + # they have the character \u200b in the superscript + # replace them with a__b so snuggle doesn't get confused + def fix_superscripts(xml): + for k in xml: + tag = gettag(k) + + # match node to a superscript + if (tag == 'msup' and + len(k) == 2 and gettag(k[1]) == 'mrow' and + gettag(k[1][0]) == 'mo' and k[1][0].text == u'\u200b'): # whew + + k[1].remove(k[1][0]) + newk = etree.Element('mi') + newk.text = '%s__%s' % (flatten_pmathml(k[0]), flatten_pmathml(k[1])) + xml.replace(k, newk) + + if (tag == 'msubsup' and + len(k) == 3 and gettag(k[2]) == 'mrow' and + gettag(k[2][0]) == 'mo' and k[2][0].text == u'\u200b'): # whew + + k[2].remove(k[2][0]) + newk = etree.Element('mi') + newk.text = '%s_%s__%s' % (flatten_pmathml(k[0]), flatten_pmathml(k[1]), flatten_pmathml(k[2])) + xml.replace(k, newk) + + fix_superscripts(k) + fix_superscripts(xml) + + # Snuggle returns an error when it sees an + # replace such elements with an , except the first element is of + # the form a_b. I.e. map a_b^c => (a_b)^c + def fix_msubsup(parent): + for child in parent: + # fix msubsup + if (gettag(child) == 'msubsup' and len(child) == 3): + newchild = etree.Element('msup') + newbase = etree.Element('mi') + newbase.text = '%s_%s' % (flatten_pmathml(child[0]), flatten_pmathml(child[1])) + newexp = child[2] + newchild.append(newbase) + newchild.append(newexp) + parent.replace(child, newchild) + + fix_msubsup(child) + fix_msubsup(xml) + self.xml = xml return self.xml @@ -257,6 +316,7 @@ class formula(object): try: xml = self.preprocess_pmathml(self.expr) except Exception, err: + # print 'Err %s while preprocessing; expr=%s' % (err, self.expr) return "Error! Cannot process pmathml" pmathml = etree.tostring(xml, pretty_print=True) self.the_pmathml = pmathml From c6545eb092d7bcbe2d934ac2753d6fb8113f0468 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Wed, 6 Mar 2013 06:21:08 -0700 Subject: [PATCH 003/436] Begin to document symmath as we go --- .../js/capa/symbolic_mathjax_preprocessor.js | 22 +++++++ .../course_data_formats/symbolic_response.rst | 26 ++++++++ lms/lib/symmath/formula.py | 59 +++++++++++++++++-- 3 files changed, 101 insertions(+), 6 deletions(-) create mode 100644 common/static/js/capa/symbolic_mathjax_preprocessor.js create mode 100644 doc/public/course_data_formats/symbolic_response.rst diff --git a/common/static/js/capa/symbolic_mathjax_preprocessor.js b/common/static/js/capa/symbolic_mathjax_preprocessor.js new file mode 100644 index 0000000000..19104553dc --- /dev/null +++ b/common/static/js/capa/symbolic_mathjax_preprocessor.js @@ -0,0 +1,22 @@ +window.SymbolicMathjaxPreprocessor = function () { + this.fn = function (eqn) { + // flags and config + var superscriptsOn = true; + + if (superscriptsOn) { + // find instances of "__" and make them superscripts ("^") and tag them + // as such. Specifcally replace instances of "__X" or "__{XYZ}" with + // "^{CHAR$1}", marking superscripts as different from powers + + // a zero width space--this is an invisible character that no one would + // use, that gets passed through MathJax and to the server + var c = "\u200b"; + eqn = eqn.replace(/__(?:([^\{])|\{([^\}]+)\})/g, '^{' + c + '$1$2}'); + + // NOTE: MathJax supports '\class{name}{mathcode}' but not for asciimath + // input, which is too bad. This would be preferable to the char tag + } + + return eqn; + }; +}; diff --git a/doc/public/course_data_formats/symbolic_response.rst b/doc/public/course_data_formats/symbolic_response.rst new file mode 100644 index 0000000000..773821766e --- /dev/null +++ b/doc/public/course_data_formats/symbolic_response.rst @@ -0,0 +1,26 @@ +################# +Symbolic Response +################# + +This document plans to document features that the current symbolic response +supports. In general it allows the input and validation of math expressions, +up to commutativity and some identities. + + +******** +Features +******** + +This is a partial list of features, to be revised as we go along: + * sub and superscripts: an expression following the ``^`` character + indicates exponentiation. To use superscripts in variables, the syntax + is ``b_x__d`` for the variable ``b`` with subscript ``x`` and super + ``d``. + + An example of a problem:: + + + + + + It's a bit of a pain to enter that. diff --git a/lms/lib/symmath/formula.py b/lms/lib/symmath/formula.py index 7c4ea084d6..914a65d1b0 100644 --- a/lms/lib/symmath/formula.py +++ b/lms/lib/symmath/formula.py @@ -248,14 +248,21 @@ class formula(object): fix_hat(xml) def flatten_pmathml(xml): - ''' - Give the text version of PMathML elements + ''' Give the text version of certain PMathML elements + + Sometimes MathML will be given with each letter separated (it + doesn't know if its implicit multiplication or what). From an xml + node, find the (text only) variable name it represents. So it takes + + m + a + x + + and returns 'max', for easier use later on. ''' tag = gettag(xml) if tag == 'mn': return xml.text elif tag == 'mi': return xml.text - # elif tag == 'msub': return '_'.join([flatten_pmathml(y) for y in xml]) - # elif tag == 'msup': return '^'.join([flatten_pmathml(y) for y in xml]) elif tag == 'mrow': return ''.join([flatten_pmathml(y) for y in xml]) raise Exception, '[flatten_pmathml] unknown tag %s' % tag @@ -263,23 +270,63 @@ class formula(object): # they have the character \u200b in the superscript # replace them with a__b so snuggle doesn't get confused def fix_superscripts(xml): + ''' Look for and replace sup elements with 'X__Y' or 'X_Y__Z' + + In the javascript, variables with '__X' in them had an invisible + character inserted into the sup (to distinguish from powers) + E.g. normal: + + a + b + c + + to be interpreted '(a_b)^c' (nothing done by this method) + + And modified: + + b + x + + + d + + + to be interpreted 'a_b__c' + + also: + + x + + + B + + + to be 'x__B' + ''' for k in xml: tag = gettag(k) - # match node to a superscript + # match things like the last example-- + # the second item in msub is an mrow with the first + # character equal to \u200b if (tag == 'msup' and len(k) == 2 and gettag(k[1]) == 'mrow' and gettag(k[1][0]) == 'mo' and k[1][0].text == u'\u200b'): # whew + # replace the msup with 'X__Y' k[1].remove(k[1][0]) newk = etree.Element('mi') newk.text = '%s__%s' % (flatten_pmathml(k[0]), flatten_pmathml(k[1])) xml.replace(k, newk) + # match things like the middle example- + # the third item in msubsup is an mrow with the first + # character equal to \u200b if (tag == 'msubsup' and len(k) == 3 and gettag(k[2]) == 'mrow' and gettag(k[2][0]) == 'mo' and k[2][0].text == u'\u200b'): # whew + # replace the msubsup with 'X_Y__Z' k[2].remove(k[2][0]) newk = etree.Element('mi') newk.text = '%s_%s__%s' % (flatten_pmathml(k[0]), flatten_pmathml(k[1]), flatten_pmathml(k[2])) @@ -316,7 +363,7 @@ class formula(object): try: xml = self.preprocess_pmathml(self.expr) except Exception, err: - # print 'Err %s while preprocessing; expr=%s' % (err, self.expr) + log.warning('Err %s while preprocessing; expr=%s' % (err, self.expr)) return "Error! Cannot process pmathml" pmathml = etree.tostring(xml, pretty_print=True) self.the_pmathml = pmathml From 4d136f8d3bc1d247941b68f0af8b2caac4e947b0 Mon Sep 17 00:00:00 2001 From: Arthur Barrett Date: Thu, 7 Mar 2013 11:51:45 -0500 Subject: [PATCH 004/436] fixed annotation tooltip styling issue in studio --- common/lib/xmodule/xmodule/css/annotatable/display.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/common/lib/xmodule/xmodule/css/annotatable/display.scss b/common/lib/xmodule/xmodule/css/annotatable/display.scss index 308b379ec1..c462d4806e 100644 --- a/common/lib/xmodule/xmodule/css/annotatable/display.scss +++ b/common/lib/xmodule/xmodule/css/annotatable/display.scss @@ -127,6 +127,7 @@ $body-font-size: em(14); font-weight: 400; padding: 0 10px 10px 10px; background-color: transparent; + border-color: transparent; } p { color: inherit; From f5c3775b5dcbb8b16e6a0fcd27fd8b835516a56e Mon Sep 17 00:00:00 2001 From: Arthur Barrett Date: Thu, 7 Mar 2013 12:21:47 -0500 Subject: [PATCH 005/436] fixed the annotation tooltip line height so it is the same in studio and the lms. --- common/lib/xmodule/xmodule/css/annotatable/display.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/common/lib/xmodule/xmodule/css/annotatable/display.scss b/common/lib/xmodule/xmodule/css/annotatable/display.scss index c462d4806e..b5739b28fc 100644 --- a/common/lib/xmodule/xmodule/css/annotatable/display.scss +++ b/common/lib/xmodule/xmodule/css/annotatable/display.scss @@ -144,6 +144,7 @@ $body-font-size: em(14); margin: 0px 0px 10px 0; max-height: 225px; overflow: auto; + line-height: normal; } .annotatable-reply { display: block; From 60b060263c15bb90fc658349224c50158609e31d Mon Sep 17 00:00:00 2001 From: Arthur Barrett Date: Thu, 7 Mar 2013 16:02:22 -0500 Subject: [PATCH 006/436] refactor highlight css to prevent issues with cascade --- common/lib/xmodule/xmodule/css/annotatable/display.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/annotatable/display.scss b/common/lib/xmodule/xmodule/css/annotatable/display.scss index b5739b28fc..f8ae779b8c 100644 --- a/common/lib/xmodule/xmodule/css/annotatable/display.scss +++ b/common/lib/xmodule/xmodule/css/annotatable/display.scss @@ -55,6 +55,7 @@ $body-font-size: em(14); display: inline; cursor: pointer; + $highlight_index: 0; @each $highlight in ( (yellow rgba(255,255,10,0.3) rgba(255,255,10,0.9)), (red rgba(178,19,16,0.3) rgba(178,19,16,0.9)), @@ -62,12 +63,13 @@ $body-font-size: em(14); (green rgba(25,255,132,0.3) rgba(25,255,132,0.9)), (blue rgba(35,163,255,0.3) rgba(35,163,255,0.9)), (purple rgba(115,9,178,0.3) rgba(115,9,178,0.9))) { - + + $highlight_index: $highlight_index + 1; $marker: nth($highlight,1); $color: nth($highlight,2); $selected_color: nth($highlight,3); - @if $marker == yellow { + @if $highlight_index == 1 { &.highlight { background-color: $color; &.selected { background-color: $selected_color; } @@ -167,5 +169,3 @@ $body-font-size: em(14); border-top-color: rgba(0, 0, 0, .85); } } - - From 49f85211fa5c5550897d25aceb786ac82d1259ee Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Fri, 8 Mar 2013 03:39:34 -0700 Subject: [PATCH 007/436] More documentation for the javascript --- .../js/capa/symbolic_mathjax_preprocessor.js | 45 ++++++++++++------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/common/static/js/capa/symbolic_mathjax_preprocessor.js b/common/static/js/capa/symbolic_mathjax_preprocessor.js index 19104553dc..766e5efc03 100644 --- a/common/static/js/capa/symbolic_mathjax_preprocessor.js +++ b/common/static/js/capa/symbolic_mathjax_preprocessor.js @@ -1,22 +1,35 @@ +/* This file defines a processor in between the student's math input + (AsciiMath) and what is read by MathJax. It allows for our own + customizations, such as use of the syntax "a_b__x" in superscripts, or + possibly coloring certain variables, etc&. + + It is used in the definition like the following: + + + + +*/ window.SymbolicMathjaxPreprocessor = function () { - this.fn = function (eqn) { - // flags and config - var superscriptsOn = true; + this.fn = function (eqn) { + // flags and config + var superscriptsOn = true; - if (superscriptsOn) { - // find instances of "__" and make them superscripts ("^") and tag them - // as such. Specifcally replace instances of "__X" or "__{XYZ}" with - // "^{CHAR$1}", marking superscripts as different from powers + if (superscriptsOn) { + // find instances of "__" and make them superscripts ("^") and tag them + // as such. Specifcally replace instances of "__X" or "__{XYZ}" with + // "^{CHAR$1}", marking superscripts as different from powers - // a zero width space--this is an invisible character that no one would - // use, that gets passed through MathJax and to the server - var c = "\u200b"; - eqn = eqn.replace(/__(?:([^\{])|\{([^\}]+)\})/g, '^{' + c + '$1$2}'); + // a zero width space--this is an invisible character that no one would + // use, that gets passed through MathJax and to the server + var c = "\u200b"; + eqn = eqn.replace(/__(?:([^\{])|\{([^\}]+)\})/g, '^{' + c + '$1$2}'); - // NOTE: MathJax supports '\class{name}{mathcode}' but not for asciimath - // input, which is too bad. This would be preferable to the char tag - } + // NOTE: MathJax supports '\class{name}{mathcode}' but not for asciimath + // input, which is too bad. This would be preferable to this char tag + } - return eqn; - }; + return eqn; + }; }; From 3b2c970248947758fc45882d9a7762f397aa8bdc Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Fri, 8 Mar 2013 11:47:11 -0500 Subject: [PATCH 008/436] studio - reorganized and documented used Sass for studio --- cms/static/sass/_account.scss | 294 --------- cms/static/sass/_alerts.scss | 162 ----- cms/static/sass/_assets.scss | 186 ------ cms/static/sass/_base.scss | 4 +- cms/static/sass/_calendar.scss | 367 ----------- cms/static/sass/_cms_mixins.scss | 3 + cms/static/sass/_content-types.scss | 69 --- cms/static/sass/_course-info.scss | 218 ------- cms/static/sass/_courseware.scss | 689 --------------------- cms/static/sass/_dashboard.scss | 114 ---- cms/static/sass/_export.scss | 123 ---- cms/static/sass/_extends.scss | 78 --- cms/static/sass/_fonts.scss | 36 -- cms/static/sass/_footer.scss | 48 -- cms/static/sass/_graphics.scss | 336 ---------- cms/static/sass/_header.scss | 562 ----------------- cms/static/sass/_import.scss | 102 ---- cms/static/sass/_index.scss | 353 ----------- cms/static/sass/_jquery-ui-calendar.scss | 53 -- cms/static/sass/_keyframes.scss | 27 - cms/static/sass/_landing.scss | 126 ---- cms/static/sass/_layout.scss | 125 ---- cms/static/sass/_lms.scss | 69 --- cms/static/sass/_login.scss | 139 ----- cms/static/sass/_modal.scss | 69 --- cms/static/sass/_module-header.scss | 128 ---- cms/static/sass/_problem.scss | 24 - cms/static/sass/_reset.scss | 3 + cms/static/sass/_section.scss | 239 -------- cms/static/sass/_settings.scss | 740 ----------------------- cms/static/sass/_static-pages.scss | 153 ----- cms/static/sass/_subsection.scss | 295 --------- cms/static/sass/_unit.scss | 667 -------------------- cms/static/sass/_users.scss | 78 --- cms/static/sass/_variables.scss | 3 + cms/static/sass/_video.scss | 33 - cms/static/sass/_week.scss | 256 -------- cms/static/sass/base-style.scss | 68 ++- common/static/sass/_mixins.scss | 89 ++- 39 files changed, 138 insertions(+), 6990 deletions(-) delete mode 100644 cms/static/sass/_account.scss delete mode 100644 cms/static/sass/_alerts.scss delete mode 100644 cms/static/sass/_assets.scss delete mode 100644 cms/static/sass/_calendar.scss delete mode 100644 cms/static/sass/_content-types.scss delete mode 100644 cms/static/sass/_course-info.scss delete mode 100644 cms/static/sass/_courseware.scss delete mode 100644 cms/static/sass/_dashboard.scss delete mode 100644 cms/static/sass/_export.scss delete mode 100644 cms/static/sass/_extends.scss delete mode 100644 cms/static/sass/_fonts.scss delete mode 100644 cms/static/sass/_footer.scss delete mode 100644 cms/static/sass/_graphics.scss delete mode 100644 cms/static/sass/_header.scss delete mode 100644 cms/static/sass/_import.scss delete mode 100644 cms/static/sass/_index.scss delete mode 100644 cms/static/sass/_jquery-ui-calendar.scss delete mode 100644 cms/static/sass/_keyframes.scss delete mode 100644 cms/static/sass/_landing.scss delete mode 100644 cms/static/sass/_layout.scss delete mode 100644 cms/static/sass/_lms.scss delete mode 100644 cms/static/sass/_login.scss delete mode 100644 cms/static/sass/_modal.scss delete mode 100644 cms/static/sass/_module-header.scss delete mode 100644 cms/static/sass/_problem.scss delete mode 100644 cms/static/sass/_section.scss delete mode 100644 cms/static/sass/_settings.scss delete mode 100644 cms/static/sass/_static-pages.scss delete mode 100644 cms/static/sass/_subsection.scss delete mode 100644 cms/static/sass/_unit.scss delete mode 100644 cms/static/sass/_users.scss delete mode 100644 cms/static/sass/_video.scss delete mode 100644 cms/static/sass/_week.scss diff --git a/cms/static/sass/_account.scss b/cms/static/sass/_account.scss deleted file mode 100644 index 650743979f..0000000000 --- a/cms/static/sass/_account.scss +++ /dev/null @@ -1,294 +0,0 @@ -// Studio - Sign In/Up -// ==================== -body.signup, body.signin { - - .wrapper-content { - margin: 0; - padding: 0 $baseline; - position: relative; - width: 100%; - } - - .content { - @include clearfix(); - @include font-size(16); - max-width: $fg-max-width; - min-width: $fg-min-width; - width: flex-grid(12); - margin: 0 auto; - color: $gray-d2; - - header { - position: relative; - margin-bottom: $baseline; - border-bottom: 1px solid $gray-l4; - padding-bottom: ($baseline/2); - - h1 { - @include font-size(32); - margin: 0; - padding: 0; - font-weight: 600; - } - - .action { - @include font-size(13); - position: absolute; - right: 0; - top: 40%; - } - } - - .introduction { - @include font-size(14); - margin: 0 0 $baseline 0; - } - } - - .content-primary, .content-supplementary { - @include box-sizing(border-box); - float: left; - } - - .content-primary { - width: flex-grid(8, 12); - margin-right: flex-gutter(); - - form { - @include box-sizing(border-box); - @include box-shadow(0 1px 2px $shadow-l1); - @include border-radius(2px); - width: 100%; - border: 1px solid $gray-l2; - padding: $baseline ($baseline*1.5); - background: $white; - - .form-actions { - margin-top: $baseline; - - .action-primary { - @include blue-button; - @include transition(all .15s); - @include font-size(15); - display:block; - width: 100%; - padding: ($baseline*0.75) ($baseline/2); - font-weight: 600; - text-transform: uppercase; - } - } - - .list-input { - margin: 0; - padding: 0; - list-style: none; - - .field { - margin: 0 0 ($baseline*0.75) 0; - - &:last-child { - margin-bottom: 0; - } - - &.required { - - label { - font-weight: 600; - } - - label:after { - margin-left: ($baseline/4); - content: "*"; - } - } - - label, input, textarea { - display: block; - } - - label { - @include font-size(14); - @include transition(color, 0.15s, ease-in-out); - margin: 0 0 ($baseline/4) 0; - - &.is-focused { - color: $blue; - } - } - - input, textarea { - @include font-size(16); - height: 100%; - width: 100%; - padding: ($baseline/2); - - &.long { - width: 100%; - } - - &.short { - width: 25%; - } - - ::-webkit-input-placeholder { - color: $gray-l4; - } - - :-moz-placeholder { - color: $gray-l3; - } - - ::-moz-placeholder { - color: $gray-l3; - } - - :-ms-input-placeholder { - color: $gray-l3; - } - - &:focus { - - + .tip { - color: $gray; - } - } - } - - textarea.long { - height: ($baseline*5); - } - - input[type="checkbox"] { - display: inline-block; - margin-right: ($baseline/4); - width: auto; - height: auto; - - & + label { - display: inline-block; - } - } - - .tip { - @include transition(color, 0.15s, ease-in-out); - @include font-size(13); - display: block; - margin-top: ($baseline/4); - color: $gray-l3; - } - } - - .field-group { - @include clearfix(); - margin: 0 0 ($baseline/2) 0; - - .field { - display: block; - width: 47%; - border-bottom: none; - margin: 0 $baseline 0 0; - padding-bottom: 0; - - &:nth-child(odd) { - float: left; - } - - &:nth-child(even) { - float: right; - margin-right: 0; - } - - input, textarea { - width: 100%; - } - } - } - } - } - } - - .content-supplementary { - width: flex-grid(4, 12); - - .bit { - @include font-size(13); - margin: 0 0 $baseline 0; - border-bottom: 1px solid $gray-l4; - padding: 0 0 $baseline 0; - color: $gray-l1; - - &:last-child { - margin-bottom: 0; - border: none; - padding-bottom: 0; - } - - h3 { - @include font-size(14); - margin: 0 0 ($baseline/4) 0; - color: $gray-d2; - font-weight: 600; - } - - } - } -} - -.signup { - -} - -.signin { - - #field-password { - position: relative; - - .action-forgotpassword { - @include font-size(13); - position: absolute; - top: 0; - right: 0; - } - } -} - -// ==================== - -// messages -.message { - @include font-size(14); - display: block; -} - -.message-status { - display: none; - @include border-top-radius(2px); - @include box-sizing(border-box); - border-bottom: 2px solid $yellow-d2; - margin: 0 0 $baseline 0; - padding: ($baseline/2) $baseline; - font-weight: 500; - background: $yellow-d1; - color: $white; - - .ss-icon { - position: relative; - top: 3px; - @include font-size(16); - display: inline-block; - margin-right: ($baseline/2); - } - - .text { - display: inline-block; - } - - &.error { - border-color: shade($red, 50%); - background: tint($red, 20%); - } - - &.is-shown { - display: block; - } -} diff --git a/cms/static/sass/_alerts.scss b/cms/static/sass/_alerts.scss deleted file mode 100644 index bd7f687f67..0000000000 --- a/cms/static/sass/_alerts.scss +++ /dev/null @@ -1,162 +0,0 @@ -// notifications -.wrapper-notification { - @include clearfix(); - @include box-sizing(border-box); - @include transition (bottom 2.0s ease-in-out 5s); - @include box-shadow(0 -1px 2px rgba(0,0,0,0.1)); - position: fixed; - bottom: -100px; - z-index: 1000; - width: 100%; - overflow: hidden; - opacity: 0; - border-top: 1px solid $darkGrey; - padding: 20px 40px; - - &.is-shown { - bottom: 0; - opacity: 1.0; - } - - &.wrapper-notification-warning { - border-color: shade($yellow, 25%); - background: tint($yellow, 25%); - } - - &.wrapper-notification-error { - border-color: shade($red, 50%); - background: tint($red, 20%); - color: $white; - } - - &.wrapper-notification-confirm { - border-color: shade($green, 30%); - background: tint($green, 40%); - color: shade($green, 30%); - } -} - -.notification { - @include box-sizing(border-box); - margin: 0 auto; - width: flex-grid(12); - max-width: $fg-max-width; - min-width: $fg-min-width; - - .copy { - float: left; - width: flex-grid(9, 12); - margin-right: flex-gutter(); - margin-top: 5px; - font-size: 14px; - - .icon { - display: inline-block; - vertical-align: top; - margin-right: 5px; - font-size: 20px; - } - - p { - width: flex-grid(8, 9); - display: inline-block; - vertical-align: top; - } - } - - .actions { - float: right; - width: flex-grid(3, 12); - margin-top: ($baseline/2); - text-align: right; - - li { - display: inline-block; - vertical-align: middle; - margin-right: 10px; - - &:last-child { - margin-right: 0; - } - } - - .save-button { - @include blue-button; - } - - .cancel-button { - @include white-button; - } - } - - strong { - font-weight: 700; - } -} - -// adopted alerts -.alert { - padding: 15px 20px; - margin-bottom: 30px; - border-radius: 3px; - border: 1px solid #edbd3c; - border-radius: 3px; - background: #fbf6e1; - // background: #edbd3c; - font-size: 14px; - @include clearfix; - - .alert-message { - float: left; - margin-top: 4px; - } - - strong { - font-weight: 700; - } - - .alert-action { - float: right; - - &.secondary { - @include orange-button; - } - } -} - -body.error { - background: $darkGrey; - color: #3c3c3c; - - .primary-header { - display: none; - } - - .error-prompt { - width: 700px; - margin: 150px auto; - padding: 60px 50px 90px; - border-radius: 3px; - background: #fff; - text-align: center; - } - - h1 { - float: none; - margin: 0; - font-size: 60px; - font-weight: 300; - color: #3c3c3c; - } - - .description { - margin-bottom: 50px; - font-size: 21px; - } - - .back-button { - @include blue-button; - padding: 14px 40px 18px; - font-size: 18px; - } -} \ No newline at end of file diff --git a/cms/static/sass/_assets.scss b/cms/static/sass/_assets.scss deleted file mode 100644 index d9b215faec..0000000000 --- a/cms/static/sass/_assets.scss +++ /dev/null @@ -1,186 +0,0 @@ -.uploads { - input.asset-search-input { - float: left; - width: 260px; - background-color: #fff; - } - - .asset-library { - @include clearfix; - - table { - width: 100%; - border-radius: 3px 3px 0 0; - border: 1px solid #c5cad4; - - td, - th { - padding: 10px 20px; - text-align: left; - vertical-align: middle; - } - - thead th { - @include linear-gradient(top, transparent, rgba(0, 0, 0, .1)); - background-color: #ced2db; - font-size: 12px; - font-weight: 700; - text-shadow: 0 1px 0 rgba(255, 255, 255, .5); - } - - tbody { - background: #fff; - - tr { - border-top: 1px solid #c5cad4; - - &:first-child { - border-top: none; - } - } - - .name-col { - font-size: 14px; - } - - .date-col { - font-size: 12px; - } - } - - .thumb-col { - width: 100px; - } - - .date-col { - width: 220px; - } - - .embed-col { - width: 250px; - } - - .embeddable-xml-input { - @include box-shadow(none); - width: 100%; - } - - .thumb { - width: 100px; - max-height: 80px; - - img { - width: 100%; - } - } - } - - .pagination { - float: right; - margin: 15px 10px; - - ol, li { - display: inline; - } - - a { - display: inline-block; - height: 25px; - padding: 0 4px; - text-align: center; - line-height: 25px; - } - } - } - .show-xml { - @include blue-button; - } -} - -.upload-modal { - display: none; - width: 640px !important; - margin-left: -320px !important; - - .modal-body { - height: auto !important; - overflow-y: auto !important; - text-align: center; - } - - .file-input { - display: none; - } - - .choose-file-button { - @include blue-button; - padding: 10px 82px 12px; - font-size: 17px; - } - - .progress-bar { - display: none; - width: 350px; - height: 50px; - margin: 30px auto 10px; - border: 1px solid $blue; - - &.loaded { - border-color: #66b93d; - - .progress-fill { - background: #66b93d; - } - } - } - - .progress-fill { - width: 0%; - height: 50px; - background: $blue; - color: #fff; - line-height: 48px; - } - - h1 { - float: none; - margin: 40px 0 30px; - font-size: 34px; - font-weight: 300; - } - - .close-button { - @include white-button; - position: absolute; - top: 0; - right: 15px; - width: 29px; - height: 29px; - padding: 0 !important; - border-radius: 17px !important; - line-height: 29px; - text-align: center; - } - - .embeddable { - display: none; - margin: 30px 0 130px; - - label { - display: block; - margin-bottom: 10px; - font-weight: 700; - } - } - - .embeddable-xml-input { - @include box-shadow(none); - width: 400px; - } - - .copy-button { - @include white-button; - display: none; - margin-bottom: 100px; - } -} \ No newline at end of file diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss index 5d4bc7c773..1bf9119654 100644 --- a/cms/static/sass/_base.scss +++ b/cms/static/sass/_base.scss @@ -1,4 +1,4 @@ -// studio base styling +// studio - base styling // ==================== // basic reset @@ -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; 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/_content-types.scss b/cms/static/sass/_content-types.scss deleted file mode 100644 index 7cca469350..0000000000 --- a/cms/static/sass/_content-types.scss +++ /dev/null @@ -1,69 +0,0 @@ -.content-type { - display: inline-block; - width: 14px; - height: 16px; - padding-left: 14px; - background-position: 8px center; - background-repeat: no-repeat; - vertical-align: middle; -} - -.videosequence-icon { - @extend .content-type; - background-image: url('../img/content-types/videosequence.png'); -} - -.video-icon { - @extend .content-type; - background-image: url('../img/content-types/video.png'); -} - -.problemset-icon { - @extend .content-type; - background-image: url('../img/content-types/problemset.png'); -} - -.problem-icon { - @extend .content-type; - background-image: url('../img/content-types/problem.png'); -} - -.lab-icon { - @extend .content-type; - background-image: url('../img/content-types/lab.png'); -} - -.tab-icon { - @extend .content-type; - background-image: url('../img/content-types/lab.png'); -} - -.html-icon { - @extend .content-type; - background-image: url('../img/content-types/html.png'); -} - -.vertical-icon { - @extend .content-type; - background-image: url('../img/content-types/vertical.png'); -} - -.sequential-icon { - @extend .content-type; - background-image: url('../img/content-types/sequential.png'); -} - -.chapter-icon { - @extend .content-type; - background-image: url('../img/content-types/chapter.png'); -} - -.module-icon { - @extend .content-type; - background-image: url('../img/content-types/module.png'); -} - -.module-icon { - @extend .content-type; - background-image: url('../img/content-types/module.png'); -} diff --git a/cms/static/sass/_course-info.scss b/cms/static/sass/_course-info.scss deleted file mode 100644 index 5a2a5a9432..0000000000 --- a/cms/static/sass/_course-info.scss +++ /dev/null @@ -1,218 +0,0 @@ -.course-info { - h2 { - margin-bottom: 24px; - font-size: 22px; - font-weight: 300; - } - - .course-info-wrapper { - display: table; - width: 100%; - clear: both; - } - - .main-column, - .course-handouts { - float: none; - display: table-cell; - } - - .main-column { - border-radius: 3px 0 0 3px; - border-right-color: $mediumGrey; - } - - .CodeMirror { - border: 1px solid #3c3c3c; - background: #fff; - color: #3c3c3c; - } -} - -.course-updates { - padding: 30px 40px; - margin: 0; - - .update-list > li { - padding: 34px 0 42px; - border-top: 1px solid #cbd1db; - - &:first-child { - padding-top: 0; - border: none; - } - - &.editing { - position: relative; - z-index: 1001; - padding: 0; - border-top: none; - border-radius: 3px; - background: #fff; - - .post-preview { - display: none; - } - } - - h1 { - float: none; - font-size: 24px; - font-weight: 300; - } - - h2 { - margin-bottom: 18px; - font-size: 14px; - font-weight: 700; - line-height: 30px; - color: #646464; - letter-spacing: 1px; - text-transform: uppercase; - } - - h3 { - margin: 34px 0 11px; - font-size: 16px; - font-weight: 700; - } - } - - .update-contents { - p { - font-size: 16px; - line-height: 25px; - } - - p + p { - margin-top: 25px; - } - - .primary { - border: 1px solid #ddd; - background: #f6f6f6; - padding: 20px; - } - - ol, ul { - margin: 1em 0; - padding: 0 0 0 1em; - color: $baseFontColor; - - li { - margin-bottom: 0.708em; - } - } - - ol { - list-style: decimal outside none; - } - - ul { - list-style: disc outside none; - } - - pre { - margin: 1em 0; - color: $baseFontColor; - font-family: monospace, serif; - font-size: 1em; - white-space: pre-wrap; - word-wrap: break-word; - } - - code { - color: $baseFontColor; - font-family: monospace, serif; - background: none; - padding: 0; - } - } - - .new-update-button { - @include blue-button; - display: block; - text-align: center; - padding: 18px 0; - margin-bottom: 28px; - } - - .new-update-form { - @include edit-box; - margin-bottom: 24px; - padding: 30px; - border: none; - - textarea { - height: 180px; - } - } - - .post-actions { - float: right; - - .edit-button, - .delete-button{ - float: left; - @include white-button; - padding: 3px 10px 4px; - margin-left: 7px; - font-size: 12px; - font-weight: 400; - - .edit-icon, - .delete-icon { - margin-right: 4px; - } - } - } -} - -.course-handouts { - width: 30%; - padding: 20px 30px; - margin: 0; - border-radius: 0 3px 3px 0; - border-left: none; - background: $lightGrey; - - h2 { - font-size: 18px; - font-weight: 700; - } - - .edit-button { - float: right; - @include white-button; - padding: 3px 10px 4px; - margin-left: 7px; - font-size: 12px; - font-weight: 400; - - .edit-icon, - .delete-icon { - margin-right: 4px; - } - } - - .handouts-content { - font-size: 14px; - } - - .treeview-handoutsnav li { - margin-bottom: 12px; - } -} - -.edit-handouts-form { - @include edit-box; - position: absolute; - right: 0; - z-index: 10001; - width: 800px; - padding: 30px; - - textarea { - height: 300px; - } -} \ No newline at end of file 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/_export.scss b/cms/static/sass/_export.scss deleted file mode 100644 index e1ab7eb605..0000000000 --- a/cms/static/sass/_export.scss +++ /dev/null @@ -1,123 +0,0 @@ -.export { - .export-overview { - @extend .window; - @include clearfix; - padding: 30px 40px; - } - - .description { - float: left; - width: 62%; - margin-right: 3%; - font-size: 14px; - - h2 { - font-weight: 700; - font-size: 19px; - margin-bottom: 20px; - } - - strong { - font-weight: 700; - } - - p + p { - margin-top: 20px; - } - - ul { - margin: 20px 0; - list-style: disc inside; - - li { - margin: 0 0 5px 0; - } - } - } - - .export-form-wrapper { - - .export-form { - float: left; - width: 35%; - padding: 25px 30px 35px; - @include box-sizing(border-box); - border: 1px solid $mediumGrey; - border-radius: 3px; - background: $lightGrey; - text-align: center; - - h2 { - margin-bottom: 30px; - font-size: 26px; - font-weight: 300; - } - - .error-block { - display: none; - margin-bottom: 15px; - font-size: 13px; - } - - .error-block { - color: $error-red; - } - - .button-export { - @include green-button; - padding: 10px 50px 11px; - font-size: 17px; - } - - .message-status { - margin-top: 10px; - font-size: 12px; - } - - .progress-bar { - display: none; - width: 350px; - height: 30px; - margin: 30px auto 10px; - border: 1px solid $blue; - - &.loaded { - border-color: #66b93d; - - .progress-fill { - background: #66b93d; - } - } - } - - .progress-fill { - width: 0%; - height: 30px; - background: $blue; - color: #fff; - line-height: 48px; - } - } - - // downloading state - &.is-downloading { - - .progress-bar { - display: block; - } - - .button-export { - padding: 10px 50px 11px; - font-size: 17px; - - &.disabled { - - pointer-events: none; - cursor: default; - } - } - } - } - - -} \ 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/_fonts.scss b/cms/static/sass/_fonts.scss deleted file mode 100644 index 92a2e185b7..0000000000 --- a/cms/static/sass/_fonts.scss +++ /dev/null @@ -1,36 +0,0 @@ -@font-face { - font-family: 'Open Sans'; - font-style: normal; - font-weight: 700; - src: local('Open Sans Bold'), local('OpenSans-Bold'), url(http://themes.googleusercontent.com/static/fonts/opensans/v6/k3k702ZOKiLJc3WVjuplzKRDOzjiPcYnFooOUGCOsRk.woff) format('woff'); -} -@font-face { - font-family: 'Open Sans'; - font-style: normal; - font-weight: 300; - src: local('Open Sans Light'), local('OpenSans-Light'), url(http://themes.googleusercontent.com/static/fonts/opensans/v6/DXI1ORHCpsQm3Vp6mXoaTaRDOzjiPcYnFooOUGCOsRk.woff) format('woff'); -} -@font-face { - font-family: 'Open Sans'; - font-style: italic; - font-weight: 700; - src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), url(http://themes.googleusercontent.com/static/fonts/opensans/v6/PRmiXeptR36kaC0GEAetxhbnBKKEOwRKgsHDreGcocg.woff) format('woff'); -} -@font-face { - font-family: 'Open Sans'; - font-style: italic; - font-weight: 300; - src: local('Open Sans Light Italic'), local('OpenSansLight-Italic'), url(http://themes.googleusercontent.com/static/fonts/opensans/v6/PRmiXeptR36kaC0GEAetxvR_54zmj3SbGZQh3vCOwvY.woff) format('woff'); -} -@font-face { - font-family: 'Open Sans'; - font-style: italic; - font-weight: 400; - src: local('Open Sans Italic'), local('OpenSans-Italic'), url(http://themes.googleusercontent.com/static/fonts/opensans/v6/xjAJXh38I15wypJXxuGMBrrIa-7acMAeDBVuclsi6Gc.woff) format('woff'); -} -@font-face { - font-family: 'Open Sans'; - font-style: normal; - font-weight: 400; - src: local('Open Sans'), local('OpenSans'), url(http://themes.googleusercontent.com/static/fonts/opensans/v6/cJZKeOuBrn4kERxqtaUH3bO3LdcAZYWl9Si6vvxL-qU.woff) format('woff'); -} diff --git a/cms/static/sass/_footer.scss b/cms/static/sass/_footer.scss deleted file mode 100644 index 66a9ce0e95..0000000000 --- a/cms/static/sass/_footer.scss +++ /dev/null @@ -1,48 +0,0 @@ -//studio global footer -.wrapper-footer { - margin: ($baseline*1.5) 0 $baseline 0; - padding: $baseline; - position: relative; - width: 100%; - - footer.primary { - @include clearfix(); - @include font-size(13); - max-width: $fg-max-width; - min-width: $fg-min-width; - width: flex-grid(12); - margin: 0 auto; - padding-top: $baseline; - border-top: 1px solid $gray-l4; - color: $gray-l2; - - .colophon { - width: flex-grid(4, 12); - float: left; - margin-right: flex-gutter(2); - } - - .nav-peripheral { - width: flex-grid(6, 12); - float: right; - text-align: right; - - .nav-item { - display: inline-block; - margin-right: ($baseline/2); - - &:last-child { - margin-right: 0; - } - } - } - - a { - color: $gray-l1; - - &:hover, &:active { - color: $blue; - } - } - } -} \ No newline at end of file diff --git a/cms/static/sass/_graphics.scss b/cms/static/sass/_graphics.scss deleted file mode 100644 index 300cf3b692..0000000000 --- a/cms/static/sass/_graphics.scss +++ /dev/null @@ -1,336 +0,0 @@ -.expand-collapse-icon { - position: relative; - display: inline-block; - width: 9px; - height: 11px; - margin-right: 10px; - background: url(../img/expand-collapse-icons.png) no-repeat; - @include transition(none); - - &.expand { - top: 1px; - background-position: 0 0; - } - - &.collapse { - top: -1px; - background-position: 0 -11px; - } -} - -.sequence-icon { - display: inline-block; - width: 15px; - height: 9px; - margin-right: 5px; - background: url(../img/sequence-icon.png) no-repeat; -} - -.video-icon { - display: inline-block; - width: 14px; - height: 12px; - margin-right: 5px; - background: url(../img/video-icon.png) no-repeat; -} - -.upload-icon { - display: inline-block; - width: 22px; - height: 13px; - margin-right: 5px; - background: url(../img/upload-icon.png) no-repeat; -} - -.list-icon { - display: inline-block; - width: 14px; - height: 10px; - margin-right: 5px; - background: url(../img/list-icon.png) no-repeat; -} - -.close-icon { - display: inline-block; - width: 13px; - height: 12px; - background: url(../img/close-icon.png) no-repeat; -} - -.home-icon { - display: inline-block; - width: 19px; - height: 16px; - background: url(../img/home-icon.png) no-repeat; -} - -.small-home-icon { - display: inline-block; - width: 16px; - height: 14px; - background: url(../img/small-home-icon.png) no-repeat; -} - -.log-out-icon { - display: inline-block; - width: 15px; - height: 13px; - background: url(../img/log-out-icon.png) no-repeat; -} - -.collapse-all-icon { - display: inline-block; - width: 15px; - height: 9px; - background: url(../img/collapse-all-icon.png) no-repeat; -} - -.calendar-icon { - display: inline-block; - width: 12px; - height: 11px; - margin-right: 5px; - background: url(../img/calendar-icon.png) no-repeat; -} - -.edit-icon { - display: inline-block; - width: 12px; - height: 12px; - margin-right: 2px; - background: url(../img/edit-icon.png) no-repeat; - - &.white { - background: url(../img/edit-icon-white.png) no-repeat; - } -} - -.visibility-toggle { - .toggle-icon { - display: inline-block; - width: 27px; - height: 20px; - background: url(../img/small-toggle-icons.png) no-repeat; - background-position: 0 -34px; - } - - &.hidden .toggle-icon { - background-position: 0 -4px; - } - - &.both .toggle-icon { - background-position: 0 -64px; - } -} - - -.delete-icon { - display: inline-block; - width: 10px; - height: 11px; - margin-right: 2px; - background: url(../img/delete-icon.png) no-repeat; - - &.white { - background: url(../img/delete-icon-white.png) no-repeat; - } -} - -.drag-handle { - display: inline-block; - float: right; - width: 7px; - height: 22px; - margin-left: 10px; - background: url(../img/drag-handles.png) no-repeat; - cursor: move; -} - -.draft-tag, -.public-tag, -.private-tag { - margin-left: 3px; - font-size: 9px; - font-weight: 600; - text-transform: uppercase; - color: #a4aab7; -} - -.draft-tag { - color: #9f7d10; -} - -.plus-icon { - display: inline-block; - width: 11px; - height: 11px; - margin-right: 8px; - background: url(../img/plus-icon.png) no-repeat; - - &.white { - background: url(../img/plus-icon-white.png) no-repeat; - } -} - -.plus-icon-small { - display: inline-block; - width: 6px; - height: 6px; - margin-right: 8px; - background: url(../img/plus-icon-small.png) no-repeat center; -} - -.folder-icon { - display: inline-block; - width: 15px; - height: 11px; - margin-right: 4px; - background: url(../img/folder-icon.png) no-repeat; -} - -.new-folder-icon { - display: inline-block; - width: 23px; - height: 11px; - margin-right: 8px; - background: url(../img/new-folder-icon.png) no-repeat; -} - -.file-icon { - display: inline-block; - width: 10px; - height: 11px; - margin-right: 8px; - background: url(../img/file-icon.png) no-repeat; -} - -.new-unit-icon { - display: inline-block; - width: 23px; - height: 12px; - margin-right: 8px; - background: url(../img/new-unit-icon.png) right no-repeat; -} - -.new-policy-icon { - display: inline-block; - width: 23px; - height: 12px; - margin-right: 8px; - background: url(../img/new-unit-icon.png) right no-repeat; -} - -.textbook-icon { - display: inline-block; - width: 32px; - height: 32px; - margin-right: 8px; - vertical-align: middle; - background: url(../img/textbook-icon.png) no-repeat; -} - -.slides-icon { - display: inline-block; - width: 32px; - height: 32px; - margin-right: 8px; - vertical-align: middle; - background: url(../img/slides-icon.png) no-repeat; -} - -.large-slide-icon { - display: inline-block; - width: 100px; - height: 60px; - margin-right: 5px; - background: url(../img/large-slide-icon.png) center no-repeat; -} - -.large-html-icon { - display: inline-block; - width: 100px; - height: 60px; - margin-right: 5px; - background: url(../img/html-icon.png) center no-repeat; -} - -.large-openended-icon { - display: inline-block; - width: 100px; - height: 60px; - margin-right: 5px; - background: url(../img/large-openended-icon.png) center no-repeat; -} - -.large-annotations-icon { - display: inline-block; - width: 100px; - height: 60px; - margin-right: 5px; - background: url(../img/large-annotations-icon.png) center no-repeat; -} - -.large-advanced-icon { - display: inline-block; - width: 100px; - height: 60px; - margin-right: 5px; - background: url(../img/large-advanced-icon.png) center no-repeat; -} - -.large-textbook-icon { - display: inline-block; - width: 100px; - height: 60px; - margin-right: 5px; - background: url(../img/large-textbook-icon.png) center no-repeat; -} - -.large-discussion-icon { - display: inline-block; - width: 100px; - height: 60px; - margin-right: 5px; - background: url(../img/large-discussion-icon.png) center no-repeat; -} - -.large-freeform-icon { - display: inline-block; - width: 100px; - height: 60px; - margin-right: 5px; - background: url(../img/large-freeform-icon.png) center no-repeat; -} - -.large-problem-icon { - display: inline-block; - width: 100px; - height: 60px; - margin-right: 5px; - background: url(../img/large-problem-icon.png) center no-repeat; -} - -.large-video-icon { - display: inline-block; - width: 100px; - height: 60px; - margin-right: 5px; - background: url(../img/large-video-icon.png) center no-repeat; -} - -.spinner-icon { - display: inline-block; - width: 20px; - height: 20px; - margin-left: 10px; - vertical-align: middle; - background: url(../img/blue-spinner.gif) no-repeat; -} - -.spinner-in-field-icon { - display: inline-block; - width: 14px; - height: 14px; - vertical-align: middle; - background: url(../img/spinner-in-field.gif) no-repeat; -} diff --git a/cms/static/sass/_header.scss b/cms/static/sass/_header.scss deleted file mode 100644 index ca1092f44b..0000000000 --- a/cms/static/sass/_header.scss +++ /dev/null @@ -1,562 +0,0 @@ -// studio global header and navigation -// ==================== - -.wrapper-header { - margin: 0 0 ($baseline*1.5) 0; - padding: $baseline; - border-bottom: 1px solid $gray; - @include box-shadow(0 1px 5px 0 rgba(0,0,0, 0.1)); - background: $white; - height: 76px; - position: relative; - width: 100%; - z-index: 10; - - a { - color: $baseFontColor; - display: block; - - &:hover, &:active { - color: $blue; - } - } - - header.primary { - @include clearfix(); - max-width: $fg-max-width; - min-width: $fg-min-width; - width: flex-grid(12); - margin: 0 auto; - color: $gray-l1; - } - - nav .nav-item { - display: inline-block; - } -} - -// ==================== - -// basic layout -.wrapper-left, .wrapper-right { - @include box-sizing(border-box); -} - -.wrapper-left { - width: flex-grid(10, 12); - float: left; - margin-right: flex-gutter(); -} - -.wrapper-right { - width: flex-grid(2, 12); - float: right; -} - -// ==================== - -// specific elements - branding -.branding, .info-course, .nav-course, .nav-account, .nav-unauth, .nav-pitch { - display: inline-block; - vertical-align: top; -} - -.branding { - position: relative; - margin: 0 ($baseline/2) 0 0; - padding-right: ($baseline*0.75); - - a { - @include text-hide(); - display: block; - width: 164px; - height: 32px; - background: transparent url('../img/logo-edx-studio.png') 0 0 no-repeat; - } -} - -// ==================== - -// specific elements - course name/info -.info-course { - @include font-size(14); - position: relative; - margin: -3px ($baseline/2) 0 0; - padding-right: ($baseline*0.75); - - &:before { - @extend .faded-vertical-divider; - content: ""; - display: block; - height: 50px; - position: absolute; - right: 1px; - top: -8px; - width: 1px; - } - - &:after { - @extend .faded-vertical-divider-light; - content: ""; - display: block; - height: 50px; - position: absolute; - right: 0px; - top: -12px; - width: 1px; - } - - .course-org { - margin-right: ($baseline/4); - } - - .course-number, .course-org { - @include font-size(12); - display: inline-block; - } - - .course-title { - display: block; - width: 100%; - max-width: 220px; - overflow: hidden; - margin-top: -4px; - white-space: nowrap; - text-overflow: ellipsis; - @include font-size(16); - font-weight: 600; - } -} - -// ==================== - -// specific elements - course nav -.nav-course { - width: 335px; - margin-top: -($baseline/4); - @include font-size(14); - - > ol > .nav-item { - vertical-align: bottom; - margin: 0 ($baseline/2) 0 0; - - &:last-child { - margin-right: 0; - } - - .title { - display: block; - padding: 5px; - text-transform: uppercase; - font-weight: 600; - color: $gray-d3; - - .label-prefix { - display: block; - @include font-size(11); - font-weight: 400; - } - } - - // specific nav items - &.nav-course-courseware { - } - - &.nav-course-settings { - } - - &.nav-course-tools { - } - } -} - -// ==================== - -// specific elements - account-based nav -.nav-account { - width: 100%; - margin-top: ($baseline*0.75); - @include font-size(14); - text-align: right; - - .nav-account-username { - width: 100%; - - .icon-user { - display: inline-block; - vertical-align: middle; - margin-right: 3px; - @include font-size(12); - } - - .account-username { - display: inline-block; - vertical-align: middle; - width: 80%; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - - .icon-expand { - display: inline-block; - vertical-align: middle; - } - } -} - -// ==================== - -// UI - dropdown -.nav-dropdown { - - .nav-item { - position: relative; - - .icon-expand { - @include font-size(14); - @include transition (color 0.5s ease-in-out, opacity 0.5s ease-in-out); - display: inline-block; - margin-left: 2px; - opacity: 0.5; - color: $gray-l2; - } - - &:hover { - - .icon-expand { - color: $blue; - opacity: 1.0; - } - } - } - - .wrapper-nav-sub { - position: absolute; - left: -7px; - top: 47px; - width: 140px; - opacity: 0; - pointer-events: none; - } - - .nav-sub { - @include border-radius(2px); - @include box-sizing(border-box); - @include box-shadow(0 1px 5px 0 rgba(0,0,0, 0.1)); - position: relative; - width: 100%; - border: 1px solid $gray-l2; - padding: ($baseline/4) ($baseline/2); - background: $white; - - &:after, &:before { - bottom: 100%; - border: solid transparent; - content: " "; - height: 0; - width: 0; - position: absolute; - pointer-events: none; - } - - &:after { - border-color: rgba(255, 255, 255, 0); - border-bottom-color: #fff; - border-width: 5px; - right: 3px; - margin-left: -5px; - } - - &:before { - border-color: rgba(178, 178, 178, 0); - border-bottom-color: $gray-l2; - border-width: 6px; - right: 3px; - margin-left: -6px; - } - - .nav-item { - display: block; - margin: 0 0 ($baseline/4) 0; - border-bottom: 1px solid $gray-l5; - padding: 0 0($baseline/4) 0; - @include font-size(13); - - &:last-child { - margin-bottom: 0; - border-bottom: none; - padding-bottom: 0; - } - - a { - display: block; - } - } - } - - // UI - dropdown - specific navs - &.nav-account { - - .wrapper-nav-sub { - top: 27px; - left: auto; - right: -13px; - width: 110px; - } - - .nav-sub { - text-align: left; - - .icon-expand { - top: -2px; - } - } - - .nav-sub:after { - left: auto; - right: 11px; - } - - .nav-sub:before { - left: auto; - right: 10px; - } - } - - &.nav-course { - - .nav-course-courseware { - - .nav-sub:after { - left: 88px; - } - - .nav-sub:before { - left: 88px; - } - } - - .nav-course-settings { - - .nav-sub:after { - left: 88px; - } - - .nav-sub:before { - left: 88px; - } - } - - .nav-course-tools { - - .wrapper-nav-sub { - top: ($baseline*1.5); - width: 100px; - } - - .nav-sub:after { - left: 68px; - } - - .nav-sub:before { - left: 68px; - } - } - } -} - -// ==================== - -// STATE: is-signed in -.is-signedin { - - &.course .branding { - - &:before { - @extend .faded-vertical-divider; - content: ""; - display: block; - height: 50px; - position: absolute; - right: 1px; - top: -8px; - width: 1px; - } - - &:after { - @extend .faded-vertical-divider-light; - content: ""; - display: block; - height: 50px; - position: absolute; - right: 0px; - top: -12px; - width: 1px; - } - } -} - -// ==================== - -// STATE: not signed in -.not-signedin { - - .wrapper-left { - width: flex-grid(4, 12); - } - - .wrapper-right { - width: flex-grid(8, 12); - } - - // STATE: not signed in - unauthenticated nav - .nav-not-signedin { - float: right; - margin-top: ($baseline/4); - - .nav-item { - @include font-size(16); - vertical-align: middle; - margin: 0 $baseline 0 0; - - &:last-child { - margin-right: 0; - } - - .action { - margin-top: -($baseline/4); - display: inline-block; - padding: ($baseline/4) ($baseline/2); - } - } - - // STATE: not signed in - specific items - .nav-not-signedin-help { - - } - - .nav-not-signedin-signup { - margin-right: ($baseline/2); - - .action-signup { - @include blue-button; - @include transition(all .15s); - @include font-size(14); - padding: ($baseline/4) ($baseline/2); - text-transform: uppercase; - font-weight: 600; - } - } - - .nav-not-signedin-signin { - - .action-signin { - @include white-button; - @include transition(all .15s); - @include font-size(14); - padding: ($baseline/4) ($baseline/2); - text-transform: uppercase; - font-weight: 600; - } - } - } -} - -// ==================== - -// STATE: active/current nav states - -.nav-item.is-current, -body.howitworks .nav-not-signedin-hiw, - -// dashboard -body.dashboard .nav-account-dashboard, - -// course content -body.course.outline .nav-course-courseware .title, -body.course.updates .nav-course-courseware .title, -body.course.pages .nav-course-courseware .title, -body.course.uploads .nav-course-courseware .title, - -body.course.outline .nav-course-courseware-outline, -body.course.updates .nav-course-courseware-updates, -body.course.pages .nav-course-courseware-pages, -body.course.uploads .nav-course-courseware-uploads, - -// course settings -body.course.schedule .nav-course-settings .title, -body.course.grading .nav-course-settings .title, -body.course.team .nav-course-settings .title, -body.course.advanced .nav-course-settings .title, - -body.course.schedule .nav-course-settings-schedule, -body.course.grading .nav-course-settings-grading, -body.course.team .nav-course-settings-team, -body.course.advanced .nav-course-settings-advanced, - -// course tools -body.course.import .nav-course-tools .title, -body.course.export .nav-course-tools .title, - -body.course.import .nav-course-tools-import, -body.course.export .nav-course-tools-export, - -{ - - color: $blue; - - a { - color: $blue; - pointer-events: none; - } -} - -body.signup .nav-not-signedin-signin { - - a { - background-color: #d9e3ee; - color: #6d788b; - } -} - -body.signin .nav-not-signedin-signup { - - a { - background-color: #62aaf5; - color: #fff; - } -} - -// ==================== - -// STATE: js enabled -.js { - - .nav-dropdown { - - .nav-item .title { - outline: 0; - cursor: pointer; - - &:hover, &:active, &.is-selected { - color: $blue; - - .icon-expand { - color: $blue; - } - } - } - } - - .wrapper-nav-sub { - @include transition (opacity 1.0s ease-in-out 0s); - opacity: 0; - pointer-events: none; - - &.is-shown { - opacity: 1.0; - pointer-events: auto; - } - } -} \ No newline at end of file diff --git a/cms/static/sass/_import.scss b/cms/static/sass/_import.scss deleted file mode 100644 index a0a1f5e512..0000000000 --- a/cms/static/sass/_import.scss +++ /dev/null @@ -1,102 +0,0 @@ -.import { - .import-overview { - @extend .window; - @include clearfix; - padding: 30px 40px; - } - - .description { - float: left; - width: 62%; - margin-right: 3%; - font-size: 14px; - - h2 { - font-weight: 700; - font-size: 19px; - margin-bottom: 20px; - } - - strong { - font-weight: 700; - } - - p + p { - margin-top: 20px; - } - } - - .import-form { - float: left; - width: 35%; - padding: 25px 30px 35px; - @include box-sizing(border-box); - border: 1px solid $mediumGrey; - border-radius: 3px; - background: $lightGrey; - text-align: center; - - h2 { - margin-bottom: 30px; - font-size: 26px; - font-weight: 300; - } - - .file-name-block, - .error-block { - display: none; - margin-bottom: 15px; - font-size: 13px; - } - - .error-block { - color: $error-red; - } - - .choose-file-button { - @include blue-button; - padding: 10px 50px 11px; - font-size: 17px; - } - - .choose-file-button-inline { - display: block; - } - - .file-input { - display: none; - } - - .submit-button { - @include orange-button; - display: none; - max-width: 100%; - padding: 8px 20px 10px; - white-space: normal; - } - } - - .progress-bar { - display: none; - width: 350px; - height: 30px; - margin: 30px auto 10px; - border: 1px solid $blue; - - &.loaded { - border-color: #66b93d; - - .progress-fill { - background: #66b93d; - } - } - } - - .progress-fill { - width: 0%; - height: 30px; - background: $blue; - color: #fff; - line-height: 48px; - } -} \ No newline at end of file diff --git a/cms/static/sass/_index.scss b/cms/static/sass/_index.scss deleted file mode 100644 index e0f6d0f2b7..0000000000 --- a/cms/static/sass/_index.scss +++ /dev/null @@ -1,353 +0,0 @@ -// how it works/not signed in index -.index { - - &.not-signedin { - - .wrapper-header { - margin-bottom: 0; - } - - .wrapper-footer { - margin: 0; - border-top: 2px solid $gray-l3; - - footer.primary { - border: none; - margin-top: 0; - padding-top: 0; - } - } - - .wrapper-content-header, .wrapper-content-features, .wrapper-content-cta { - @include box-sizing(border-box); - margin: 0; - padding: 0 $baseline; - position: relative; - width: 100%; - } - - .content { - @include clearfix(); - @include font-size(16); - max-width: $fg-max-width; - min-width: $fg-min-width; - width: flex-grid(12); - margin: 0 auto; - color: $gray-d2; - - header { - border: none; - padding-bottom: 0; - margin-bottom: 0; - } - - h1, h2, h3, h4, h5, h6 { - color: $gray-d3; - } - - h2 { - - } - - h3 { - - } - - h4 { - - } - } - - // welcome content - .wrapper-content-header { - @include linear-gradient($blue-l1,$blue,$blue-d1); - padding-bottom: ($baseline*4); - padding-top: ($baseline*4); - } - - .content-header { - position: relative; - text-align: center; - color: $white; - - h1 { - @include font-size(52); - float: none; - margin: 0 0 ($baseline/2) 0; - border-bottom: 1px solid $blue-l1; - padding: 0; - font-weight: 500; - color: $white; - } - - .logo { - @include text-hide(); - position: relative; - top: 3px; - display: inline-block; - vertical-align: baseline; - width: 282px; - height: 57px; - background: transparent url('../img/logo-edx-studio-white.png') 0 0 no-repeat; - } - - .tagline { - @include font-size(24); - margin: 0; - color: $blue-l3; - } - } - - .arrow_box { - position: relative; - background: #fff; - border: 4px solid #000; - } - .arrow_box:after, .arrow_box:before { - top: 100%; - border: solid transparent; - content: " "; - height: 0; - width: 0; - position: absolute; - pointer-events: none; - } - - .arrow_box:after { - border-color: rgba(255, 255, 255, 0); - border-top-color: #fff; - border-width: 30px; - left: 50%; - margin-left: -30px; - } - .arrow_box:before { - border-color: rgba(0, 0, 0, 0); - border-top-color: #000; - border-width: 36px; - left: 50%; - margin-left: -36px; - } - - // feature content - .wrapper-content-features { - @include box-shadow(0 -1px ($baseline/4) $shadow); - padding-bottom: ($baseline*2); - padding-top: ($baseline*3); - background: $white; - } - - .content-features { - - .list-features { - - } - - // indiv features - .feature { - @include clearfix(); - margin: 0 0 ($baseline*2) 0; - border-bottom: 1px solid $gray-l4; - padding: 0 0 ($baseline*2) 0; - - .img { - @include box-sizing(border-box); - float: left; - width: flex-grid(3, 12); - margin-right: flex-gutter(); - - a { - @include box-sizing(border-box); - @include box-shadow(0 1px ($baseline/10) $shadow-l1); - position: relative; - top: 0; - display: block; - overflow: hidden; - border: 1px solid $gray-l3; - padding: ($baseline/4); - background: $white; - - .action-zoom { - @include transition(bottom .50s ease-in-out); - position: absolute; - bottom: -30px; - right: ($baseline/2); - opacity: 0; - - .ss-icon { - @include font-size(18); - @include border-top-radius(3px); - display: inline-block; - padding: ($baseline/4) ($baseline/2); - background: $blue; - color: $white; - text-align: center; - } - } - - &:hover { - border-color: $blue; - - .action-zoom { - opacity: 1.0; - bottom: -2px; - } - } - } - - img { - display: block; - width: 100%; - height: 100%; - } - } - - .copy { - float: left; - width: flex-grid(9, 12); - margin-top: -($baseline/4); - - h3 { - margin: 0 0 ($baseline/2) 0; - @include font-size(24); - font-weight: 600; - } - - > p { - @include font-size(18); - color: $gray-d1; - } - - strong { - color: $gray-d2; - font-weight: 500; - } - - .list-proofpoints { - @include clearfix(); - @include font-size(14); - width: flex-grid(9, 9); - margin: ($baseline*1.5) 0 0 0; - - .proofpoint { - @include box-sizing(border-box); - @include border-radius(($baseline/4)); - @include transition(color .50s ease-in-out); - position: relative; - top: 0; - float: left; - width: flex-grid(3, 9); - min-height: ($baseline*8); - margin-right: flex-gutter(); - padding: ($baseline*0.75) $baseline; - color: $gray-l1; - - .title { - @include font-size(16); - margin: 0 0 ($baseline/4) 0; - font-weight: 500; - color: $gray-d3; - } - - &:hover { - @include box-shadow(0 1px ($baseline/10) $shadow-l1); - background: $blue-l5; - top: -($baseline/5); - - .title { - color: $blue; - } - } - - &:last-child { - margin-right: 0; - } - } - } - } - - - &:last-child { - margin-bottom: 0; - border: none; - padding-bottom: 0; - } - - &:nth-child(even) { - - .img { - float: right; - margin-right: 0; - margin-left: flex-gutter(); - } - - .copy { - float: right; - text-align: right; - } - - .list-proofpoints { - - .proofpoint { - float: right; - width: flex-grid(3, 9); - margin-left: flex-gutter(); - margin-right: 0; - - &:last-child { - margin-left: 0; - } - } - } - } - } - } - - // call to action content - .wrapper-content-cta { - padding-bottom: ($baseline*2); - padding-top: ($baseline*2); - background: $white; - } - - .content-cta { - border-top: 1px solid $gray-l4; - - header { - border: none; - margin: 0; - padding: 0; - } - - .list-actions { - position: relative; - margin-top: -($baseline*1.5); - - li { - width: flex-grid(6, 12); - margin: 0 auto; - } - - .action { - display: block; - width: 100%; - text-align: center; - } - - .action-primary { - @include blue-button; - @include transition(all .15s); - @include font-size(18); - padding: ($baseline*0.75) ($baseline/2); - font-weight: 600; - text-align: center; - text-transform: uppercase; - } - - .action-secondary { - @include font-size(14); - margin-top: ($baseline/2); - } - } - } - } -} \ No newline at end of file diff --git a/cms/static/sass/_jquery-ui-calendar.scss b/cms/static/sass/_jquery-ui-calendar.scss deleted file mode 100644 index 96cffc059f..0000000000 --- a/cms/static/sass/_jquery-ui-calendar.scss +++ /dev/null @@ -1,53 +0,0 @@ -.ui-datepicker { - border-color: $darkGrey; - border-radius: 2px; - background: #fff; - font-family: $sans-serif; - font-size: 12px; - @include box-shadow(0 5px 10px rgba(0, 0, 0, 0.1)); - - .ui-widget-header { - background: $darkGrey; - border: none; - border-radius: 2px; - } - - .ui-datepicker-next, - .ui-datepicker-prev { - @include transition(none); - - &.ui-state-hover { - border-color: transparent; - background: $mediumGrey; - - .ui-icon-circle-triangle-e, - .ui-icon-circle-triangle-w { - background-image: url(../css/vendor/ui-lightness/images/ui-icons_ffffff_256x240.png); - } - } - } - - .ui-state-default { - border-color: $mediumGrey; - color: $blue; - @include transition(none); - - &.ui-state-hover { - background: $orange; - border-color: $orange; - color: #fff; - } - } - - .ui-state-highlight { - background: $blue; - border-color: $blue; - color: #fff; - } - - .ui-state-active { - background: $orange; - border-color: $orange; - color: #fff; - } -} \ No newline at end of file diff --git a/cms/static/sass/_keyframes.scss b/cms/static/sass/_keyframes.scss deleted file mode 100644 index 7661f18980..0000000000 --- a/cms/static/sass/_keyframes.scss +++ /dev/null @@ -1,27 +0,0 @@ -@mixin bounce-in { - 0% { - opacity: 0; - @include transform(scale(.3)); - } - - 50% { - opacity: 1; - @include transform(scale(1.05)); - } - - 100% { - @include transform(scale(1)); - } -} - -@-moz-keyframes bounce-in { @include bounce-in(); } -@-webkit-keyframes bounce-in { @include bounce-in(); } -@-o-keyframes bounce-in { @include bounce-in(); } -@keyframes bounce-in { @include bounce-in();} - -@mixin bounce-in-animation($duration, $timing: ease-in-out) { - @include animation-name(bounce-in); - @include animation-duration($duration); - @include animation-timing-function($timing); - @include animation-fill-mode(both); -} 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/_modal.scss b/cms/static/sass/_modal.scss deleted file mode 100644 index f9fbf81a8f..0000000000 --- a/cms/static/sass/_modal.scss +++ /dev/null @@ -1,69 +0,0 @@ -.modal-cover { - display: none; - position: fixed; - top: 0; - left: 0; - z-index: 1000; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, .8); -} - -.modal { - display: none; - position: fixed; - top: 60px; - left: 50%; - z-index: 1001; - width: 930px; - height: 540px; - margin-left: -465px; - background: #fff; - - .modal-body { - height: 400px; - padding: 40px; - overflow-y: scroll; - } - - .modal-actions { - height: 60px; - @include linear-gradient(top, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0)); - background-color: #d1dae3; - } - - h2 { - margin: 0 10px 30px; - color: #646464; - font-size: 19px; - font-weight: 300; - letter-spacing: 1px; - text-transform: uppercase; - } - - p { - margin: 20px; - } - - .revert-button { - @include blue-button; - margin: 13px 6px 0 13px; - } - - .close-button { - @include white-button; - margin-top: 13px; - } -} - -// lean modal alternative -#lean_overlay { - position: fixed; - z-index: 10000; - top: 0px; - left: 0px; - display: none; - height: 100%; - width: 100%; - background: $black; -} \ 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..8a7e7e9835 100644 --- a/cms/static/sass/_reset.scss +++ b/cms/static/sass/_reset.scss @@ -1,3 +1,6 @@ +// studio - utilities - reset +// ==================== + html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, 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/_settings.scss b/cms/static/sass/_settings.scss deleted file mode 100644 index d8011dd651..0000000000 --- a/cms/static/sass/_settings.scss +++ /dev/null @@ -1,740 +0,0 @@ -// Studio - Course Settings -// ==================== -body.course.settings { - - .content-primary, .content-supplementary { - @include box-sizing(border-box); - float: left; - } - - .content-primary { - @extend .window; - width: flex-grid(9, 12); - margin-right: flex-gutter(); - padding: $baseline ($baseline*1.5); - } - - // messages - should be synced up with global messages in the future - .message { - display: block; - font-size: 14px; - } - - .message-status { - display: none; - @include border-top-radius(2px); - @include box-sizing(border-box); - border-bottom: 2px solid $yellow; - margin: 0 0 20px 0; - padding: 10px 20px; - font-weight: 500; - background: $paleYellow; - - .text { - display: inline-block; - } - - &.error { - border-color: shade($red, 50%); - background: tint($red, 20%); - color: $white; - } - - &.confirm { - border-color: shade($green, 50%); - background: tint($green, 20%); - color: $white; - } - - &.is-shown { - display: block; - } - } - - // in form - elements - .group-settings { - margin: 0 0 ($baseline*2) 0; - - header { - @include clearfix(); - - .title-2 { - width: flex-grid(4, 9); - margin: 0 flex-gutter() 0 0; - float: left; - } - - .tip { - @include font-size(13); - width: flex-grid(5, 9); - float: right; - margin-top: ($baseline/2); - text-align: right; - color: $gray-l2; - } - } - - // basic layout/elements - .title-2 { - - } - - .title-3 { - - } - - // in form -UI hints/tips/messages - .instructions { - @include font-size(14); - margin: 0 0 $baseline 0; - } - - .tip { - @include transition(color, 0.15s, ease-in-out); - @include font-size(13); - display: block; - margin-top: ($baseline/4); - color: $gray-l3; - } - - .message-error { - @include font-size(13); - display: block; - margin-top: ($baseline/4); - margin-bottom: ($baseline/2); - color: $red; - } - - // buttons - .remove-item { - @include white-button; - @include font-size(13); - font-weight: 400; - } - - .new-button { - @include font-size(13); - } - - // form basics - .list-input { - margin: 0; - padding: 0; - list-style: none; - - .field { - margin: 0 0 $baseline 0; - - &:last-child { - margin-bottom: 0; - } - - &.required { - - label { - font-weight: 600; - } - - label:after { - margin-left: ($baseline/4); - content: "*"; - } - } - - label, input, textarea { - display: block; - } - - label { - @include font-size(14); - @include transition(color, 0.15s, ease-in-out); - margin: 0 0 ($baseline/4) 0; - font-weight: 400; - - &.is-focused { - color: $blue; - } - } - - input, textarea { - @include placeholder($gray-l4); - @include font-size(16); - @include size(100%,100%); - padding: ($baseline/2); - - &.long { - } - - &.short { - } - - &.error { - border-color: $red; - } - - &:focus { - - + .tip { - color: $gray; - } - } - } - - textarea.long { - height: ($baseline*5); - } - - input[type="checkbox"] { - display: inline-block; - margin-right: ($baseline/4); - width: auto; - height: auto; - - & + label { - display: inline-block; - } - } - } - - .field-group { - @include clearfix(); - margin: 0 0 ($baseline/2) 0; - } - - // enumerated/grouped lists - &.enum { - - .field-group { - @include box-sizing(border-box); - @include border-radius(3px); - background: $gray-l5; - padding: $baseline; - - &:last-child { - padding-bottom: $baseline; - } - - .actions { - @include clearfix(); - margin-top: ($baseline/2); - border-top: 1px solid $gray-l4; - padding-top: ($baseline/2); - - .remove-item { - float: right; - } - } - } - } - } - - // existing inputs - .input-existing { - margin: 0 0 $baseline 0; - - .actions { - margin: ($baseline/4) 0 0 0; - } - } - - // not editable fields - .field.is-not-editable { - - label, .label { - color: $gray-l3; - } - - input { - opacity: 0.25; - } - } - - // field with error - .field.error { - - input, textarea { - border-color: $red; - } - } - - // specific fields - basic - &.basic { - - .list-input { - @include clearfix(); - - .field { - margin-bottom: 0; - } - } - - #field-course-organization { - float: left; - width: flex-grid(2, 9); - margin-right: flex-gutter(); - } - - #field-course-number { - float: left; - width: flex-grid(2, 9); - margin-right: flex-gutter(); - } - - #field-course-name { - float: left; - width: flex-grid(5, 9); - } - } - - // specific fields - schedule - &.schedule { - - .list-input { - margin-bottom: ($baseline*1.5); - - &:last-child { - margin-bottom: 0; - } - } - - .field-group { - @include clearfix(); - border-bottom: 1px solid $gray-l5; - padding-bottom: ($baseline/2); - - &:last-child { - border: none; - padding-bottom: 0; - } - - .field { - float: left; - width: flex-grid(3, 9); - margin-bottom: ($baseline/4); - margin-right: flex-gutter(); - } - - .field.time { - position: relative; - - .tip { - position: absolute; - top: 0; - right: 0; - } - } - } - } - - // specific fields - overview - #field-course-overview { - - #course-overview { - height: ($baseline*20); - } - } - - // specific fields - video - #field-course-introduction-video { - - .input-existing { - @include box-sizing(border-box); - @include border-radius(3px); - background: $gray-l5; - padding: ($baseline/2); - - .actions { - @include clearfix(); - margin-top: ($baseline/2); - border-top: 1px solid $gray-l4; - padding-top: ($baseline/2); - - .remove-item { - float: right; - } - } - } - - .actions { - margin-top: ($baseline/2); - border-top: 1px solid $gray-l5; - padding-top: ($baseline/2); - } - } - - // specific fields - requirements - &.requirements { - - #field-course-effort { - width: flex-grid(3, 9); - } - } - - // specific fields - grading range (artifact styling) - &.grade-range { - margin-bottom: ($baseline*3); - - .grade-controls { - @include clearfix; - width: flex-grid(9,9); - } - - .new-grade-button { - @include box-sizing(border-box); - @include linear-gradient(top, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0)); - @include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset); - width: flex-grid(1,9); - height: ($baseline*2); - position: relative; - display: inline-block; - vertical-align: middle; - margin-right: flex-gutter(); - border-radius: 20px; - border: 1px solid $darkGrey; - background-color: #d1dae3; - color: #6d788b; - - .plus-icon { - position: absolute; - top: 50%; - left: 50%; - margin-left: -6px; - margin-top: -6px; - } - } - - .grade-slider { - @include box-sizing(border-box); - width: flex-grid(8,9); - display: inline-block; - vertical-align: middle; - - .grade-bar { - position: relative; - width: 100%; - height: ($baseline*2.5); - background: $lightGrey; - - .increments { - position: relative; - - li { - position: absolute; - top: 52px; - width: 30px; - margin-left: -15px; - font-size: 9px; - text-align: center; - - &.increment-0 { - left: 0; - } - - &.increment-10 { - left: 10%; - } - - &.increment-20 { - left: 20%; - } - - &.increment-30 { - left: 30%; - } - - &.increment-40 { - left: 40%; - } - - &.increment-50 { - left: 50%; - } - - &.increment-60 { - left: 60%; - } - - &.increment-70 { - left: 70%; - } - - &.increment-80 { - left: 80%; - } - - &.increment-90 { - left: 90%; - } - - &.increment-100 { - left: 100%; - } - } - } - - .grade-specific-bar { - height: 50px !important; - } - - .grades { - position: relative; - - li { - position: absolute; - top: 0; - height: 50px; - text-align: right; - @include border-radius(2px); - - &:hover, - &.is-dragging { - .remove-button { - display: block; - } - } - - &.is-dragging { - - } - - .remove-button { - display: none; - position: absolute; - top: -17px; - right: 1px; - height: 17px; - font-size: 10px; - } - - &:nth-child(1) { - background: #4fe696; - } - - &:nth-child(2) { - background: #ffdf7e; - } - - &:nth-child(3) { - background: #ffb657; - } - - &:nth-child(4) { - background: #ef54a1; - } - - &:nth-child(5), - &.bar-fail { - background: #fb336c; - } - - .letter-grade { - display: block; - margin: 10px 15px 0 0; - font-size: 16px; - font-weight: 700; - line-height: 14px; - } - - .range { - display: block; - margin-right: 15px; - font-size: 10px; - line-height: 12px; - } - - .drag-bar { - position: absolute; - top: 0; - right: -1px; - height: 50px; - width: 2px; - background-color: #fff; - @include box-shadow(-1px 0 3px rgba(0,0,0,0.1)); - - cursor: ew-resize; - @include transition(none); - - &:hover { - width: 6px; - right: -2px; - } - } - } - } - } - } - } - - // specific fields - grading rules - &.grade-rules { - - #field-course-grading-graceperiod { - width: flex-grid(3, 9); - } - } - - &.assignment-types { - - .list-input { - - &:last-child { - margin-bottom: 0; - } - } - - .field-group { - @include clearfix(); - width: flex-grid(9, 9); - margin-bottom: ($baseline*1.5); - border-bottom: 1px solid $gray-l5; - padding-bottom: ($baseline*1.5); - - &:last-child { - border: none; - padding-bottom: 0; - } - - .field { - display: inline-block; - vertical-align: top; - width: flex-grid(3, 6); - margin-bottom: ($baseline/2); - margin-right: flex-gutter(); - } - - #field-course-grading-assignment-shortname, - #field-course-grading-assignment-totalassignments, - #field-course-grading-assignment-gradeweight, - #field-course-grading-assignment-droppable { - width: flex-grid(2, 6); - } - } - - .actions { - float: left; - width: flex-grid(9, 9); - - .delete-button { - margin: 0; - } - } - } - - // specific fields - advanced settings - &.advanced-policies { - - .field-group { - margin-bottom: ($baseline*1.5); - - &:last-child { - border: none; - padding-bottom: 0; - } - } - - .course-advanced-policy-list-item { - @include clearfix(); - position: relative; - - .field { - - input { - width: 100%; - } - - .tip { - @include transition (opacity 0.5s ease-in-out 0s); - opacity: 0; - position: absolute; - bottom: ($baseline*1.25); - } - - input:focus { - - & + .tip { - opacity: 1.0; - } - } - - input.error { - - & + .tip { - opacity: 0; - } - } - } - - .key, .value { - float: left; - margin: 0 0 ($baseline/2) 0; - } - - .key { - width: flex-grid(3, 9); - margin-right: flex-gutter(); - } - - .value { - width: flex-grid(6, 9); - } - - .actions { - float: left; - width: flex-grid(9, 9); - - .delete-button { - margin: 0; - } - } - } - - .message-error { - position: absolute; - bottom: ($baseline*0.75); - } - - // specific to code mirror instance in JSON policy editing, need to sync up with other similar code mirror UIs - .CodeMirror { - @include font-size(16); - @include box-sizing(border-box); - @include box-shadow(0 1px 2px rgba(0, 0, 0, .1) inset); - @include linear-gradient($lightGrey, tint($lightGrey, 90%)); - padding: 5px 8px; - border: 1px solid $mediumGrey; - border-radius: 2px; - background-color: $lightGrey; - font-family: 'Open Sans', sans-serif; - color: $baseFontColor; - outline: 0; - - &.CodeMirror-focused { - @include linear-gradient($paleYellow, tint($paleYellow, 90%)); - outline: 0; - } - - .CodeMirror-scroll { - overflow: hidden; - height: auto; - min-height: ($baseline*1.5); - max-height: ($baseline*10); - } - - // editor color changes just for JSON - .CodeMirror-lines { - - .cm-string { - color: #cb9c40; - } - - pre { - line-height: 2.0rem; - } - } - } - } - } - - .content-supplementary { - width: flex-grid(3, 12); - } -} \ No newline at end of file diff --git a/cms/static/sass/_static-pages.scss b/cms/static/sass/_static-pages.scss deleted file mode 100644 index 138e817769..0000000000 --- a/cms/static/sass/_static-pages.scss +++ /dev/null @@ -1,153 +0,0 @@ -.static-pages { - .new-static-page-button { - @include grey-button; - display: block; - text-align: center; - padding: 12px 0; - } - - .unit-body { - padding: 0; - - .details { - display: block !important; - - h2 { - margin: 0 0 5px 0; - } - } - } - - .component-editor { - border: none; - border-radius: 0; - } - - .components > li { - margin: 0; - border-radius: 0; - - &.new-component-item { - background: transparent; - border: none; - @include box-shadow(none); - } - } - - .component { - border: 1px solid $mediumGrey; - border-top: none; - - &:first-child { - border-top: 1px solid $mediumGrey; - } - - &:hover { - border: 1px solid $mediumGrey; - border-top: none; - - &:first-child { - border-top: 1px solid $mediumGrey; - } - - .drag-handle { - background: url(../img/drag-handles.png) center no-repeat #fff; - } - } - - .drag-handle { - top: 0; - right: 0; - z-index: 11; - width: 35px; - border: none; - background: url(../img/drag-handles.png) center no-repeat #fff; - - &:hover { - background: url(../img/drag-handles.png) center no-repeat #fff; - } - } - - .component-actions { - top: 26px; - right: 44px; - } - } - - .component.editing { - border-left: 1px solid $mediumGrey; - border-right: 1px solid $mediumGrey; - - .xmodule_display { - display: none; - } - } - - .new .xmodule_display { - background: $yellow; - } - - .xmodule_display { - padding: 20px 20px 22px; - font-size: 24px; - font-weight: 300; - background: #fff; - @include transition(background-color 3s); - } - - .static-page-item { - position: relative; - margin: 10px 0; - padding: 22px 20px; - border: 1px solid $darkGrey; - border-radius: 3px; - background: #fff; - @include box-shadow(0 1px 2px rgba(0, 0, 0, .1)); - - .page-name { - font-size: 19px; - font-weight: 700; - } - - .item-actions { - margin-top: 19px; - margin-right: 12px; - } - } -} - -.edit-static-page { - .main-wrapper { - margin-top: 40px; - } - - .static-page-details { - @extend .window; - padding: 32px 40px; - - .row { - border: none; - } - } - - .page-display-name-input { - width: 100%; - font-size: 20px; - } - - .page-contents { - @include box-sizing(border-box); - width: 100%; - height: 360px; - padding: 15px; - border: 1px solid #b0b6c2; - border-radius: 2px; - @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); - font-family: Monaco, monospace; - font-size: 13px; - color: #3c3c3c; - outline: 0; - } -} \ No newline at end of file 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/_users.scss b/cms/static/sass/_users.scss deleted file mode 100644 index e107bdbb6d..0000000000 --- a/cms/static/sass/_users.scss +++ /dev/null @@ -1,78 +0,0 @@ -.users { - .new-user-form { - display: none; - padding: 15px 20px; - background-color: $lightBluishGrey2; - - #result { - display: none; - float: left; - margin-bottom: 15px; - padding: 3px 15px; - border-radius: 3px; - background: $error-red; - font-size: 14px; - color: #fff; - } - - .form-elements { - clear: both; - } - - label { - display: inline-block; - margin-right: 10px; - } - - .email-input { - width: 350px; - padding: 8px 8px 10px; - border-color: $darkGrey; - } - - .add-button { - @include blue-button; - padding: 5px 20px 9px; - } - - .cancel-button { - @include white-button; - padding: 5px 20px 9px; - } - } - - .user-list { - border: 1px solid $mediumGrey; - background: #fff; - - li { - position: relative; - padding: 20px; - border-bottom: 1px solid $mediumGrey; - - &:last-child { - border-bottom: none; - } - - span { - display: inline-block; - } - - .user-name { - margin-right: 10px; - font-size: 24px; - font-weight: 300; - } - - .user-email { - font-size: 14px; - font-style: italic; - color: $mediumGrey; - } - - .item-actions { - top: 24px; - } - } - } -} \ No newline at end of file diff --git a/cms/static/sass/_variables.scss b/cms/static/sass/_variables.scss index 4d8e26b2f9..78b6f2b221 100644 --- a/cms/static/sass/_variables.scss +++ b/cms/static/sass/_variables.scss @@ -1,3 +1,6 @@ +// studio - utilities - variables +// ==================== + $baseline: 20px; // grid 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/base-style.scss b/cms/static/sass/base-style.scss index dceac4233d..33b312d235 100644 --- a/cms/static/sass/base-style.scss +++ b/cms/static/sass/base-style.scss @@ -1,39 +1,49 @@ +// studio - css architecture +// ==================== + +// bourbon libs and resets @import 'bourbon/bourbon'; @import 'bourbon/addons/button'; @import 'vendor/normalize'; -@import 'keyframes'; - @import 'reset'; -@import 'mixins'; -@import "fonts"; +// utilities +@import 'mixins'; @import "variables"; @import "cms_mixins"; -@import "extends"; + +// assets +@import "assets/fonts"; +@import "assets/graphics"; +@import 'assets/keyframes'; + +// base @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'; -@import 'content-types'; +// elements +@import "elements/header"; +@import "elements/footer"; +@import "elements/navigation"; +@import "elements/modal"; +@import "elements/alerts"; +@import 'elements/jquery-ui-calendar'; -@import 'module/module-styles.scss'; -@import 'descriptor/module-styles.scss'; +// 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'; + +@import 'xmodule/module/module-styles.scss'; +@import 'xmodule/descriptor/module-styles.scss'; diff --git a/common/static/sass/_mixins.scss b/common/static/sass/_mixins.scss index 76d52ed930..3145745906 100644 --- a/common/static/sass/_mixins.scss +++ b/common/static/sass/_mixins.scss @@ -1,3 +1,6 @@ +// all - utilities - mixins and extends +// ==================== + // font-sizing @function em($pxval, $base: 16) { @return #{$pxval / $base}em; @@ -64,4 +67,88 @@ :-ms-input-placeholder { color: $color; } -} \ No newline at end of file +} + +// ==================== + +// 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 \ No newline at end of file From 6418d033ddfa5514baa2b1216555dbc5d2b62b1d Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Fri, 8 Mar 2013 12:20:40 -0500 Subject: [PATCH 009/436] studio - adding files I forgot to add when cleaning up Sass and originally committing --- cms/static/sass/assets/_content-types.scss | 69 ++ cms/static/sass/assets/_fonts.scss | 36 + cms/static/sass/assets/_graphics.scss | 336 ++++++++ cms/static/sass/assets/_keyframes.scss | 27 + cms/static/sass/elements/_alerts.scss | 165 ++++ cms/static/sass/elements/_footer.scss | 50 ++ cms/static/sass/elements/_header.scss | 558 +++++++++++++ .../sass/elements/_jquery-ui-calendar.scss | 56 ++ cms/static/sass/elements/_modal.scss | 72 ++ cms/static/sass/elements/_navigation.scss | 31 + cms/static/sass/views/_account.scss | 295 +++++++ cms/static/sass/views/_assets.scss | 189 +++++ cms/static/sass/views/_dashboard.scss | 117 +++ cms/static/sass/views/_export.scss | 126 +++ cms/static/sass/views/_import.scss | 105 +++ cms/static/sass/views/_index.scss | 355 +++++++++ cms/static/sass/views/_outline.scss | 691 ++++++++++++++++ cms/static/sass/views/_settings.scss | 741 ++++++++++++++++++ cms/static/sass/views/_static-pages.scss | 156 ++++ cms/static/sass/views/_subsection.scss | 298 +++++++ cms/static/sass/views/_unit.scss | 670 ++++++++++++++++ cms/static/sass/views/_updates.scss | 221 ++++++ cms/static/sass/views/_users.scss | 81 ++ 23 files changed, 5445 insertions(+) create mode 100644 cms/static/sass/assets/_content-types.scss create mode 100644 cms/static/sass/assets/_fonts.scss create mode 100644 cms/static/sass/assets/_graphics.scss create mode 100644 cms/static/sass/assets/_keyframes.scss create mode 100644 cms/static/sass/elements/_alerts.scss create mode 100644 cms/static/sass/elements/_footer.scss create mode 100644 cms/static/sass/elements/_header.scss create mode 100644 cms/static/sass/elements/_jquery-ui-calendar.scss create mode 100644 cms/static/sass/elements/_modal.scss create mode 100644 cms/static/sass/elements/_navigation.scss create mode 100644 cms/static/sass/views/_account.scss create mode 100644 cms/static/sass/views/_assets.scss create mode 100644 cms/static/sass/views/_dashboard.scss create mode 100644 cms/static/sass/views/_export.scss create mode 100644 cms/static/sass/views/_import.scss create mode 100644 cms/static/sass/views/_index.scss create mode 100644 cms/static/sass/views/_outline.scss create mode 100644 cms/static/sass/views/_settings.scss create mode 100644 cms/static/sass/views/_static-pages.scss create mode 100644 cms/static/sass/views/_subsection.scss create mode 100644 cms/static/sass/views/_unit.scss create mode 100644 cms/static/sass/views/_updates.scss create mode 100644 cms/static/sass/views/_users.scss diff --git a/cms/static/sass/assets/_content-types.scss b/cms/static/sass/assets/_content-types.scss new file mode 100644 index 0000000000..7cca469350 --- /dev/null +++ b/cms/static/sass/assets/_content-types.scss @@ -0,0 +1,69 @@ +.content-type { + display: inline-block; + width: 14px; + height: 16px; + padding-left: 14px; + background-position: 8px center; + background-repeat: no-repeat; + vertical-align: middle; +} + +.videosequence-icon { + @extend .content-type; + background-image: url('../img/content-types/videosequence.png'); +} + +.video-icon { + @extend .content-type; + background-image: url('../img/content-types/video.png'); +} + +.problemset-icon { + @extend .content-type; + background-image: url('../img/content-types/problemset.png'); +} + +.problem-icon { + @extend .content-type; + background-image: url('../img/content-types/problem.png'); +} + +.lab-icon { + @extend .content-type; + background-image: url('../img/content-types/lab.png'); +} + +.tab-icon { + @extend .content-type; + background-image: url('../img/content-types/lab.png'); +} + +.html-icon { + @extend .content-type; + background-image: url('../img/content-types/html.png'); +} + +.vertical-icon { + @extend .content-type; + background-image: url('../img/content-types/vertical.png'); +} + +.sequential-icon { + @extend .content-type; + background-image: url('../img/content-types/sequential.png'); +} + +.chapter-icon { + @extend .content-type; + background-image: url('../img/content-types/chapter.png'); +} + +.module-icon { + @extend .content-type; + background-image: url('../img/content-types/module.png'); +} + +.module-icon { + @extend .content-type; + background-image: url('../img/content-types/module.png'); +} diff --git a/cms/static/sass/assets/_fonts.scss b/cms/static/sass/assets/_fonts.scss new file mode 100644 index 0000000000..92a2e185b7 --- /dev/null +++ b/cms/static/sass/assets/_fonts.scss @@ -0,0 +1,36 @@ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 700; + src: local('Open Sans Bold'), local('OpenSans-Bold'), url(http://themes.googleusercontent.com/static/fonts/opensans/v6/k3k702ZOKiLJc3WVjuplzKRDOzjiPcYnFooOUGCOsRk.woff) format('woff'); +} +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 300; + src: local('Open Sans Light'), local('OpenSans-Light'), url(http://themes.googleusercontent.com/static/fonts/opensans/v6/DXI1ORHCpsQm3Vp6mXoaTaRDOzjiPcYnFooOUGCOsRk.woff) format('woff'); +} +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 700; + src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), url(http://themes.googleusercontent.com/static/fonts/opensans/v6/PRmiXeptR36kaC0GEAetxhbnBKKEOwRKgsHDreGcocg.woff) format('woff'); +} +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 300; + src: local('Open Sans Light Italic'), local('OpenSansLight-Italic'), url(http://themes.googleusercontent.com/static/fonts/opensans/v6/PRmiXeptR36kaC0GEAetxvR_54zmj3SbGZQh3vCOwvY.woff) format('woff'); +} +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 400; + src: local('Open Sans Italic'), local('OpenSans-Italic'), url(http://themes.googleusercontent.com/static/fonts/opensans/v6/xjAJXh38I15wypJXxuGMBrrIa-7acMAeDBVuclsi6Gc.woff) format('woff'); +} +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 400; + src: local('Open Sans'), local('OpenSans'), url(http://themes.googleusercontent.com/static/fonts/opensans/v6/cJZKeOuBrn4kERxqtaUH3bO3LdcAZYWl9Si6vvxL-qU.woff) format('woff'); +} diff --git a/cms/static/sass/assets/_graphics.scss b/cms/static/sass/assets/_graphics.scss new file mode 100644 index 0000000000..300cf3b692 --- /dev/null +++ b/cms/static/sass/assets/_graphics.scss @@ -0,0 +1,336 @@ +.expand-collapse-icon { + position: relative; + display: inline-block; + width: 9px; + height: 11px; + margin-right: 10px; + background: url(../img/expand-collapse-icons.png) no-repeat; + @include transition(none); + + &.expand { + top: 1px; + background-position: 0 0; + } + + &.collapse { + top: -1px; + background-position: 0 -11px; + } +} + +.sequence-icon { + display: inline-block; + width: 15px; + height: 9px; + margin-right: 5px; + background: url(../img/sequence-icon.png) no-repeat; +} + +.video-icon { + display: inline-block; + width: 14px; + height: 12px; + margin-right: 5px; + background: url(../img/video-icon.png) no-repeat; +} + +.upload-icon { + display: inline-block; + width: 22px; + height: 13px; + margin-right: 5px; + background: url(../img/upload-icon.png) no-repeat; +} + +.list-icon { + display: inline-block; + width: 14px; + height: 10px; + margin-right: 5px; + background: url(../img/list-icon.png) no-repeat; +} + +.close-icon { + display: inline-block; + width: 13px; + height: 12px; + background: url(../img/close-icon.png) no-repeat; +} + +.home-icon { + display: inline-block; + width: 19px; + height: 16px; + background: url(../img/home-icon.png) no-repeat; +} + +.small-home-icon { + display: inline-block; + width: 16px; + height: 14px; + background: url(../img/small-home-icon.png) no-repeat; +} + +.log-out-icon { + display: inline-block; + width: 15px; + height: 13px; + background: url(../img/log-out-icon.png) no-repeat; +} + +.collapse-all-icon { + display: inline-block; + width: 15px; + height: 9px; + background: url(../img/collapse-all-icon.png) no-repeat; +} + +.calendar-icon { + display: inline-block; + width: 12px; + height: 11px; + margin-right: 5px; + background: url(../img/calendar-icon.png) no-repeat; +} + +.edit-icon { + display: inline-block; + width: 12px; + height: 12px; + margin-right: 2px; + background: url(../img/edit-icon.png) no-repeat; + + &.white { + background: url(../img/edit-icon-white.png) no-repeat; + } +} + +.visibility-toggle { + .toggle-icon { + display: inline-block; + width: 27px; + height: 20px; + background: url(../img/small-toggle-icons.png) no-repeat; + background-position: 0 -34px; + } + + &.hidden .toggle-icon { + background-position: 0 -4px; + } + + &.both .toggle-icon { + background-position: 0 -64px; + } +} + + +.delete-icon { + display: inline-block; + width: 10px; + height: 11px; + margin-right: 2px; + background: url(../img/delete-icon.png) no-repeat; + + &.white { + background: url(../img/delete-icon-white.png) no-repeat; + } +} + +.drag-handle { + display: inline-block; + float: right; + width: 7px; + height: 22px; + margin-left: 10px; + background: url(../img/drag-handles.png) no-repeat; + cursor: move; +} + +.draft-tag, +.public-tag, +.private-tag { + margin-left: 3px; + font-size: 9px; + font-weight: 600; + text-transform: uppercase; + color: #a4aab7; +} + +.draft-tag { + color: #9f7d10; +} + +.plus-icon { + display: inline-block; + width: 11px; + height: 11px; + margin-right: 8px; + background: url(../img/plus-icon.png) no-repeat; + + &.white { + background: url(../img/plus-icon-white.png) no-repeat; + } +} + +.plus-icon-small { + display: inline-block; + width: 6px; + height: 6px; + margin-right: 8px; + background: url(../img/plus-icon-small.png) no-repeat center; +} + +.folder-icon { + display: inline-block; + width: 15px; + height: 11px; + margin-right: 4px; + background: url(../img/folder-icon.png) no-repeat; +} + +.new-folder-icon { + display: inline-block; + width: 23px; + height: 11px; + margin-right: 8px; + background: url(../img/new-folder-icon.png) no-repeat; +} + +.file-icon { + display: inline-block; + width: 10px; + height: 11px; + margin-right: 8px; + background: url(../img/file-icon.png) no-repeat; +} + +.new-unit-icon { + display: inline-block; + width: 23px; + height: 12px; + margin-right: 8px; + background: url(../img/new-unit-icon.png) right no-repeat; +} + +.new-policy-icon { + display: inline-block; + width: 23px; + height: 12px; + margin-right: 8px; + background: url(../img/new-unit-icon.png) right no-repeat; +} + +.textbook-icon { + display: inline-block; + width: 32px; + height: 32px; + margin-right: 8px; + vertical-align: middle; + background: url(../img/textbook-icon.png) no-repeat; +} + +.slides-icon { + display: inline-block; + width: 32px; + height: 32px; + margin-right: 8px; + vertical-align: middle; + background: url(../img/slides-icon.png) no-repeat; +} + +.large-slide-icon { + display: inline-block; + width: 100px; + height: 60px; + margin-right: 5px; + background: url(../img/large-slide-icon.png) center no-repeat; +} + +.large-html-icon { + display: inline-block; + width: 100px; + height: 60px; + margin-right: 5px; + background: url(../img/html-icon.png) center no-repeat; +} + +.large-openended-icon { + display: inline-block; + width: 100px; + height: 60px; + margin-right: 5px; + background: url(../img/large-openended-icon.png) center no-repeat; +} + +.large-annotations-icon { + display: inline-block; + width: 100px; + height: 60px; + margin-right: 5px; + background: url(../img/large-annotations-icon.png) center no-repeat; +} + +.large-advanced-icon { + display: inline-block; + width: 100px; + height: 60px; + margin-right: 5px; + background: url(../img/large-advanced-icon.png) center no-repeat; +} + +.large-textbook-icon { + display: inline-block; + width: 100px; + height: 60px; + margin-right: 5px; + background: url(../img/large-textbook-icon.png) center no-repeat; +} + +.large-discussion-icon { + display: inline-block; + width: 100px; + height: 60px; + margin-right: 5px; + background: url(../img/large-discussion-icon.png) center no-repeat; +} + +.large-freeform-icon { + display: inline-block; + width: 100px; + height: 60px; + margin-right: 5px; + background: url(../img/large-freeform-icon.png) center no-repeat; +} + +.large-problem-icon { + display: inline-block; + width: 100px; + height: 60px; + margin-right: 5px; + background: url(../img/large-problem-icon.png) center no-repeat; +} + +.large-video-icon { + display: inline-block; + width: 100px; + height: 60px; + margin-right: 5px; + background: url(../img/large-video-icon.png) center no-repeat; +} + +.spinner-icon { + display: inline-block; + width: 20px; + height: 20px; + margin-left: 10px; + vertical-align: middle; + background: url(../img/blue-spinner.gif) no-repeat; +} + +.spinner-in-field-icon { + display: inline-block; + width: 14px; + height: 14px; + vertical-align: middle; + background: url(../img/spinner-in-field.gif) no-repeat; +} diff --git a/cms/static/sass/assets/_keyframes.scss b/cms/static/sass/assets/_keyframes.scss new file mode 100644 index 0000000000..7661f18980 --- /dev/null +++ b/cms/static/sass/assets/_keyframes.scss @@ -0,0 +1,27 @@ +@mixin bounce-in { + 0% { + opacity: 0; + @include transform(scale(.3)); + } + + 50% { + opacity: 1; + @include transform(scale(1.05)); + } + + 100% { + @include transform(scale(1)); + } +} + +@-moz-keyframes bounce-in { @include bounce-in(); } +@-webkit-keyframes bounce-in { @include bounce-in(); } +@-o-keyframes bounce-in { @include bounce-in(); } +@keyframes bounce-in { @include bounce-in();} + +@mixin bounce-in-animation($duration, $timing: ease-in-out) { + @include animation-name(bounce-in); + @include animation-duration($duration); + @include animation-timing-function($timing); + @include animation-fill-mode(both); +} diff --git a/cms/static/sass/elements/_alerts.scss b/cms/static/sass/elements/_alerts.scss new file mode 100644 index 0000000000..9c15f811e0 --- /dev/null +++ b/cms/static/sass/elements/_alerts.scss @@ -0,0 +1,165 @@ +// studio - elements - alerts, notifications, prompts +// ==================== + +// notifications +.wrapper-notification { + @include clearfix(); + @include box-sizing(border-box); + @include transition (bottom 2.0s ease-in-out 5s); + @include box-shadow(0 -1px 2px rgba(0,0,0,0.1)); + position: fixed; + bottom: -100px; + z-index: 1000; + width: 100%; + overflow: hidden; + opacity: 0; + border-top: 1px solid $darkGrey; + padding: 20px 40px; + + &.is-shown { + bottom: 0; + opacity: 1.0; + } + + &.wrapper-notification-warning { + border-color: shade($yellow, 25%); + background: tint($yellow, 25%); + } + + &.wrapper-notification-error { + border-color: shade($red, 50%); + background: tint($red, 20%); + color: $white; + } + + &.wrapper-notification-confirm { + border-color: shade($green, 30%); + background: tint($green, 40%); + color: shade($green, 30%); + } +} + +.notification { + @include box-sizing(border-box); + margin: 0 auto; + width: flex-grid(12); + max-width: $fg-max-width; + min-width: $fg-min-width; + + .copy { + float: left; + width: flex-grid(9, 12); + margin-right: flex-gutter(); + margin-top: 5px; + font-size: 14px; + + .icon { + display: inline-block; + vertical-align: top; + margin-right: 5px; + font-size: 20px; + } + + p { + width: flex-grid(8, 9); + display: inline-block; + vertical-align: top; + } + } + + .actions { + float: right; + width: flex-grid(3, 12); + margin-top: ($baseline/2); + text-align: right; + + li { + display: inline-block; + vertical-align: middle; + margin-right: 10px; + + &:last-child { + margin-right: 0; + } + } + + .save-button { + @include blue-button; + } + + .cancel-button { + @include white-button; + } + } + + strong { + font-weight: 700; + } +} + +// adopted alerts +.alert { + padding: 15px 20px; + margin-bottom: 30px; + border-radius: 3px; + border: 1px solid #edbd3c; + border-radius: 3px; + background: #fbf6e1; + // background: #edbd3c; + font-size: 14px; + @include clearfix; + + .alert-message { + float: left; + margin-top: 4px; + } + + strong { + font-weight: 700; + } + + .alert-action { + float: right; + + &.secondary { + @include orange-button; + } + } +} + +body.error { + background: $darkGrey; + color: #3c3c3c; + + .primary-header { + display: none; + } + + .error-prompt { + width: 700px; + margin: 150px auto; + padding: 60px 50px 90px; + border-radius: 3px; + background: #fff; + text-align: center; + } + + h1 { + float: none; + margin: 0; + font-size: 60px; + font-weight: 300; + color: #3c3c3c; + } + + .description { + margin-bottom: 50px; + font-size: 21px; + } + + .back-button { + @include blue-button; + padding: 14px 40px 18px; + font-size: 18px; + } +} \ No newline at end of file diff --git a/cms/static/sass/elements/_footer.scss b/cms/static/sass/elements/_footer.scss new file mode 100644 index 0000000000..2c32de4bb3 --- /dev/null +++ b/cms/static/sass/elements/_footer.scss @@ -0,0 +1,50 @@ +// studio - global footer +// ==================== + +.wrapper-footer { + margin: ($baseline*1.5) 0 $baseline 0; + padding: $baseline; + position: relative; + width: 100%; + + footer.primary { + @include clearfix(); + @include font-size(13); + max-width: $fg-max-width; + min-width: $fg-min-width; + width: flex-grid(12); + margin: 0 auto; + padding-top: $baseline; + border-top: 1px solid $gray-l4; + color: $gray-l2; + + .colophon { + width: flex-grid(4, 12); + float: left; + margin-right: flex-gutter(2); + } + + .nav-peripheral { + width: flex-grid(6, 12); + float: right; + text-align: right; + + .nav-item { + display: inline-block; + margin-right: ($baseline/2); + + &:last-child { + margin-right: 0; + } + } + } + + a { + color: $gray-l1; + + &:hover, &:active { + color: $blue; + } + } + } +} \ No newline at end of file diff --git a/cms/static/sass/elements/_header.scss b/cms/static/sass/elements/_header.scss new file mode 100644 index 0000000000..1e09184801 --- /dev/null +++ b/cms/static/sass/elements/_header.scss @@ -0,0 +1,558 @@ +// studio - global header +// ==================== + +.wrapper-header { + margin: 0 0 ($baseline*1.5) 0; + padding: $baseline; + border-bottom: 1px solid $gray; + @include box-shadow(0 1px 5px 0 rgba(0,0,0, 0.1)); + background: $white; + height: 76px; + position: relative; + width: 100%; + z-index: 10; + + a { + color: $baseFontColor; + display: block; + + &:hover, &:active { + color: $blue; + } + } + + header.primary { + @include clearfix(); + max-width: $fg-max-width; + min-width: $fg-min-width; + width: flex-grid(12); + margin: 0 auto; + color: $gray-l1; + } +} + +// ==================== + +// basic layout +.wrapper-left, .wrapper-right { + @include box-sizing(border-box); +} + +.wrapper-left { + width: flex-grid(10, 12); + float: left; + margin-right: flex-gutter(); +} + +.wrapper-right { + width: flex-grid(2, 12); + float: right; +} + +// ==================== + +// specific elements - branding +.branding, .info-course, .nav-course, .nav-account, .nav-unauth, .nav-pitch { + display: inline-block; + vertical-align: top; +} + +.branding { + position: relative; + margin: 0 ($baseline/2) 0 0; + padding-right: ($baseline*0.75); + + a { + @include text-hide(); + display: block; + width: 164px; + height: 32px; + background: transparent url('../img/logo-edx-studio.png') 0 0 no-repeat; + } +} + +// ==================== + +// specific elements - course name/info +.info-course { + @include font-size(14); + position: relative; + margin: -3px ($baseline/2) 0 0; + padding-right: ($baseline*0.75); + + &:before { + @extend .faded-vertical-divider; + content: ""; + display: block; + height: 50px; + position: absolute; + right: 1px; + top: -8px; + width: 1px; + } + + &:after { + @extend .faded-vertical-divider-light; + content: ""; + display: block; + height: 50px; + position: absolute; + right: 0px; + top: -12px; + width: 1px; + } + + .course-org { + margin-right: ($baseline/4); + } + + .course-number, .course-org { + @include font-size(12); + display: inline-block; + } + + .course-title { + display: block; + width: 100%; + max-width: 220px; + overflow: hidden; + margin-top: -4px; + white-space: nowrap; + text-overflow: ellipsis; + @include font-size(16); + font-weight: 600; + } +} + +// ==================== + +// specific elements - course nav +.nav-course { + width: 335px; + margin-top: -($baseline/4); + @include font-size(14); + + > ol > .nav-item { + vertical-align: bottom; + margin: 0 ($baseline/2) 0 0; + + &:last-child { + margin-right: 0; + } + + .title { + display: block; + padding: 5px; + text-transform: uppercase; + font-weight: 600; + color: $gray-d3; + + .label-prefix { + display: block; + @include font-size(11); + font-weight: 400; + } + } + + // specific nav items + &.nav-course-courseware { + } + + &.nav-course-settings { + } + + &.nav-course-tools { + } + } +} + +// ==================== + +// specific elements - account-based nav +.nav-account { + width: 100%; + margin-top: ($baseline*0.75); + @include font-size(14); + text-align: right; + + .nav-account-username { + width: 100%; + + .icon-user { + display: inline-block; + vertical-align: middle; + margin-right: 3px; + @include font-size(12); + } + + .account-username { + display: inline-block; + vertical-align: middle; + width: 80%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .icon-expand { + display: inline-block; + vertical-align: middle; + } + } +} + +// ==================== + +// UI - dropdown +.nav-dropdown { + + .nav-item { + position: relative; + + .icon-expand { + @include font-size(14); + @include transition (color 0.5s ease-in-out, opacity 0.5s ease-in-out); + display: inline-block; + margin-left: 2px; + opacity: 0.5; + color: $gray-l2; + } + + &:hover { + + .icon-expand { + color: $blue; + opacity: 1.0; + } + } + } + + .wrapper-nav-sub { + position: absolute; + left: -7px; + top: 47px; + width: 140px; + opacity: 0; + pointer-events: none; + } + + .nav-sub { + @include border-radius(2px); + @include box-sizing(border-box); + @include box-shadow(0 1px 5px 0 rgba(0,0,0, 0.1)); + position: relative; + width: 100%; + border: 1px solid $gray-l2; + padding: ($baseline/4) ($baseline/2); + background: $white; + + &:after, &:before { + bottom: 100%; + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + } + + &:after { + border-color: rgba(255, 255, 255, 0); + border-bottom-color: #fff; + border-width: 5px; + right: 3px; + margin-left: -5px; + } + + &:before { + border-color: rgba(178, 178, 178, 0); + border-bottom-color: $gray-l2; + border-width: 6px; + right: 3px; + margin-left: -6px; + } + + .nav-item { + display: block; + margin: 0 0 ($baseline/4) 0; + border-bottom: 1px solid $gray-l5; + padding: 0 0($baseline/4) 0; + @include font-size(13); + + &:last-child { + margin-bottom: 0; + border-bottom: none; + padding-bottom: 0; + } + + a { + display: block; + } + } + } + + // UI - dropdown - specific navs + &.nav-account { + + .wrapper-nav-sub { + top: 27px; + left: auto; + right: -13px; + width: 110px; + } + + .nav-sub { + text-align: left; + + .icon-expand { + top: -2px; + } + } + + .nav-sub:after { + left: auto; + right: 11px; + } + + .nav-sub:before { + left: auto; + right: 10px; + } + } + + &.nav-course { + + .nav-course-courseware { + + .nav-sub:after { + left: 88px; + } + + .nav-sub:before { + left: 88px; + } + } + + .nav-course-settings { + + .nav-sub:after { + left: 88px; + } + + .nav-sub:before { + left: 88px; + } + } + + .nav-course-tools { + + .wrapper-nav-sub { + top: ($baseline*1.5); + width: 100px; + } + + .nav-sub:after { + left: 68px; + } + + .nav-sub:before { + left: 68px; + } + } + } +} + +// ==================== + +// STATE: is-signed in +.is-signedin { + + &.course .branding { + + &:before { + @extend .faded-vertical-divider; + content: ""; + display: block; + height: 50px; + position: absolute; + right: 1px; + top: -8px; + width: 1px; + } + + &:after { + @extend .faded-vertical-divider-light; + content: ""; + display: block; + height: 50px; + position: absolute; + right: 0px; + top: -12px; + width: 1px; + } + } +} + +// ==================== + +// STATE: not signed in +.not-signedin { + + .wrapper-left { + width: flex-grid(4, 12); + } + + .wrapper-right { + width: flex-grid(8, 12); + } + + // STATE: not signed in - unauthenticated nav + .nav-not-signedin { + float: right; + margin-top: ($baseline/4); + + .nav-item { + @include font-size(16); + vertical-align: middle; + margin: 0 $baseline 0 0; + + &:last-child { + margin-right: 0; + } + + .action { + margin-top: -($baseline/4); + display: inline-block; + padding: ($baseline/4) ($baseline/2); + } + } + + // STATE: not signed in - specific items + .nav-not-signedin-help { + + } + + .nav-not-signedin-signup { + margin-right: ($baseline/2); + + .action-signup { + @include blue-button; + @include transition(all .15s); + @include font-size(14); + padding: ($baseline/4) ($baseline/2); + text-transform: uppercase; + font-weight: 600; + } + } + + .nav-not-signedin-signin { + + .action-signin { + @include white-button; + @include transition(all .15s); + @include font-size(14); + padding: ($baseline/4) ($baseline/2); + text-transform: uppercase; + font-weight: 600; + } + } + } +} + +// ==================== + +// STATE: active/current nav states + +.nav-item.is-current, +body.howitworks .nav-not-signedin-hiw, + +// dashboard +body.dashboard .nav-account-dashboard, + +// course content +body.course.outline .nav-course-courseware .title, +body.course.updates .nav-course-courseware .title, +body.course.pages .nav-course-courseware .title, +body.course.uploads .nav-course-courseware .title, + +body.course.outline .nav-course-courseware-outline, +body.course.updates .nav-course-courseware-updates, +body.course.pages .nav-course-courseware-pages, +body.course.uploads .nav-course-courseware-uploads, + +// course settings +body.course.schedule .nav-course-settings .title, +body.course.grading .nav-course-settings .title, +body.course.team .nav-course-settings .title, +body.course.advanced .nav-course-settings .title, + +body.course.schedule .nav-course-settings-schedule, +body.course.grading .nav-course-settings-grading, +body.course.team .nav-course-settings-team, +body.course.advanced .nav-course-settings-advanced, + +// course tools +body.course.import .nav-course-tools .title, +body.course.export .nav-course-tools .title, + +body.course.import .nav-course-tools-import, +body.course.export .nav-course-tools-export, + +{ + + color: $blue; + + a { + color: $blue; + pointer-events: none; + } +} + +body.signup .nav-not-signedin-signin { + + a { + background-color: #d9e3ee; + color: #6d788b; + } +} + +body.signin .nav-not-signedin-signup { + + a { + background-color: #62aaf5; + color: #fff; + } +} + +// ==================== + +// STATE: js enabled +.js { + + .nav-dropdown { + + .nav-item .title { + outline: 0; + cursor: pointer; + + &:hover, &:active, &.is-selected { + color: $blue; + + .icon-expand { + color: $blue; + } + } + } + } + + .wrapper-nav-sub { + @include transition (opacity 1.0s ease-in-out 0s); + opacity: 0; + pointer-events: none; + + &.is-shown { + opacity: 1.0; + pointer-events: auto; + } + } +} \ No newline at end of file diff --git a/cms/static/sass/elements/_jquery-ui-calendar.scss b/cms/static/sass/elements/_jquery-ui-calendar.scss new file mode 100644 index 0000000000..3d20bde642 --- /dev/null +++ b/cms/static/sass/elements/_jquery-ui-calendar.scss @@ -0,0 +1,56 @@ +// studio - elements - JQUI calendar +// ==================== + +.ui-datepicker { + border-color: $darkGrey; + border-radius: 2px; + background: #fff; + font-family: $sans-serif; + font-size: 12px; + @include box-shadow(0 5px 10px rgba(0, 0, 0, 0.1)); + + .ui-widget-header { + background: $darkGrey; + border: none; + border-radius: 2px; + } + + .ui-datepicker-next, + .ui-datepicker-prev { + @include transition(none); + + &.ui-state-hover { + border-color: transparent; + background: $mediumGrey; + + .ui-icon-circle-triangle-e, + .ui-icon-circle-triangle-w { + background-image: url(../css/vendor/ui-lightness/images/ui-icons_ffffff_256x240.png); + } + } + } + + .ui-state-default { + border-color: $mediumGrey; + color: $blue; + @include transition(none); + + &.ui-state-hover { + background: $orange; + border-color: $orange; + color: #fff; + } + } + + .ui-state-highlight { + background: $blue; + border-color: $blue; + color: #fff; + } + + .ui-state-active { + background: $orange; + border-color: $orange; + color: #fff; + } +} \ No newline at end of file diff --git a/cms/static/sass/elements/_modal.scss b/cms/static/sass/elements/_modal.scss new file mode 100644 index 0000000000..b81baf4565 --- /dev/null +++ b/cms/static/sass/elements/_modal.scss @@ -0,0 +1,72 @@ +// studio - elements - modal windows +// ==================== + +.modal-cover { + display: none; + position: fixed; + top: 0; + left: 0; + z-index: 1000; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, .8); +} + +.modal { + display: none; + position: fixed; + top: 60px; + left: 50%; + z-index: 1001; + width: 930px; + height: 540px; + margin-left: -465px; + background: #fff; + + .modal-body { + height: 400px; + padding: 40px; + overflow-y: scroll; + } + + .modal-actions { + height: 60px; + @include linear-gradient(top, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0)); + background-color: #d1dae3; + } + + h2 { + margin: 0 10px 30px; + color: #646464; + font-size: 19px; + font-weight: 300; + letter-spacing: 1px; + text-transform: uppercase; + } + + p { + margin: 20px; + } + + .revert-button { + @include blue-button; + margin: 13px 6px 0 13px; + } + + .close-button { + @include white-button; + margin-top: 13px; + } +} + +// lean modal alternative +#lean_overlay { + position: fixed; + z-index: 10000; + top: 0px; + left: 0px; + display: none; + height: 100%; + width: 100%; + background: $black; +} \ No newline at end of file diff --git a/cms/static/sass/elements/_navigation.scss b/cms/static/sass/elements/_navigation.scss new file mode 100644 index 0000000000..8c123db64f --- /dev/null +++ b/cms/static/sass/elements/_navigation.scss @@ -0,0 +1,31 @@ +// studio - elements - navigation +// ==================== + +// common + +// ==================== + +// primary +nav.primary { + + .nav-item { + display: inline-block; + } +} + + +// ==================== + +// right hand side + +// ==================== + +// tabs + +// ==================== + +// dropdown + +// ==================== + +// \ No newline at end of file diff --git a/cms/static/sass/views/_account.scss b/cms/static/sass/views/_account.scss new file mode 100644 index 0000000000..1206db5e76 --- /dev/null +++ b/cms/static/sass/views/_account.scss @@ -0,0 +1,295 @@ +// studio - views - sign up/in +// ==================== + +body.signup, body.signin { + + .wrapper-content { + margin: 0; + padding: 0 $baseline; + position: relative; + width: 100%; + } + + .content { + @include clearfix(); + @include font-size(16); + max-width: $fg-max-width; + min-width: $fg-min-width; + width: flex-grid(12); + margin: 0 auto; + color: $gray-d2; + + header { + position: relative; + margin-bottom: $baseline; + border-bottom: 1px solid $gray-l4; + padding-bottom: ($baseline/2); + + h1 { + @include font-size(32); + margin: 0; + padding: 0; + font-weight: 600; + } + + .action { + @include font-size(13); + position: absolute; + right: 0; + top: 40%; + } + } + + .introduction { + @include font-size(14); + margin: 0 0 $baseline 0; + } + } + + .content-primary, .content-supplementary { + @include box-sizing(border-box); + float: left; + } + + .content-primary { + width: flex-grid(8, 12); + margin-right: flex-gutter(); + + form { + @include box-sizing(border-box); + @include box-shadow(0 1px 2px $shadow-l1); + @include border-radius(2px); + width: 100%; + border: 1px solid $gray-l2; + padding: $baseline ($baseline*1.5); + background: $white; + + .form-actions { + margin-top: $baseline; + + .action-primary { + @include blue-button; + @include transition(all .15s); + @include font-size(15); + display:block; + width: 100%; + padding: ($baseline*0.75) ($baseline/2); + font-weight: 600; + text-transform: uppercase; + } + } + + .list-input { + margin: 0; + padding: 0; + list-style: none; + + .field { + margin: 0 0 ($baseline*0.75) 0; + + &:last-child { + margin-bottom: 0; + } + + &.required { + + label { + font-weight: 600; + } + + label:after { + margin-left: ($baseline/4); + content: "*"; + } + } + + label, input, textarea { + display: block; + } + + label { + @include font-size(14); + @include transition(color, 0.15s, ease-in-out); + margin: 0 0 ($baseline/4) 0; + + &.is-focused { + color: $blue; + } + } + + input, textarea { + @include font-size(16); + height: 100%; + width: 100%; + padding: ($baseline/2); + + &.long { + width: 100%; + } + + &.short { + width: 25%; + } + + ::-webkit-input-placeholder { + color: $gray-l4; + } + + :-moz-placeholder { + color: $gray-l3; + } + + ::-moz-placeholder { + color: $gray-l3; + } + + :-ms-input-placeholder { + color: $gray-l3; + } + + &:focus { + + + .tip { + color: $gray; + } + } + } + + textarea.long { + height: ($baseline*5); + } + + input[type="checkbox"] { + display: inline-block; + margin-right: ($baseline/4); + width: auto; + height: auto; + + & + label { + display: inline-block; + } + } + + .tip { + @include transition(color, 0.15s, ease-in-out); + @include font-size(13); + display: block; + margin-top: ($baseline/4); + color: $gray-l3; + } + } + + .field-group { + @include clearfix(); + margin: 0 0 ($baseline/2) 0; + + .field { + display: block; + width: 47%; + border-bottom: none; + margin: 0 $baseline 0 0; + padding-bottom: 0; + + &:nth-child(odd) { + float: left; + } + + &:nth-child(even) { + float: right; + margin-right: 0; + } + + input, textarea { + width: 100%; + } + } + } + } + } + } + + .content-supplementary { + width: flex-grid(4, 12); + + .bit { + @include font-size(13); + margin: 0 0 $baseline 0; + border-bottom: 1px solid $gray-l4; + padding: 0 0 $baseline 0; + color: $gray-l1; + + &:last-child { + margin-bottom: 0; + border: none; + padding-bottom: 0; + } + + h3 { + @include font-size(14); + margin: 0 0 ($baseline/4) 0; + color: $gray-d2; + font-weight: 600; + } + + } + } +} + +.signup { + +} + +.signin { + + #field-password { + position: relative; + + .action-forgotpassword { + @include font-size(13); + position: absolute; + top: 0; + right: 0; + } + } +} + +// ==================== + +// messages +.message { + @include font-size(14); + display: block; +} + +.message-status { + display: none; + @include border-top-radius(2px); + @include box-sizing(border-box); + border-bottom: 2px solid $yellow-d2; + margin: 0 0 $baseline 0; + padding: ($baseline/2) $baseline; + font-weight: 500; + background: $yellow-d1; + color: $white; + + .ss-icon { + position: relative; + top: 3px; + @include font-size(16); + display: inline-block; + margin-right: ($baseline/2); + } + + .text { + display: inline-block; + } + + &.error { + border-color: shade($red, 50%); + background: tint($red, 20%); + } + + &.is-shown { + display: block; + } +} diff --git a/cms/static/sass/views/_assets.scss b/cms/static/sass/views/_assets.scss new file mode 100644 index 0000000000..2c1b435c44 --- /dev/null +++ b/cms/static/sass/views/_assets.scss @@ -0,0 +1,189 @@ +// studio - views - assets +// ==================== + +.uploads { + input.asset-search-input { + float: left; + width: 260px; + background-color: #fff; + } + + .asset-library { + @include clearfix; + + table { + width: 100%; + border-radius: 3px 3px 0 0; + border: 1px solid #c5cad4; + + td, + th { + padding: 10px 20px; + text-align: left; + vertical-align: middle; + } + + thead th { + @include linear-gradient(top, transparent, rgba(0, 0, 0, .1)); + background-color: #ced2db; + font-size: 12px; + font-weight: 700; + text-shadow: 0 1px 0 rgba(255, 255, 255, .5); + } + + tbody { + background: #fff; + + tr { + border-top: 1px solid #c5cad4; + + &:first-child { + border-top: none; + } + } + + .name-col { + font-size: 14px; + } + + .date-col { + font-size: 12px; + } + } + + .thumb-col { + width: 100px; + } + + .date-col { + width: 220px; + } + + .embed-col { + width: 250px; + } + + .embeddable-xml-input { + @include box-shadow(none); + width: 100%; + } + + .thumb { + width: 100px; + max-height: 80px; + + img { + width: 100%; + } + } + } + + .pagination { + float: right; + margin: 15px 10px; + + ol, li { + display: inline; + } + + a { + display: inline-block; + height: 25px; + padding: 0 4px; + text-align: center; + line-height: 25px; + } + } + } + .show-xml { + @include blue-button; + } +} + +.upload-modal { + display: none; + width: 640px !important; + margin-left: -320px !important; + + .modal-body { + height: auto !important; + overflow-y: auto !important; + text-align: center; + } + + .file-input { + display: none; + } + + .choose-file-button { + @include blue-button; + padding: 10px 82px 12px; + font-size: 17px; + } + + .progress-bar { + display: none; + width: 350px; + height: 50px; + margin: 30px auto 10px; + border: 1px solid $blue; + + &.loaded { + border-color: #66b93d; + + .progress-fill { + background: #66b93d; + } + } + } + + .progress-fill { + width: 0%; + height: 50px; + background: $blue; + color: #fff; + line-height: 48px; + } + + h1 { + float: none; + margin: 40px 0 30px; + font-size: 34px; + font-weight: 300; + } + + .close-button { + @include white-button; + position: absolute; + top: 0; + right: 15px; + width: 29px; + height: 29px; + padding: 0 !important; + border-radius: 17px !important; + line-height: 29px; + text-align: center; + } + + .embeddable { + display: none; + margin: 30px 0 130px; + + label { + display: block; + margin-bottom: 10px; + font-weight: 700; + } + } + + .embeddable-xml-input { + @include box-shadow(none); + width: 400px; + } + + .copy-button { + @include white-button; + display: none; + margin-bottom: 100px; + } +} \ No newline at end of file diff --git a/cms/static/sass/views/_dashboard.scss b/cms/static/sass/views/_dashboard.scss new file mode 100644 index 0000000000..2a995ffdc7 --- /dev/null +++ b/cms/static/sass/views/_dashboard.scss @@ -0,0 +1,117 @@ +// studio - views - user dashboard +// ==================== + +.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/views/_export.scss b/cms/static/sass/views/_export.scss new file mode 100644 index 0000000000..27cb7f923b --- /dev/null +++ b/cms/static/sass/views/_export.scss @@ -0,0 +1,126 @@ +// studio - views - course export +// ==================== + +.export { + .export-overview { + @extend .window; + @include clearfix; + padding: 30px 40px; + } + + .description { + float: left; + width: 62%; + margin-right: 3%; + font-size: 14px; + + h2 { + font-weight: 700; + font-size: 19px; + margin-bottom: 20px; + } + + strong { + font-weight: 700; + } + + p + p { + margin-top: 20px; + } + + ul { + margin: 20px 0; + list-style: disc inside; + + li { + margin: 0 0 5px 0; + } + } + } + + .export-form-wrapper { + + .export-form { + float: left; + width: 35%; + padding: 25px 30px 35px; + @include box-sizing(border-box); + border: 1px solid $mediumGrey; + border-radius: 3px; + background: $lightGrey; + text-align: center; + + h2 { + margin-bottom: 30px; + font-size: 26px; + font-weight: 300; + } + + .error-block { + display: none; + margin-bottom: 15px; + font-size: 13px; + } + + .error-block { + color: $error-red; + } + + .button-export { + @include green-button; + padding: 10px 50px 11px; + font-size: 17px; + } + + .message-status { + margin-top: 10px; + font-size: 12px; + } + + .progress-bar { + display: none; + width: 350px; + height: 30px; + margin: 30px auto 10px; + border: 1px solid $blue; + + &.loaded { + border-color: #66b93d; + + .progress-fill { + background: #66b93d; + } + } + } + + .progress-fill { + width: 0%; + height: 30px; + background: $blue; + color: #fff; + line-height: 48px; + } + } + + // downloading state + &.is-downloading { + + .progress-bar { + display: block; + } + + .button-export { + padding: 10px 50px 11px; + font-size: 17px; + + &.disabled { + + pointer-events: none; + cursor: default; + } + } + } + } + + +} \ No newline at end of file diff --git a/cms/static/sass/views/_import.scss b/cms/static/sass/views/_import.scss new file mode 100644 index 0000000000..fe7d65626b --- /dev/null +++ b/cms/static/sass/views/_import.scss @@ -0,0 +1,105 @@ +// studio - views - course import +// ==================== + +.import { + .import-overview { + @extend .window; + @include clearfix; + padding: 30px 40px; + } + + .description { + float: left; + width: 62%; + margin-right: 3%; + font-size: 14px; + + h2 { + font-weight: 700; + font-size: 19px; + margin-bottom: 20px; + } + + strong { + font-weight: 700; + } + + p + p { + margin-top: 20px; + } + } + + .import-form { + float: left; + width: 35%; + padding: 25px 30px 35px; + @include box-sizing(border-box); + border: 1px solid $mediumGrey; + border-radius: 3px; + background: $lightGrey; + text-align: center; + + h2 { + margin-bottom: 30px; + font-size: 26px; + font-weight: 300; + } + + .file-name-block, + .error-block { + display: none; + margin-bottom: 15px; + font-size: 13px; + } + + .error-block { + color: $error-red; + } + + .choose-file-button { + @include blue-button; + padding: 10px 50px 11px; + font-size: 17px; + } + + .choose-file-button-inline { + display: block; + } + + .file-input { + display: none; + } + + .submit-button { + @include orange-button; + display: none; + max-width: 100%; + padding: 8px 20px 10px; + white-space: normal; + } + } + + .progress-bar { + display: none; + width: 350px; + height: 30px; + margin: 30px auto 10px; + border: 1px solid $blue; + + &.loaded { + border-color: #66b93d; + + .progress-fill { + background: #66b93d; + } + } + } + + .progress-fill { + width: 0%; + height: 30px; + background: $blue; + color: #fff; + line-height: 48px; + } +} \ No newline at end of file diff --git a/cms/static/sass/views/_index.scss b/cms/static/sass/views/_index.scss new file mode 100644 index 0000000000..8d81ea33fd --- /dev/null +++ b/cms/static/sass/views/_index.scss @@ -0,0 +1,355 @@ +// studio - views - how it works +// ==================== + +.index { + + &.not-signedin { + + .wrapper-header { + margin-bottom: 0; + } + + .wrapper-footer { + margin: 0; + border-top: 2px solid $gray-l3; + + footer.primary { + border: none; + margin-top: 0; + padding-top: 0; + } + } + + .wrapper-content-header, .wrapper-content-features, .wrapper-content-cta { + @include box-sizing(border-box); + margin: 0; + padding: 0 $baseline; + position: relative; + width: 100%; + } + + .content { + @include clearfix(); + @include font-size(16); + max-width: $fg-max-width; + min-width: $fg-min-width; + width: flex-grid(12); + margin: 0 auto; + color: $gray-d2; + + header { + border: none; + padding-bottom: 0; + margin-bottom: 0; + } + + h1, h2, h3, h4, h5, h6 { + color: $gray-d3; + } + + h2 { + + } + + h3 { + + } + + h4 { + + } + } + + // welcome content + .wrapper-content-header { + @include linear-gradient($blue-l1,$blue,$blue-d1); + padding-bottom: ($baseline*4); + padding-top: ($baseline*4); + } + + .content-header { + position: relative; + text-align: center; + color: $white; + + h1 { + @include font-size(52); + float: none; + margin: 0 0 ($baseline/2) 0; + border-bottom: 1px solid $blue-l1; + padding: 0; + font-weight: 500; + color: $white; + } + + .logo { + @include text-hide(); + position: relative; + top: 3px; + display: inline-block; + vertical-align: baseline; + width: 282px; + height: 57px; + background: transparent url('../img/logo-edx-studio-white.png') 0 0 no-repeat; + } + + .tagline { + @include font-size(24); + margin: 0; + color: $blue-l3; + } + } + + .arrow_box { + position: relative; + background: #fff; + border: 4px solid #000; + } + .arrow_box:after, .arrow_box:before { + top: 100%; + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + } + + .arrow_box:after { + border-color: rgba(255, 255, 255, 0); + border-top-color: #fff; + border-width: 30px; + left: 50%; + margin-left: -30px; + } + .arrow_box:before { + border-color: rgba(0, 0, 0, 0); + border-top-color: #000; + border-width: 36px; + left: 50%; + margin-left: -36px; + } + + // feature content + .wrapper-content-features { + @include box-shadow(0 -1px ($baseline/4) $shadow); + padding-bottom: ($baseline*2); + padding-top: ($baseline*3); + background: $white; + } + + .content-features { + + .list-features { + + } + + // indiv features + .feature { + @include clearfix(); + margin: 0 0 ($baseline*2) 0; + border-bottom: 1px solid $gray-l4; + padding: 0 0 ($baseline*2) 0; + + .img { + @include box-sizing(border-box); + float: left; + width: flex-grid(3, 12); + margin-right: flex-gutter(); + + a { + @include box-sizing(border-box); + @include box-shadow(0 1px ($baseline/10) $shadow-l1); + position: relative; + top: 0; + display: block; + overflow: hidden; + border: 1px solid $gray-l3; + padding: ($baseline/4); + background: $white; + + .action-zoom { + @include transition(bottom .50s ease-in-out); + position: absolute; + bottom: -30px; + right: ($baseline/2); + opacity: 0; + + .ss-icon { + @include font-size(18); + @include border-top-radius(3px); + display: inline-block; + padding: ($baseline/4) ($baseline/2); + background: $blue; + color: $white; + text-align: center; + } + } + + &:hover { + border-color: $blue; + + .action-zoom { + opacity: 1.0; + bottom: -2px; + } + } + } + + img { + display: block; + width: 100%; + height: 100%; + } + } + + .copy { + float: left; + width: flex-grid(9, 12); + margin-top: -($baseline/4); + + h3 { + margin: 0 0 ($baseline/2) 0; + @include font-size(24); + font-weight: 600; + } + + > p { + @include font-size(18); + color: $gray-d1; + } + + strong { + color: $gray-d2; + font-weight: 500; + } + + .list-proofpoints { + @include clearfix(); + @include font-size(14); + width: flex-grid(9, 9); + margin: ($baseline*1.5) 0 0 0; + + .proofpoint { + @include box-sizing(border-box); + @include border-radius(($baseline/4)); + @include transition(color .50s ease-in-out); + position: relative; + top: 0; + float: left; + width: flex-grid(3, 9); + min-height: ($baseline*8); + margin-right: flex-gutter(); + padding: ($baseline*0.75) $baseline; + color: $gray-l1; + + .title { + @include font-size(16); + margin: 0 0 ($baseline/4) 0; + font-weight: 500; + color: $gray-d3; + } + + &:hover { + @include box-shadow(0 1px ($baseline/10) $shadow-l1); + background: $blue-l5; + top: -($baseline/5); + + .title { + color: $blue; + } + } + + &:last-child { + margin-right: 0; + } + } + } + } + + + &:last-child { + margin-bottom: 0; + border: none; + padding-bottom: 0; + } + + &:nth-child(even) { + + .img { + float: right; + margin-right: 0; + margin-left: flex-gutter(); + } + + .copy { + float: right; + text-align: right; + } + + .list-proofpoints { + + .proofpoint { + float: right; + width: flex-grid(3, 9); + margin-left: flex-gutter(); + margin-right: 0; + + &:last-child { + margin-left: 0; + } + } + } + } + } + } + + // call to action content + .wrapper-content-cta { + padding-bottom: ($baseline*2); + padding-top: ($baseline*2); + background: $white; + } + + .content-cta { + border-top: 1px solid $gray-l4; + + header { + border: none; + margin: 0; + padding: 0; + } + + .list-actions { + position: relative; + margin-top: -($baseline*1.5); + + li { + width: flex-grid(6, 12); + margin: 0 auto; + } + + .action { + display: block; + width: 100%; + text-align: center; + } + + .action-primary { + @include blue-button; + @include transition(all .15s); + @include font-size(18); + padding: ($baseline*0.75) ($baseline/2); + font-weight: 600; + text-align: center; + text-transform: uppercase; + } + + .action-secondary { + @include font-size(14); + margin-top: ($baseline/2); + } + } + } + } +} \ No newline at end of file diff --git a/cms/static/sass/views/_outline.scss b/cms/static/sass/views/_outline.scss new file mode 100644 index 0000000000..2957b57849 --- /dev/null +++ b/cms/static/sass/views/_outline.scss @@ -0,0 +1,691 @@ +// studio - views - 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)); +} + +.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/views/_settings.scss b/cms/static/sass/views/_settings.scss new file mode 100644 index 0000000000..8fc9540a57 --- /dev/null +++ b/cms/static/sass/views/_settings.scss @@ -0,0 +1,741 @@ +// studio - views - course settings +// ==================== + +body.course.settings { + + .content-primary, .content-supplementary { + @include box-sizing(border-box); + float: left; + } + + .content-primary { + @extend .window; + width: flex-grid(9, 12); + margin-right: flex-gutter(); + padding: $baseline ($baseline*1.5); + } + + // messages - should be synced up with global messages in the future + .message { + display: block; + font-size: 14px; + } + + .message-status { + display: none; + @include border-top-radius(2px); + @include box-sizing(border-box); + border-bottom: 2px solid $yellow; + margin: 0 0 20px 0; + padding: 10px 20px; + font-weight: 500; + background: $paleYellow; + + .text { + display: inline-block; + } + + &.error { + border-color: shade($red, 50%); + background: tint($red, 20%); + color: $white; + } + + &.confirm { + border-color: shade($green, 50%); + background: tint($green, 20%); + color: $white; + } + + &.is-shown { + display: block; + } + } + + // in form - elements + .group-settings { + margin: 0 0 ($baseline*2) 0; + + header { + @include clearfix(); + + .title-2 { + width: flex-grid(4, 9); + margin: 0 flex-gutter() 0 0; + float: left; + } + + .tip { + @include font-size(13); + width: flex-grid(5, 9); + float: right; + margin-top: ($baseline/2); + text-align: right; + color: $gray-l2; + } + } + + // basic layout/elements + .title-2 { + + } + + .title-3 { + + } + + // in form -UI hints/tips/messages + .instructions { + @include font-size(14); + margin: 0 0 $baseline 0; + } + + .tip { + @include transition(color, 0.15s, ease-in-out); + @include font-size(13); + display: block; + margin-top: ($baseline/4); + color: $gray-l3; + } + + .message-error { + @include font-size(13); + display: block; + margin-top: ($baseline/4); + margin-bottom: ($baseline/2); + color: $red; + } + + // buttons + .remove-item { + @include white-button; + @include font-size(13); + font-weight: 400; + } + + .new-button { + @include font-size(13); + } + + // form basics + .list-input { + margin: 0; + padding: 0; + list-style: none; + + .field { + margin: 0 0 $baseline 0; + + &:last-child { + margin-bottom: 0; + } + + &.required { + + label { + font-weight: 600; + } + + label:after { + margin-left: ($baseline/4); + content: "*"; + } + } + + label, input, textarea { + display: block; + } + + label { + @include font-size(14); + @include transition(color, 0.15s, ease-in-out); + margin: 0 0 ($baseline/4) 0; + font-weight: 400; + + &.is-focused { + color: $blue; + } + } + + input, textarea { + @include placeholder($gray-l4); + @include font-size(16); + @include size(100%,100%); + padding: ($baseline/2); + + &.long { + } + + &.short { + } + + &.error { + border-color: $red; + } + + &:focus { + + + .tip { + color: $gray; + } + } + } + + textarea.long { + height: ($baseline*5); + } + + input[type="checkbox"] { + display: inline-block; + margin-right: ($baseline/4); + width: auto; + height: auto; + + & + label { + display: inline-block; + } + } + } + + .field-group { + @include clearfix(); + margin: 0 0 ($baseline/2) 0; + } + + // enumerated/grouped lists + &.enum { + + .field-group { + @include box-sizing(border-box); + @include border-radius(3px); + background: $gray-l5; + padding: $baseline; + + &:last-child { + padding-bottom: $baseline; + } + + .actions { + @include clearfix(); + margin-top: ($baseline/2); + border-top: 1px solid $gray-l4; + padding-top: ($baseline/2); + + .remove-item { + float: right; + } + } + } + } + } + + // existing inputs + .input-existing { + margin: 0 0 $baseline 0; + + .actions { + margin: ($baseline/4) 0 0 0; + } + } + + // not editable fields + .field.is-not-editable { + + label, .label { + color: $gray-l3; + } + + input { + opacity: 0.25; + } + } + + // field with error + .field.error { + + input, textarea { + border-color: $red; + } + } + + // specific fields - basic + &.basic { + + .list-input { + @include clearfix(); + + .field { + margin-bottom: 0; + } + } + + #field-course-organization { + float: left; + width: flex-grid(2, 9); + margin-right: flex-gutter(); + } + + #field-course-number { + float: left; + width: flex-grid(2, 9); + margin-right: flex-gutter(); + } + + #field-course-name { + float: left; + width: flex-grid(5, 9); + } + } + + // specific fields - schedule + &.schedule { + + .list-input { + margin-bottom: ($baseline*1.5); + + &:last-child { + margin-bottom: 0; + } + } + + .field-group { + @include clearfix(); + border-bottom: 1px solid $gray-l5; + padding-bottom: ($baseline/2); + + &:last-child { + border: none; + padding-bottom: 0; + } + + .field { + float: left; + width: flex-grid(3, 9); + margin-bottom: ($baseline/4); + margin-right: flex-gutter(); + } + + .field.time { + position: relative; + + .tip { + position: absolute; + top: 0; + right: 0; + } + } + } + } + + // specific fields - overview + #field-course-overview { + + #course-overview { + height: ($baseline*20); + } + } + + // specific fields - video + #field-course-introduction-video { + + .input-existing { + @include box-sizing(border-box); + @include border-radius(3px); + background: $gray-l5; + padding: ($baseline/2); + + .actions { + @include clearfix(); + margin-top: ($baseline/2); + border-top: 1px solid $gray-l4; + padding-top: ($baseline/2); + + .remove-item { + float: right; + } + } + } + + .actions { + margin-top: ($baseline/2); + border-top: 1px solid $gray-l5; + padding-top: ($baseline/2); + } + } + + // specific fields - requirements + &.requirements { + + #field-course-effort { + width: flex-grid(3, 9); + } + } + + // specific fields - grading range (artifact styling) + &.grade-range { + margin-bottom: ($baseline*3); + + .grade-controls { + @include clearfix; + width: flex-grid(9,9); + } + + .new-grade-button { + @include box-sizing(border-box); + @include linear-gradient(top, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0)); + @include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset); + width: flex-grid(1,9); + height: ($baseline*2); + position: relative; + display: inline-block; + vertical-align: middle; + margin-right: flex-gutter(); + border-radius: 20px; + border: 1px solid $darkGrey; + background-color: #d1dae3; + color: #6d788b; + + .plus-icon { + position: absolute; + top: 50%; + left: 50%; + margin-left: -6px; + margin-top: -6px; + } + } + + .grade-slider { + @include box-sizing(border-box); + width: flex-grid(8,9); + display: inline-block; + vertical-align: middle; + + .grade-bar { + position: relative; + width: 100%; + height: ($baseline*2.5); + background: $lightGrey; + + .increments { + position: relative; + + li { + position: absolute; + top: 52px; + width: 30px; + margin-left: -15px; + font-size: 9px; + text-align: center; + + &.increment-0 { + left: 0; + } + + &.increment-10 { + left: 10%; + } + + &.increment-20 { + left: 20%; + } + + &.increment-30 { + left: 30%; + } + + &.increment-40 { + left: 40%; + } + + &.increment-50 { + left: 50%; + } + + &.increment-60 { + left: 60%; + } + + &.increment-70 { + left: 70%; + } + + &.increment-80 { + left: 80%; + } + + &.increment-90 { + left: 90%; + } + + &.increment-100 { + left: 100%; + } + } + } + + .grade-specific-bar { + height: 50px !important; + } + + .grades { + position: relative; + + li { + position: absolute; + top: 0; + height: 50px; + text-align: right; + @include border-radius(2px); + + &:hover, + &.is-dragging { + .remove-button { + display: block; + } + } + + &.is-dragging { + + } + + .remove-button { + display: none; + position: absolute; + top: -17px; + right: 1px; + height: 17px; + font-size: 10px; + } + + &:nth-child(1) { + background: #4fe696; + } + + &:nth-child(2) { + background: #ffdf7e; + } + + &:nth-child(3) { + background: #ffb657; + } + + &:nth-child(4) { + background: #ef54a1; + } + + &:nth-child(5), + &.bar-fail { + background: #fb336c; + } + + .letter-grade { + display: block; + margin: 10px 15px 0 0; + font-size: 16px; + font-weight: 700; + line-height: 14px; + } + + .range { + display: block; + margin-right: 15px; + font-size: 10px; + line-height: 12px; + } + + .drag-bar { + position: absolute; + top: 0; + right: -1px; + height: 50px; + width: 2px; + background-color: #fff; + @include box-shadow(-1px 0 3px rgba(0,0,0,0.1)); + + cursor: ew-resize; + @include transition(none); + + &:hover { + width: 6px; + right: -2px; + } + } + } + } + } + } + } + + // specific fields - grading rules + &.grade-rules { + + #field-course-grading-graceperiod { + width: flex-grid(3, 9); + } + } + + &.assignment-types { + + .list-input { + + &:last-child { + margin-bottom: 0; + } + } + + .field-group { + @include clearfix(); + width: flex-grid(9, 9); + margin-bottom: ($baseline*1.5); + border-bottom: 1px solid $gray-l5; + padding-bottom: ($baseline*1.5); + + &:last-child { + border: none; + padding-bottom: 0; + } + + .field { + display: inline-block; + vertical-align: top; + width: flex-grid(3, 6); + margin-bottom: ($baseline/2); + margin-right: flex-gutter(); + } + + #field-course-grading-assignment-shortname, + #field-course-grading-assignment-totalassignments, + #field-course-grading-assignment-gradeweight, + #field-course-grading-assignment-droppable { + width: flex-grid(2, 6); + } + } + + .actions { + float: left; + width: flex-grid(9, 9); + + .delete-button { + margin: 0; + } + } + } + + // specific fields - advanced settings + &.advanced-policies { + + .field-group { + margin-bottom: ($baseline*1.5); + + &:last-child { + border: none; + padding-bottom: 0; + } + } + + .course-advanced-policy-list-item { + @include clearfix(); + position: relative; + + .field { + + input { + width: 100%; + } + + .tip { + @include transition (opacity 0.5s ease-in-out 0s); + opacity: 0; + position: absolute; + bottom: ($baseline*1.25); + } + + input:focus { + + & + .tip { + opacity: 1.0; + } + } + + input.error { + + & + .tip { + opacity: 0; + } + } + } + + .key, .value { + float: left; + margin: 0 0 ($baseline/2) 0; + } + + .key { + width: flex-grid(3, 9); + margin-right: flex-gutter(); + } + + .value { + width: flex-grid(6, 9); + } + + .actions { + float: left; + width: flex-grid(9, 9); + + .delete-button { + margin: 0; + } + } + } + + .message-error { + position: absolute; + bottom: ($baseline*0.75); + } + + // specific to code mirror instance in JSON policy editing, need to sync up with other similar code mirror UIs + .CodeMirror { + @include font-size(16); + @include box-sizing(border-box); + @include box-shadow(0 1px 2px rgba(0, 0, 0, .1) inset); + @include linear-gradient($lightGrey, tint($lightGrey, 90%)); + padding: 5px 8px; + border: 1px solid $mediumGrey; + border-radius: 2px; + background-color: $lightGrey; + font-family: 'Open Sans', sans-serif; + color: $baseFontColor; + outline: 0; + + &.CodeMirror-focused { + @include linear-gradient($paleYellow, tint($paleYellow, 90%)); + outline: 0; + } + + .CodeMirror-scroll { + overflow: hidden; + height: auto; + min-height: ($baseline*1.5); + max-height: ($baseline*10); + } + + // editor color changes just for JSON + .CodeMirror-lines { + + .cm-string { + color: #cb9c40; + } + + pre { + line-height: 2.0rem; + } + } + } + } + } + + .content-supplementary { + width: flex-grid(3, 12); + } +} \ No newline at end of file diff --git a/cms/static/sass/views/_static-pages.scss b/cms/static/sass/views/_static-pages.scss new file mode 100644 index 0000000000..83856c773e --- /dev/null +++ b/cms/static/sass/views/_static-pages.scss @@ -0,0 +1,156 @@ +// studio - views - course static pages +// ==================== + +.static-pages { + .new-static-page-button { + @include grey-button; + display: block; + text-align: center; + padding: 12px 0; + } + + .unit-body { + padding: 0; + + .details { + display: block !important; + + h2 { + margin: 0 0 5px 0; + } + } + } + + .component-editor { + border: none; + border-radius: 0; + } + + .components > li { + margin: 0; + border-radius: 0; + + &.new-component-item { + background: transparent; + border: none; + @include box-shadow(none); + } + } + + .component { + border: 1px solid $mediumGrey; + border-top: none; + + &:first-child { + border-top: 1px solid $mediumGrey; + } + + &:hover { + border: 1px solid $mediumGrey; + border-top: none; + + &:first-child { + border-top: 1px solid $mediumGrey; + } + + .drag-handle { + background: url(../img/drag-handles.png) center no-repeat #fff; + } + } + + .drag-handle { + top: 0; + right: 0; + z-index: 11; + width: 35px; + border: none; + background: url(../img/drag-handles.png) center no-repeat #fff; + + &:hover { + background: url(../img/drag-handles.png) center no-repeat #fff; + } + } + + .component-actions { + top: 26px; + right: 44px; + } + } + + .component.editing { + border-left: 1px solid $mediumGrey; + border-right: 1px solid $mediumGrey; + + .xmodule_display { + display: none; + } + } + + .new .xmodule_display { + background: $yellow; + } + + .xmodule_display { + padding: 20px 20px 22px; + font-size: 24px; + font-weight: 300; + background: #fff; + @include transition(background-color 3s); + } + + .static-page-item { + position: relative; + margin: 10px 0; + padding: 22px 20px; + border: 1px solid $darkGrey; + border-radius: 3px; + background: #fff; + @include box-shadow(0 1px 2px rgba(0, 0, 0, .1)); + + .page-name { + font-size: 19px; + font-weight: 700; + } + + .item-actions { + margin-top: 19px; + margin-right: 12px; + } + } +} + +.edit-static-page { + .main-wrapper { + margin-top: 40px; + } + + .static-page-details { + @extend .window; + padding: 32px 40px; + + .row { + border: none; + } + } + + .page-display-name-input { + width: 100%; + font-size: 20px; + } + + .page-contents { + @include box-sizing(border-box); + width: 100%; + height: 360px; + padding: 15px; + border: 1px solid #b0b6c2; + border-radius: 2px; + @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); + font-family: Monaco, monospace; + font-size: 13px; + color: #3c3c3c; + outline: 0; + } +} \ No newline at end of file diff --git a/cms/static/sass/views/_subsection.scss b/cms/static/sass/views/_subsection.scss new file mode 100644 index 0000000000..9b0789ba52 --- /dev/null +++ b/cms/static/sass/views/_subsection.scss @@ -0,0 +1,298 @@ +// studio - views - course subsection +// ==================== + +.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/views/_unit.scss b/cms/static/sass/views/_unit.scss new file mode 100644 index 0000000000..bcd3fdb912 --- /dev/null +++ b/cms/static/sass/views/_unit.scss @@ -0,0 +1,670 @@ +// studio - views - 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 { + .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/views/_updates.scss b/cms/static/sass/views/_updates.scss new file mode 100644 index 0000000000..1a4a54ca5e --- /dev/null +++ b/cms/static/sass/views/_updates.scss @@ -0,0 +1,221 @@ +// studio - views - course updates +// ==================== + +.course-info { + h2 { + margin-bottom: 24px; + font-size: 22px; + font-weight: 300; + } + + .course-info-wrapper { + display: table; + width: 100%; + clear: both; + } + + .main-column, + .course-handouts { + float: none; + display: table-cell; + } + + .main-column { + border-radius: 3px 0 0 3px; + border-right-color: $mediumGrey; + } + + .CodeMirror { + border: 1px solid #3c3c3c; + background: #fff; + color: #3c3c3c; + } +} + +.course-updates { + padding: 30px 40px; + margin: 0; + + .update-list > li { + padding: 34px 0 42px; + border-top: 1px solid #cbd1db; + + &:first-child { + padding-top: 0; + border: none; + } + + &.editing { + position: relative; + z-index: 1001; + padding: 0; + border-top: none; + border-radius: 3px; + background: #fff; + + .post-preview { + display: none; + } + } + + h1 { + float: none; + font-size: 24px; + font-weight: 300; + } + + h2 { + margin-bottom: 18px; + font-size: 14px; + font-weight: 700; + line-height: 30px; + color: #646464; + letter-spacing: 1px; + text-transform: uppercase; + } + + h3 { + margin: 34px 0 11px; + font-size: 16px; + font-weight: 700; + } + } + + .update-contents { + p { + font-size: 16px; + line-height: 25px; + } + + p + p { + margin-top: 25px; + } + + .primary { + border: 1px solid #ddd; + background: #f6f6f6; + padding: 20px; + } + + ol, ul { + margin: 1em 0; + padding: 0 0 0 1em; + color: $baseFontColor; + + li { + margin-bottom: 0.708em; + } + } + + ol { + list-style: decimal outside none; + } + + ul { + list-style: disc outside none; + } + + pre { + margin: 1em 0; + color: $baseFontColor; + font-family: monospace, serif; + font-size: 1em; + white-space: pre-wrap; + word-wrap: break-word; + } + + code { + color: $baseFontColor; + font-family: monospace, serif; + background: none; + padding: 0; + } + } + + .new-update-button { + @include blue-button; + display: block; + text-align: center; + padding: 18px 0; + margin-bottom: 28px; + } + + .new-update-form { + @include edit-box; + margin-bottom: 24px; + padding: 30px; + border: none; + + textarea { + height: 180px; + } + } + + .post-actions { + float: right; + + .edit-button, + .delete-button{ + float: left; + @include white-button; + padding: 3px 10px 4px; + margin-left: 7px; + font-size: 12px; + font-weight: 400; + + .edit-icon, + .delete-icon { + margin-right: 4px; + } + } + } +} + +.course-handouts { + width: 30%; + padding: 20px 30px; + margin: 0; + border-radius: 0 3px 3px 0; + border-left: none; + background: $lightGrey; + + h2 { + font-size: 18px; + font-weight: 700; + } + + .edit-button { + float: right; + @include white-button; + padding: 3px 10px 4px; + margin-left: 7px; + font-size: 12px; + font-weight: 400; + + .edit-icon, + .delete-icon { + margin-right: 4px; + } + } + + .handouts-content { + font-size: 14px; + } + + .treeview-handoutsnav li { + margin-bottom: 12px; + } +} + +.edit-handouts-form { + @include edit-box; + position: absolute; + right: 0; + z-index: 10001; + width: 800px; + padding: 30px; + + textarea { + height: 300px; + } +} \ No newline at end of file diff --git a/cms/static/sass/views/_users.scss b/cms/static/sass/views/_users.scss new file mode 100644 index 0000000000..6423bddd75 --- /dev/null +++ b/cms/static/sass/views/_users.scss @@ -0,0 +1,81 @@ +// studio - views - course users +// ==================== + +.users { + .new-user-form { + display: none; + padding: 15px 20px; + background-color: $lightBluishGrey2; + + #result { + display: none; + float: left; + margin-bottom: 15px; + padding: 3px 15px; + border-radius: 3px; + background: $error-red; + font-size: 14px; + color: #fff; + } + + .form-elements { + clear: both; + } + + label { + display: inline-block; + margin-right: 10px; + } + + .email-input { + width: 350px; + padding: 8px 8px 10px; + border-color: $darkGrey; + } + + .add-button { + @include blue-button; + padding: 5px 20px 9px; + } + + .cancel-button { + @include white-button; + padding: 5px 20px 9px; + } + } + + .user-list { + border: 1px solid $mediumGrey; + background: #fff; + + li { + position: relative; + padding: 20px; + border-bottom: 1px solid $mediumGrey; + + &:last-child { + border-bottom: none; + } + + span { + display: inline-block; + } + + .user-name { + margin-right: 10px; + font-size: 24px; + font-weight: 300; + } + + .user-email { + font-size: 14px; + font-style: italic; + color: $mediumGrey; + } + + .item-actions { + top: 24px; + } + } + } +} \ No newline at end of file From e9a2413c0483f0b15dff6882f015777ccfd9dbba Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Fri, 8 Mar 2013 12:35:28 -0500 Subject: [PATCH 010/436] studio - further small cleanup on Sass --- cms/static/sass/base-style.scss | 46 +++++++++++------------ cms/static/sass/elements/_header.scss | 4 ++ cms/static/sass/elements/_navigation.scss | 7 ---- 3 files changed, 27 insertions(+), 30 deletions(-) diff --git a/cms/static/sass/base-style.scss b/cms/static/sass/base-style.scss index 33b312d235..4b9edea40c 100644 --- a/cms/static/sass/base-style.scss +++ b/cms/static/sass/base-style.scss @@ -9,39 +9,39 @@ // utilities @import 'mixins'; -@import "variables"; -@import "cms_mixins"; +@import 'variables'; +@import 'cms_mixins'; // assets -@import "assets/fonts"; -@import "assets/graphics"; +@import 'assets/fonts'; +@import 'assets/graphics'; @import 'assets/keyframes'; // base -@import "base"; +@import 'base'; // elements -@import "elements/header"; -@import "elements/footer"; -@import "elements/navigation"; -@import "elements/modal"; -@import "elements/alerts"; +@import 'elements/header'; +@import 'elements/footer'; +@import 'elements/navigation'; +@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 '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'; diff --git a/cms/static/sass/elements/_header.scss b/cms/static/sass/elements/_header.scss index 1e09184801..432e78895a 100644 --- a/cms/static/sass/elements/_header.scss +++ b/cms/static/sass/elements/_header.scss @@ -29,6 +29,10 @@ margin: 0 auto; color: $gray-l1; } + + nav .nav-item { + display: inline-block; + } } // ==================== diff --git a/cms/static/sass/elements/_navigation.scss b/cms/static/sass/elements/_navigation.scss index 8c123db64f..066c47298b 100644 --- a/cms/static/sass/elements/_navigation.scss +++ b/cms/static/sass/elements/_navigation.scss @@ -6,13 +6,6 @@ // ==================== // primary -nav.primary { - - .nav-item { - display: inline-block; - } -} - // ==================== From 786ffd10925774f6dbd255338720325b5cc7bf81 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Fri, 8 Mar 2013 12:44:24 -0500 Subject: [PATCH 011/436] studio - further small cleanup on Sass --- cms/static/sass/_base.scss | 8 -------- common/static/sass/_mixins.scss | 22 ++++++++++++++++++++-- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss index 1bf9119654..e5d389e6d2 100644 --- a/cms/static/sass/_base.scss +++ b/cms/static/sass/_base.scss @@ -455,14 +455,6 @@ code { // ==================== // 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; -} // ==================== diff --git a/common/static/sass/_mixins.scss b/common/static/sass/_mixins.scss index 3145745906..64eaf20591 100644 --- a/common/static/sass/_mixins.scss +++ b/common/static/sass/_mixins.scss @@ -1,4 +1,4 @@ -// all - utilities - mixins and extends +// studio - utilities - mixins and extends // ==================== // font-sizing @@ -151,4 +151,22 @@ border: none; } -// extends - ui \ No newline at end of file +// 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 From 3bc73de1b494adac6342bc1300b69c415780ba38 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Fri, 8 Mar 2013 12:56:00 -0500 Subject: [PATCH 012/436] studio - changed order of Sass import to account for variables needed in architecture --- cms/static/sass/base-style.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/static/sass/base-style.scss b/cms/static/sass/base-style.scss index 4b9edea40c..750fa9071a 100644 --- a/cms/static/sass/base-style.scss +++ b/cms/static/sass/base-style.scss @@ -8,8 +8,8 @@ @import 'reset'; // utilities -@import 'mixins'; @import 'variables'; +@import 'mixins'; @import 'cms_mixins'; // assets From 7a9fdb90696688ae97d75a9bb7bfd9f8ca5b9643 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Mon, 11 Mar 2013 11:04:43 -0400 Subject: [PATCH 013/436] studio - sass clean up: in progress business with git ignore issues --- cms/static/sass/base-style.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cms/static/sass/base-style.scss b/cms/static/sass/base-style.scss index 750fa9071a..46885864ac 100644 --- a/cms/static/sass/base-style.scss +++ b/cms/static/sass/base-style.scss @@ -45,5 +45,5 @@ @import 'assets/content-types'; -@import 'xmodule/module/module-styles.scss'; -@import 'xmodule/descriptor/module-styles.scss'; +@import 'module/module-styles.scss'; +@import 'descriptor/module-styles.scss'; From a496cc0a1e64d1c8c74ccbddba7cd0f3fcf70655 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Mon, 11 Mar 2013 13:28:28 -0400 Subject: [PATCH 014/436] studio - Sass Cleanup: moved all page/view specific css to their own views and created forms specific sheet --- cms/static/sass/_base.scss | 99 +- cms/static/sass/base-style.scss | 1 + cms/static/sass/elements/_footer.scss | 2 +- cms/static/sass/elements/_forms.scss | 76 ++ cms/static/sass/elements/_header.scss | 2 +- cms/static/sass/views/_assets.scss | 3 +- cms/static/sass/views/_dashboard.scss | 203 ++-- cms/static/sass/views/_export.scss | 5 +- cms/static/sass/views/_import.scss | 3 +- cms/static/sass/views/_index.scss | 2 +- cms/static/sass/views/_outline.scss | 1211 +++++++++++----------- cms/static/sass/views/_static-pages.scss | 3 +- cms/static/sass/views/_subsection.scss | 672 +++++++----- cms/static/sass/views/_unit.scss | 1187 ++++++++++----------- cms/static/sass/views/_updates.scss | 3 +- cms/static/sass/views/_users.scss | 3 +- 16 files changed, 1819 insertions(+), 1656 deletions(-) create mode 100644 cms/static/sass/elements/_forms.scss diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss index e5d389e6d2..bdafbad09a 100644 --- a/cms/static/sass/_base.scss +++ b/cms/static/sass/_base.scss @@ -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,86 +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; - } -} - -// 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 - -// ==================== - // UI - actions .new-unit-item, .new-subsection-item, @@ -838,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/base-style.scss b/cms/static/sass/base-style.scss index 46885864ac..b092f0054b 100644 --- a/cms/static/sass/base-style.scss +++ b/cms/static/sass/base-style.scss @@ -24,6 +24,7 @@ @import 'elements/header'; @import 'elements/footer'; @import 'elements/navigation'; +@import 'elements/forms'; @import 'elements/modal'; @import 'elements/alerts'; @import 'elements/jquery-ui-calendar'; diff --git a/cms/static/sass/elements/_footer.scss b/cms/static/sass/elements/_footer.scss index 2c32de4bb3..b1c0f57bb2 100644 --- a/cms/static/sass/elements/_footer.scss +++ b/cms/static/sass/elements/_footer.scss @@ -1,4 +1,4 @@ -// studio - global footer +// studio - elements - global footer // ==================== .wrapper-footer { 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/elements/_header.scss b/cms/static/sass/elements/_header.scss index 432e78895a..e8df37f57f 100644 --- a/cms/static/sass/elements/_header.scss +++ b/cms/static/sass/elements/_header.scss @@ -1,4 +1,4 @@ -// studio - global header +// studio - elements - global header // ==================== .wrapper-header { diff --git a/cms/static/sass/views/_assets.scss b/cms/static/sass/views/_assets.scss index 2c1b435c44..779dc56684 100644 --- a/cms/static/sass/views/_assets.scss +++ b/cms/static/sass/views/_assets.scss @@ -1,7 +1,8 @@ // studio - views - assets // ==================== -.uploads { +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 index 2a995ffdc7..a02c4e0c29 100644 --- a/cms/static/sass/views/_dashboard.scss +++ b/cms/static/sass/views/_dashboard.scss @@ -1,117 +1,124 @@ // studio - views - user dashboard // ==================== -.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)); +body.dashboard { - li { - position: relative; - border-bottom: 1px solid $mediumGrey; + .my-classes { + margin-top: $baseline; + } - &:last-child { - border-bottom: none; + .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-link { - z-index: 100; + .class-name { display: block; - padding: 20px 25px; - line-height: 1.3; - - &:hover { - background: $paleYellow; + font-size: 19px; + font-weight: 300; + } - + .view-live-button { - opacity: 1.0; - pointer-events: auto; - } + .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; } } } - .class-name { - display: block; - font-size: 19px; - font-weight: 300; - } + .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; - .detail { - font-size: 14px; - font-weight: 400; - margin-right: 20px; - color: #3c3c3c; - } + .row { + margin-bottom: 15px; + @include clearfix; + } - // 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; + .column { + float: left; + width: 48%; + } - &:hover { - opacity: 1.0; - pointer-events: auto; + .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; } } -} - -.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/views/_export.scss b/cms/static/sass/views/_export.scss index 27cb7f923b..933bb50252 100644 --- a/cms/static/sass/views/_export.scss +++ b/cms/static/sass/views/_export.scss @@ -1,7 +1,8 @@ // studio - views - course export // ==================== -.export { +body.course.export { + .export-overview { @extend .window; @include clearfix; @@ -121,6 +122,4 @@ } } } - - } \ No newline at end of file diff --git a/cms/static/sass/views/_import.scss b/cms/static/sass/views/_import.scss index fe7d65626b..e5fb955348 100644 --- a/cms/static/sass/views/_import.scss +++ b/cms/static/sass/views/_import.scss @@ -1,7 +1,8 @@ // studio - views - course import // ==================== -.import { +body.course.import { + .import-overview { @extend .window; @include clearfix; diff --git a/cms/static/sass/views/_index.scss b/cms/static/sass/views/_index.scss index 8d81ea33fd..f4087a8605 100644 --- a/cms/static/sass/views/_index.scss +++ b/cms/static/sass/views/_index.scss @@ -1,7 +1,7 @@ // studio - views - how it works // ==================== -.index { +body.index { &.not-signedin { diff --git a/cms/static/sass/views/_outline.scss b/cms/static/sass/views/_outline.scss index 2957b57849..0d72e2d2bf 100644 --- a/cms/static/sass/views/_outline.scss +++ b/cms/static/sass/views/_outline.scss @@ -1,380 +1,102 @@ // studio - views - course outline // ==================== -input.courseware-unit-search-input { - float: left; - width: 260px; - background-color: #fff; -} +body.course.outline { -.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; - } - } - } - } + input.courseware-unit-search-input { + float: left; + width: 260px; + background-color: #fff; } + .branch { -.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; + .section-item { @include clearfix(); - .item-details, .section-published-date { - + .details { + display: block; + float: left; + margin-bottom: 0; + width: 650px; } - .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 { + .gradable-status { float: right; - width: 265px; - margin-right: 220px; - @include border-radius(3px); - background: $lightGrey; + position: relative; + top: -4px; + right: 50px; + width: 145px; - .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 { + .status-label { position: absolute; - top: 20px; - right: 70px; - width: 145px; + 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; + } - .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: 0; + right: 5px; + padding: 5px; + color: $mediumGrey; - .menu-toggle { - z-index: 10; - position: absolute; - top: 2px; - right: 5px; - padding: 5px; - color: $lightGrey; - - &:hover, &.is-active { - color: $blue; + &: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); + .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; + li { + width: 115px; + margin-bottom: 3px; + padding-bottom: 3px; + border-bottom: 1px solid $lightGrey; - &:last-child { - margin-bottom: 0; - padding-bottom: 0; - border: none; + &:last-child { + margin-bottom: 0; + padding-bottom: 0; + border: none; - a { - color: $darkGrey; + a { + color: $darkGrey; + } } } - } - a { + a { + color: $blue; - &.is-selected { - font-weight: bold; - } + &.is-selected { + font-weight: bold; } + } } // dropdown state @@ -386,306 +108,573 @@ input.courseware-unit-search-input { opacity: 1.0; } + .menu-toggle { + z-index: 10000; + } + } - .menu-toggle { - z-index: 10000; + // set state + &.is-set { + + .menu-toggle { + color: $blue; + } + + .status-label { + display: block; + color: $blue; + } } } - - // 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; - } - } + .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)); - .expand-collapse-icon { - float: left; - margin: 29px 6px 16px 16px; - @include transition(none); + &:first-child { + margin-top: 0; + } - &.expand { - background-position: 0 0; - } + &.collapsed { + padding-bottom: 0; + } - &.collapsed { - - } - } + label { + float: left; + line-height: 29px; + } - .drag-handle { - margin-left: 11px; - } - } + .datepair { + float: left; + margin-left: 10px; + } - h3 { - font-size: 19px; - font-weight: 700; - color: $blue; - } + .section-published-date { + position: absolute; + top: 19px; + right: 90px; + padding: 4px 10px; + border-radius: 3px; + background: $lightGrey; + text-align: right; - .section-name-span { - cursor: pointer; - @include transition(color .15s); + .published-status { + font-size: 12px; + margin-right: 15px; - &: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%; + 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; } - } -} + } -.toggle-button-sections { - display: none; - position: relative; - float: right; - margin-top: 10px; + .item-actions { + margin-top: 21px; + margin-right: 12px; - font-size: 13px; - color: $darkGrey; + .edit-button, + .delete-button { + margin-top: -3px; + } + } - &.is-shown { - display: block; - } + .expand-collapse-icon { + float: left; + margin: 29px 6px 16px 16px; + @include transition(none); - .ss-icon { - @include border-radius(20px); - position: relative; - top: -1px; - display: inline-block; - margin-right: 2px; - line-height: 5px; - font-size: 11px; - } + &.expand { + background-position: 0 0; + } - .label { - display: inline-block; - } -} + &.collapsed { + + } + } -.new-section-name, -.new-subsection-name-input { - width: 515px; -} + .drag-handle { + margin-left: 11px; + } + } -.new-section-name-save, -.new-subsection-name-save { - @include blue-button; - padding: 4px 20px 7px; - margin: 0 5px; - color: #fff !important; -} + h3 { + font-size: 19px; + font-weight: 700; + color: $blue; + } -.new-section-name-cancel, -.new-subsection-name-cancel { - @include white-button; - padding: 4px 20px 7px; - color: #8891a1 !important; -} + .section-name-span { + cursor: pointer; + @include transition(color .15s); -.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)); -} + &:hover { + color: $orange; + } + } -.unit-name-input { - padding: 20px 40px; + .section-name-form { + margin-bottom: 15px; + } - label { - display: block; - } + .section-name-edit { + input { + font-size: 16px; + } + + .save-button { + @include blue-button; + padding: 7px 20px 7px; + margin-right: 5px; + } - input { - width: 100%; - font-size: 20px; - } -} + .cancel-button { + @include white-button; + padding: 7px 20px 7px; + } + } -.preview { - background: url(../img/preview.jpg) center top no-repeat; -} + h4 { + font-size: 12px; + color: #878e9d; -.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; + strong { + font-weight: bold; + } + } - .settings { - padding: 40px; - } + .list-header { + @include linear-gradient(top, transparent, rgba(0, 0, 0, .1)); + background-color: #ced2db; + border-radius: 3px 3px 0 0; + } - h3 { - font-size: 34px; - font-weight: 300; - } + .subsection-list { + margin: 0 12px; - .picker { - margin: 30px 0 65px; - } + > ol { + @include tree-view; + border-top-width: 0; + } + } - .description { - margin-top: 30px; - font-size: 14px; - line-height: 20px; - } + &.new-section { - strong { - font-weight: 700; - } + header { + height: auto; + @include clearfix(); + } - .start-date, - .start-time { - font-size: 19px; - } + .expand-collapse-icon { + visibility: hidden; + } - .save-button { - @include blue-button; - margin-right: 10px; - } + .item-details { + padding: 25px 0 0 0; - .cancel-button { - @include white-button; - } + .section-name { + float: none; + width: 100%; + } + } + } + } - .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 { + .toggle-button-sections { display: none; - } -} + position: relative; + float: right; + margin-top: 10px; -ol.ui-droppable .branch:first-child .section-item { - border-top: none; -} + 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/views/_static-pages.scss b/cms/static/sass/views/_static-pages.scss index 83856c773e..bc9bccf1bb 100644 --- a/cms/static/sass/views/_static-pages.scss +++ b/cms/static/sass/views/_static-pages.scss @@ -1,7 +1,8 @@ // studio - views - course static pages // ==================== -.static-pages { +body.course.static-pages { + .new-static-page-button { @include grey-button; display: block; diff --git a/cms/static/sass/views/_subsection.scss b/cms/static/sass/views/_subsection.scss index 9b0789ba52..3c6bfa9f11 100644 --- a/cms/static/sass/views/_subsection.scss +++ b/cms/static/sass/views/_subsection.scss @@ -1,297 +1,449 @@ // studio - views - course subsection // ==================== -.subsection .main-wrapper { - margin: 40px; -} +body.course.subsection { -.subsection .inner-wrapper { - @include clearfix(); -} + .unit-settings { + .window-contents { + padding: 10px 20px; + } -.subsection-body { - padding: 32px 40px; - @include clearfix; + .unit-actions { + border-bottom: none; + padding-bottom: 0; + } - > div { - margin-bottom: 40px; - } + .published-alert { + display: none; + padding: 10px; + border: 1px solid #edbd3c; + border-radius: 3px; + background: #fbf6e1; + font-size: 14px; + line-height: 1.4; - input { - font-size: 14px; - } + div { + margin-top: 15px; + } + } - .unit-subtitle { - display: block; - width: 100%; - } + input[type="radio"] { + margin-right: 7px; + } - .sortable-unit-list { - ol { - @include tree-view; - } - } + .status { + font-size: 12px; - .policy-list { - input[disabled] { - border: none; - @include box-shadow(none); - } + strong { + font-weight: 700; + } + } - .policy-list-name { - margin-right: 5px; - margin-bottom: 10px; - } + .preview-button, .view-button { + @include white-button; + margin-bottom: 10px; + } - .policy-list-value { - width: 320px; - margin-right: 10px; - } - } + .publish-button { + @include orange-button; + } - .policy-list-element { - .save-button, - .cancel-button { - display: none; - } + .delete-button { + @include blue-button; + } - .edit-icon { - margin-right: 8px; - } + .delete-draft { + display: inline-block; + } - &.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; + .delete-button, + .preview-button, + .publish-button, + .view-button { + font-size: 11px; + margin-top: 10px; + padding: 6px 15px 8px; + } } - .gradable-status { - position: relative; - top: -4px; - display: inline-block; - margin-left: 10px; - width: 65%; + .unit-history { + &.collapsed { + h4 { + border-bottom: none; + border-radius: 3px; + } - .status-label { - margin: 0; - padding: 0; - background: transparent; - color: $blue; - border: none; + .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; } - .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; + .date-setter { + @include clearfix; 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 { + .remove-date { + display: block; + } + } - .menu { - z-index: 10000; - display: block; - opacity: 1.0; - } - - .menu-toggle { - z-index: 1000; - } + .row.visibility { + label { + display: inline-block !important; + margin-right: 10px; + line-height: 21px; } - // set state - &.is-set { + a { + display: inline-block; + height: 31px; + margin-right: 8px; + vertical-align: middle; + font-size: 11px; + font-weight: 700; + line-height: 31px; + text-transform: uppercase; + } - .menu-toggle { - color: $blue; + .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 { - display: block; + 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; + } } } } diff --git a/cms/static/sass/views/_unit.scss b/cms/static/sass/views/_unit.scss index bcd3fdb912..a9ce1c8c4d 100644 --- a/cms/static/sass/views/_unit.scss +++ b/cms/static/sass/views/_unit.scss @@ -1,670 +1,687 @@ -// studio - views - course unit +// studio - views - unit // ==================== -.unit .main-wrapper { - @include clearfix(); - margin: 40px; -} +body.course.unit { -//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 .main-wrapper { + @include clearfix(); + margin: 40px; } -} -.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; + //Problem Selector tab menu requirements + .js .tabs .tab { + display: none; + } + //end problem selector reqs - li { - float: left; - } + .main-column { + clear: both; + float: left; + width: 70%; + } - a, - .current-page { - display: block; - padding: 15px 35px 15px 30px; - font-size: 14px; - background: url(../img/breadcrumb-arrow.png) no-repeat right center; + .unit-body.published { + .components > li { + border: none; + + .rendered-component { + padding: 0 20px; + } } } - h2 { - margin: 30px 40px 30px 0; - color: #646464; - font-size: 19px; - font-weight: 300; - letter-spacing: 1px; - text-transform: uppercase; - } + .unit-body { + + .unit-name-input { + padding: 20px 40px; - .components { - - > li { - position: relative; - z-index: 10; - margin: 20px 40px; - - - - .title { - margin: 0 0 15px 0; - color: $mediumGrey; - - .value { - } + label { + display: block; } - &.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; + 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; - .new-component-button { - display: block; - padding: 20px; - text-align: center; - color: #edf1f5; + 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 { + } } - h5 { + &.new-component-item { margin: 20px 0px; - color: #fff; - font-weight: 600; - font-size: 18px; - } + 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; - .rendered-component { - display: none; - background: #fff; - border-radius: 3px 3px 0 0; - } - - .new-component-type { - - a, - li { - display: inline-block; + .new-component-button { + display: block; + padding: 20px; + text-align: center; + color: #edf1f5; } - a { - border: 1px solid $mediumGrey; - width: 100px; - height: 100px; + h5 { + margin: 20px 0px; 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); + font-weight: 600; + font-size: 18px; + } - .name { - position: absolute; - bottom: 5px; - left: 0; - width: 100%; - padding: 10px; - @include box-sizing(border-box); + .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 { + .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; - // specific menu types - &.new-component-problem { - padding-bottom:10px; - - .ss-icon, .editor-indicator { - display: inline-block; + .cancel-button { + margin: 20px 0px 10px 10px; + @include white-button; } .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; + // specific menu types + &.new-component-problem { + padding-bottom:10px; - .ss-icon { - opacity: 1.0; + .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 { - opacity: 1.0; + @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; + } } } } - // specific editor types - .empty { + .new-component { + text-align: center; - a { - line-height: 1.4; - font-weight: 400; - background: #fff; - color: #3c3c3c; - - - &:hover { - background: tint($green,30%); - color: #fff; - } + h5 { + color: $darkGreen; } + } } - - .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 { + .component { 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; + background: #fff; + @include transition(none); &:hover { - background: #fffcf1; + border-color: #6696d7; - .item-actions { - display: block; + .drag-handle { + background-color: $blue; + border-color: $blue; } } - &.checked { - background: #d1dae3; + &.editing { + border: 1px solid $lightBluishGrey2; + z-index: auto; + + .drag-handle, + .component-actions { + display: none; + } } - .item-actions { - display: none; + &.component-placeholder { + border-color: #6696d7; } - input[type="radio"] { - margin-right: 7px; + .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-location { - .url { - width: 100%; - margin-bottom: 10px; - @include box-shadow(none); - } + .unit-settings { + .window-contents { + padding: 10px 20px; + } - .draft-tag, - .hidden-tag, - .private-tag, - .has-new-draft-tag { - font-size: 8px; - } + .unit-actions { + border-bottom: none; + padding-bottom: 0; + } - .window-contents > ol { - @include tree-view; + .published-alert { + display: none; + padding: 10px; + border: 1px solid #edbd3c; + border-radius: 3px; + background: #fbf6e1; + font-size: 14px; + line-height: 1.4; - .section-item { + 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; - width: 100%; + } + + .delete-button, + .preview-button, + .publish-button, + .view-button { font-size: 11px; - padding: 2px 8px 4px; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - @include box-sizing(border-box); + margin-top: 10px; + padding: 6px 15px 8px; + } + } + + .unit-history { + &.collapsed { + h4 { + border-bottom: none; + border-radius: 3px; + } + + .window-contents { + display: none; + } } ol { - .section-item { - padding-left: 20px; - } + border: 1px solid #ced2db; - .new-unit-item { - margin-left: 20px; - } - } + li { + display: block; + padding: 6px 8px 8px 10px; + background: #edf1f5; + font-size: 12px; - ol ol { - .section-item { - padding-left: 34px; - } + &:hover { + background: #fffcf1; - .new-unit-item { - margin: 0 0 10px 41px; + .item-actions { + display: block; + } + } + + &.checked { + background: #d1dae3; + } + + .item-actions { + display: none; + } + + input[type="radio"] { + margin-right: 7px; + } } } } -} -.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; + .unit-location { + .url { width: 100%; - padding: 15px; - top: 0; - left: 0; - border-bottom: 1px solid $lightBluishGrey2; - background: $lightGrey; + margin-bottom: 10px; + @include box-shadow(none); } - &.editing { - padding-top: 0; + .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/views/_updates.scss b/cms/static/sass/views/_updates.scss index 1a4a54ca5e..8d92c9d860 100644 --- a/cms/static/sass/views/_updates.scss +++ b/cms/static/sass/views/_updates.scss @@ -1,7 +1,8 @@ // studio - views - course updates // ==================== -.course-info { +body.course.updates { + h2 { margin-bottom: 24px; font-size: 22px; diff --git a/cms/static/sass/views/_users.scss b/cms/static/sass/views/_users.scss index 6423bddd75..ecaa319707 100644 --- a/cms/static/sass/views/_users.scss +++ b/cms/static/sass/views/_users.scss @@ -1,7 +1,8 @@ // studio - views - course users // ==================== -.users { +body.course.users { + .new-user-form { display: none; padding: 15px 20px; From 094458dd6f0e4437a71dcbcd990d31286725dc16 Mon Sep 17 00:00:00 2001 From: Arthur Barrett Date: Mon, 11 Mar 2013 16:19:36 -0400 Subject: [PATCH 015/436] Modified tooltip positioning on non-overlapping annotation spans. --- .../xmodule/js/src/annotatable/display.coffee | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee b/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee index 2ad49ae6d7..523b0e99cf 100644 --- a/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee @@ -75,6 +75,7 @@ class @Annotatable classes: 'ui-tooltip-annotatable' events: show: @onShowTip + move: @onMoveTip onClickToggleAnnotations: (e) => @toggleAnnotations() @@ -87,6 +88,40 @@ class @Annotatable onShowTip: (event, api) => event.preventDefault() if @annotationsHidden + onMoveTip: (event, api, position) => + ### + This method handles an edge case in which a tooltip is displayed above + a non-overlapping span like this: + + (( TOOLTIP )) + \/ + text text text ... text text text ...... + + + The problem is that the tooltip looks disconnected from both spans, so + we should re-position the tooltip to appear above the span. + ### + + tip = api.elements.tooltip + adjust_y = api.options.position?.adjust?.y || 0 + target = api.elements.target + rects = $(target).get(0).getClientRects() + is_non_overlapping = (rects?.length == 2 and rects[0].left > rects[1].right) + + if is_non_overlapping + focus_rect = rects[0] + rect_center = focus_rect.left + (focus_rect.width / 2) + rect_top = focus_rect.top + tip_width = $(tip).width() + tip_height = $(tip).height() + tip_left = rect_center - (tip_width / 2) + tip_top = window.pageYOffset + rect_top - tip_height + adjust_y + win_width = $(window).width() + if tip_left + tip_width > win_width + tip_left = win_width - tip_width + position.left = tip_left + position.top = tip_top + getSpanForProblemReturn: (el) -> problem_id = $(@problemReturnSelector).index(el) @$(@spanSelector).filter("[data-problem-id='#{problem_id}']") From fcf82ba2bc44cb701c020d0494e2537139635f27 Mon Sep 17 00:00:00 2001 From: Arthur Barrett Date: Mon, 11 Mar 2013 18:02:22 -0400 Subject: [PATCH 016/436] fixed pep8 violations for annotation module --- common/lib/xmodule/xmodule/annotatable_module.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/common/lib/xmodule/xmodule/annotatable_module.py b/common/lib/xmodule/xmodule/annotatable_module.py index f093b76f52..1385296ddf 100644 --- a/common/lib/xmodule/xmodule/annotatable_module.py +++ b/common/lib/xmodule/xmodule/annotatable_module.py @@ -11,13 +11,13 @@ from xmodule.contentstore.content import StaticContent log = logging.getLogger(__name__) + class AnnotatableModule(XModule): js = {'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee'), resource_string(__name__, 'js/src/collapsible.coffee'), resource_string(__name__, 'js/src/html/display.coffee'), resource_string(__name__, 'js/src/annotatable/display.coffee')], - 'js': [] - } + 'js': []} js_module_name = "Annotatable" css = {'scss': [resource_string(__name__, 'css/annotatable/display.scss')]} icon_class = 'annotatable' @@ -34,11 +34,11 @@ class AnnotatableModule(XModule): if color is not None: if color in self.highlight_colors: - cls.append('highlight-'+color) + cls.append('highlight-' + color) attr['_delete'] = highlight_key attr['value'] = ' '.join(cls) - return { 'class' : attr } + return {'class': attr} def _get_annotation_data_attr(self, index, el): """ Returns a dict in which the keys are the HTML data attributes @@ -58,7 +58,7 @@ class AnnotatableModule(XModule): if xml_key in el.attrib: value = el.get(xml_key, '') html_key = attrs_map[xml_key] - data_attrs[html_key] = { 'value': value, '_delete': xml_key } + data_attrs[html_key] = {'value': value, '_delete': xml_key} return data_attrs @@ -76,7 +76,6 @@ class AnnotatableModule(XModule): delete_key = attr[key]['_delete'] del el.attrib[delete_key] - def _render_content(self): """ Renders annotatable content with annotation spans and returns HTML. """ xmltree = etree.fromstring(self.content) @@ -123,9 +122,9 @@ class AnnotatableModule(XModule): self.element_id = self.location.html_id() self.highlight_colors = ['yellow', 'orange', 'purple', 'blue', 'green'] + class AnnotatableDescriptor(RawDescriptor): module_class = AnnotatableModule stores_state = True template_dir_name = "annotatable" mako_template = "widgets/raw-edit.html" - From d860b167d6838443d05b5b67805047a7e032f6a3 Mon Sep 17 00:00:00 2001 From: Arthur Barrett Date: Tue, 12 Mar 2013 14:09:56 -0400 Subject: [PATCH 017/436] fixed tooltip positioning for non-overlapping spans in studio --- .../xmodule/css/annotatable/display.scss | 4 +++ .../xmodule/js/src/annotatable/display.coffee | 34 ++++++++++++++----- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/annotatable/display.scss b/common/lib/xmodule/xmodule/css/annotatable/display.scss index f8ae779b8c..6e1a38ee31 100644 --- a/common/lib/xmodule/xmodule/css/annotatable/display.scss +++ b/common/lib/xmodule/xmodule/css/annotatable/display.scss @@ -1,6 +1,10 @@ $border-color: #C8C8C8; $body-font-size: em(14); +.annotatable-wrapper { + position: relative; +} + .annotatable-header { margin-bottom: .5em; .annotatable-title { diff --git a/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee b/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee index 523b0e99cf..e38e48eeda 100644 --- a/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee @@ -1,7 +1,8 @@ class @Annotatable _debug: false - # selectors for the annotatable xmodule + # selectors for the annotatable xmodule + wrapperSelector: '.annotatable-wrapper' toggleAnnotationsSelector: '.annotatable-toggle-annotations' toggleInstructionsSelector: '.annotatable-toggle-instructions' instructionsSelector: '.annotatable-instructions' @@ -61,7 +62,7 @@ class @Annotatable my: 'bottom center' # of tooltip at: 'top center' # of target target: $(el) # where the tooltip was triggered (i.e. the annotation span) - container: @$el + container: @$(@wrapperSelector) adjust: y: -5 show: @@ -104,23 +105,38 @@ class @Annotatable tip = api.elements.tooltip adjust_y = api.options.position?.adjust?.y || 0 + container = api.options.position?.container || $('body') target = api.elements.target + rects = $(target).get(0).getClientRects() is_non_overlapping = (rects?.length == 2 and rects[0].left > rects[1].right) if is_non_overlapping - focus_rect = rects[0] + # we want to choose the largest of the two non-overlapping spans and display + # the tooltip above the center of it (see api.options.position settings) + focus_rect = (if rects[0].width > rects[1].width then rects[0] else rects[1]) rect_center = focus_rect.left + (focus_rect.width / 2) rect_top = focus_rect.top tip_width = $(tip).width() tip_height = $(tip).height() - tip_left = rect_center - (tip_width / 2) - tip_top = window.pageYOffset + rect_top - tip_height + adjust_y + + # tooltip is positioned relative to its container, so we need to factor in offsets + container_offset = $(container).offset() + offset_left = -container_offset.left + offset_top = $('body').scrollTop() - container_offset.top + + tip_left = offset_left + rect_center - (tip_width / 2) + tip_top = offset_top + rect_top - tip_height + adjust_y + + # make sure the new tip position doesn't clip the edges of the screen win_width = $(window).width() - if tip_left + tip_width > win_width - tip_left = win_width - tip_width - position.left = tip_left - position.top = tip_top + if tip_left < offset_left + tip_left = offset_left + else if tip_left + tip_width > win_width + offset_left + tip_left = win_width + offset_left - tip_width + + # final step: update the position object (used by qtip2 to show the tip after the move event) + $.extend position, 'left': tip_left, 'top': tip_top getSpanForProblemReturn: (el) -> problem_id = $(@problemReturnSelector).index(el) From bf6ca1b0e759252795ca89ad905828d30ceada28 Mon Sep 17 00:00:00 2001 From: Arthur Barrett Date: Tue, 12 Mar 2013 17:32:00 -0400 Subject: [PATCH 018/436] use document to get scrollTop for firefox --- common/lib/xmodule/xmodule/js/src/annotatable/display.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee b/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee index e38e48eeda..8a32c8f51e 100644 --- a/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee @@ -123,7 +123,7 @@ class @Annotatable # tooltip is positioned relative to its container, so we need to factor in offsets container_offset = $(container).offset() offset_left = -container_offset.left - offset_top = $('body').scrollTop() - container_offset.top + offset_top = $(document).scrollTop() - container_offset.top tip_left = offset_left + rect_center - (tip_width / 2) tip_top = offset_top + rect_top - tip_height + adjust_y From 1b5f0400212cbd7f1d975cd4da3c523685395aad Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Tue, 12 Mar 2013 21:57:40 -0400 Subject: [PATCH 019/436] studio - checklists: roughed out initial static design, HTML, and most CSS for checklists v0 UI --- cms/djangoapps/contentstore/views.py | 3 + cms/static/sass/_base.scss | 10 +- cms/static/sass/_variables.scss | 3 + cms/static/sass/base-style.scss | 1 + cms/templates/checklists.html | 43 +++++ cms/templates/ux-checklists.html | 246 +++++++++++++++++++++++++++ cms/templates/widgets/header.html | 1 + cms/urls.py | 1 + 8 files changed, 305 insertions(+), 3 deletions(-) create mode 100644 cms/templates/checklists.html create mode 100644 cms/templates/ux-checklists.html diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index c2c80106fa..7394edb4f7 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -113,6 +113,9 @@ def howitworks(request): else: return render_to_response('howitworks.html', {}) +def ux_checklists(request): + return render_to_response('ux-checklists.html', {}) + # ==== Views for any logged-in user ================================== diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss index 5d4bc7c773..23ff2b93e8 100644 --- a/cms/static/sass/_base.scss +++ b/cms/static/sass/_base.scss @@ -214,7 +214,7 @@ h1 { color: $gray-l2; } - .title, .title-1 { + .title-1 { @include font-size(32); margin: 0; padding: 0; @@ -283,8 +283,8 @@ h1 { .title-3 { @include font-size(16); - margin: 0 0 ($baseline/4) 0; - font-weight: 500; + margin: 0 0 ($baseline/2) 0; + font-weight: 600; } .title-4 { @@ -772,6 +772,10 @@ hr.divide { word-wrap: break-word; } +hr.divider { + @extend .sr; +} + // ==================== // js dependant diff --git a/cms/static/sass/_variables.scss b/cms/static/sass/_variables.scss index 4d8e26b2f9..e94ebcb7bc 100644 --- a/cms/static/sass/_variables.scss +++ b/cms/static/sass/_variables.scss @@ -77,6 +77,9 @@ $shadow: rgba(0,0,0,0.2); $shadow-l1: rgba(0,0,0,0.1); $shadow-d1: rgba(0,0,0,0.4); +// misc. +$elem-height-imaginary: 1000000px; + // colors - inherited $baseFontColor: #3c3c3c; $offBlack: #3c3c3c; diff --git a/cms/static/sass/base-style.scss b/cms/static/sass/base-style.scss index dceac4233d..e37ea22aad 100644 --- a/cms/static/sass/base-style.scss +++ b/cms/static/sass/base-style.scss @@ -31,6 +31,7 @@ @import "login"; @import "account"; @import "index"; +@import "checklists"; @import 'jquery-ui-calendar'; @import 'content-types'; diff --git a/cms/templates/checklists.html b/cms/templates/checklists.html new file mode 100644 index 0000000000..21f5c5007d --- /dev/null +++ b/cms/templates/checklists.html @@ -0,0 +1,43 @@ +<%inherit file="base.html" /> +<%block name="title">Course Checklists +<%block name="bodyclass">is-signedin course checklists + +<%block name="jsextra"> + + + +<%block name="content"> +
+
+
+ UX Design +

Alerts & Notifications

+
+
+
+ +
+
+
+
+
+

Alerts

+ persistant, static messages to the user +
+ +

In Studio, alerts are 1) general warnings/notes (e.g. drafts, published content, next steps) about the current view a user is interacting with or 2) notes about the status (e.g. saved confirmations, errors, next system steps) of any previous state that need to communicated to the user when arriving at the current view.

+
+
+
+
+ + +<%block name="view_alerts"> + + \ No newline at end of file diff --git a/cms/templates/ux-checklists.html b/cms/templates/ux-checklists.html new file mode 100644 index 0000000000..ef63561ce2 --- /dev/null +++ b/cms/templates/ux-checklists.html @@ -0,0 +1,246 @@ +<%inherit file="base.html" /> +<%block name="title">Course Checklists +<%block name="bodyclass">is-signedin course uxdesign checklists + +<%block name="content"> +
+
+
+ Tools +

Course Tasks & Checklists

+
+
+
+ +
+
+
+
+

Current Checklists

+ +
+
+

Getting Started with Studio

+ Tasks Completed: 2/5 +
+ +
    +
  • +
    +
    + + +
    +

    Grant your collaborators permission to edit your course so you can work together.

    +
    + + +
  • +
  • +
    +
    + + +
    +

    Establish a course start and end date, course enrollment start and end dates, content release and due dates, and other important dates.

    +
    + + +
  • +
  • +
    +
    + + +
    +

    Grant your collaborators permission to edit your course so you can work together.

    +
    + + +
  • +
  • +
    +
    + + +
    +

    Establish a course start and end date, course enrollment start and end dates, content release and due dates, and other important dates.

    +
    + + +
  • +
  • +
    +
    + + +
    +

    Grant your collaborators permission to edit your course so you can work together.

    +
    + + +
  • +
  • +
    +
    + + +
    +

    Establish a course start and end date, course enrollment start and end dates, content release and due dates, and other important dates.

    +
    + + +
  • +
+ + +
+ +
+ +

Completed Checklists

+ +
+
+

Getting Started with Studio

+ Tasks Completed: 5/5 +
+ +
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+ + +
+ +
+
+
+ + +
+
+ + +<%block name="view_alerts"> + +
+
+ + +
+

Your policy changes have been saved.

+

Please note that validation of your policy key and value pairs is not currently in place yet. If you are having difficulties, please review your policy pairs.

+
+ + + + close alert + +
+
+ + +<%block name="jsextra"> + + \ No newline at end of file diff --git a/cms/templates/widgets/header.html b/cms/templates/widgets/header.html index a063e4b526..7648f8b6f2 100644 --- a/cms/templates/widgets/header.html +++ b/cms/templates/widgets/header.html @@ -58,6 +58,7 @@ diff --git a/cms/urls.py b/cms/urls.py index d43b9bc44c..cba19310fe 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -83,6 +83,7 @@ urlpatterns = ('', # User creation and updating views urlpatterns += ( + url(r'^ux-checklists$', 'contentstore.views.ux_checklists', name='checklists'), url(r'^howitworks$', 'contentstore.views.howitworks', name='howitworks'), url(r'^signup$', 'contentstore.views.signup', name='signup'), From 66a8735a58908b3ce1e72f5fca0e90cdc12327f9 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 13 Mar 2013 10:50:34 -0400 Subject: [PATCH 020/436] studio - Checklists: initial design and front end proofing/firming up - WIP --- cms/static/sass/_base.scss | 3 +- cms/static/sass/_checklists.scss | 273 ++++++++++++++++++++++++++++ cms/static/sass/_variables.scss | 1 + cms/templates/ux-checklists.html | 303 ++++++++++++++++++++++--------- 4 files changed, 497 insertions(+), 83 deletions(-) create mode 100644 cms/static/sass/_checklists.scss diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss index 23ff2b93e8..995a0bbe9f 100644 --- a/cms/static/sass/_base.scss +++ b/cms/static/sass/_base.scss @@ -327,7 +327,8 @@ h1 { } } - .nav-related { + // navigation + .nav-related, .nav-page { .nav-item { margin-bottom: ($baseline/4); diff --git a/cms/static/sass/_checklists.scss b/cms/static/sass/_checklists.scss new file mode 100644 index 0000000000..66601fcf2a --- /dev/null +++ b/cms/static/sass/_checklists.scss @@ -0,0 +1,273 @@ +// Studio - Course Settings +// ==================== +body.course.checklists { + + .content-primary, .content-supplementary { + @include box-sizing(border-box); + float: left; + } + + .content-primary { + width: flex-grid(9, 12); + margin-right: flex-gutter(); + } + + // checklists - general + .course-checklist { + @extend .window; + margin: 0 0 ($baseline*2) 0; + + &:last-child { + margin-bottom: 0; + } + + header { + @include clearfix(); + margin-bottom: 0; + padding: $baseline ($baseline*1.5); + + .checklist-title { + @include transition(color .15s .25s ease-in-out); + width: flex-grid(7, 9); + margin: 0 flex-gutter() 0 0; + float: left; + + &.is-selectable { + cursor: pointer; + + &:hover { + color: $blue; + } + } + } + + .checklist-status { + @include font-size(13); + width: flex-grid(2, 9); + float: right; + margin-top: ($baseline/2); + text-align: right; + color: $gray-l2; + + .status-count { + @include font-size(16); + margin-left: ($baseline/4); + margin-right: ($baseline/4); + color: $gray-d3; + font-weight: 600; + } + + .status-amount { + @include font-size(16); + margin-left: ($baseline/4); + color: $gray-d3; + font-weight: 600; + } + } + } + + // checklist actions + .course-checklist-actions { + @include clearfix(); + @include box-shadow(inset 0 1px 1px $shadow-l1); + @include transition(border .15s ease-in-out .25s); + border-top: 1px solid $gray-l2; + padding: $baseline ($baseline*1.5); + background: $gray-l4; + + .action-primary { + @include green-button(); + float: left; + + .icon-add { + @include font-size(12); + display: inline-block; + vertical-align: middle; + margin-right: ($baseline/4); + } + } + + .action-secondary { + @include font-size(14); + @include grey-button(); + font-weight: 400; + float: right; + + .icon-delete { + @include font-size(12); + display: inline-block; + vertical-align: middle; + margin-right: ($baseline/4); + } + } + } + + // state - collapsed + &.is-collapsed { + + .list-tasks { + height: 0; + } + } + + // state - completed + &.is-completed { + + header { + + .checklist-title { + color: $gray-l1; + } + + .checklist-status { + + .status-count, .status-amount, .icon-confirm { + color: $green; + } + } + + .checklist-status .icon-confirm { + @include font-size(12); + display: inline-block; + vertical-align: middle; + margin-right: ($baseline/4); + } + } + } + + // state - not available + .is-not-available { + + } + } + + // list of tasks + .list-tasks { + height: auto; + overflow: hidden; + + .task { + @include transition(background .15s ease-in-out .25s, border .15s ease-in-out .25s); + @include clearfix(); + position: relative; + border-top: 1px solid $white; + border-bottom: 1px solid $gray-l5; + padding: $baseline ($baseline*1.5); + background: $white; + opacity: 1.0; + + + &:last-child { + margin-bottom: 0; + border-bottom: none; + } + + label { + float: left; + width: flex-grid(7,9); + font-weight: 500; + + .task-input { + display: inline-block; + vertical-align: middle; + margin-right: flex-gutter(); + } + + .task-details { + display: inline-block; + vertical-align: middle; + width: flex-grid(6,7); + + .task-name { + @include transition(color .15s .25s ease-in-out); + vertical-align: baseline; + cursor: pointer; + margin-bottom: 0; + } + + .task-description { + @include transition(color .15s .25s ease-in-out); + @include font-size(14); + color: $gray-l2; + } + + .task-support { + @include transition(opacity .15s .25s ease-in-out); + @include font-size(12); + opacity: 0; + pointer-events: none; + } + } + } + + .task-actions { + @include transition(opacity .15s .25s ease-in-out); + @include clearfix(); + display: inline-block; + vertical-align: middle; + float: left; + width: flex-grid(2,9); + margin: ($baseline/2) 0 0 flex-gutter(); + opacity: 0; + pointer-events: none; + text-align: right; + + .action-primary { + @include blue-button; + @include transition(all .15s); + @include font-size(12); + font-weight: 600; + text-align: center; + } + + .action-secondary { + @include font-size(13); + margin-top: ($baseline/2); + } + } + + // state - hover + &:hover { + background: $blue-l5; + border-bottom-color: $blue-l4; + border-top-color: $blue-l4; + opacity: 1.0; + + .task-details { + .task-support { + opacity: 1.0; + pointer-events: auto; + } + } + + .task-actions { + opacity: 1.0; + pointer-events: auto; + } + } + + + // state - completed + &.is-completed { + background: $gray-l6; + border-top-color: $gray-l5; + border-bottom-color: $gray-l5; + + .task-details { + opacity: 0.50; + } + + &:hover { + + .task-details { + opacity: 1.0; + } + } + } + } + } + + .content-supplementary { + width: flex-grid(3, 12); + } +} \ No newline at end of file diff --git a/cms/static/sass/_variables.scss b/cms/static/sass/_variables.scss index e94ebcb7bc..9e383af99d 100644 --- a/cms/static/sass/_variables.scss +++ b/cms/static/sass/_variables.scss @@ -24,6 +24,7 @@ $gray-l2: tint($gray,40%); $gray-l3: tint($gray,60%); $gray-l4: tint($gray,80%); $gray-l5: tint($gray,90%); +$gray-l6: tint($gray,95%); $gray-d1: shade($gray,20%); $gray-d2: shade($gray,40%); $gray-d3: shade($gray,60%); diff --git a/cms/templates/ux-checklists.html b/cms/templates/ux-checklists.html index ef63561ce2..d0a5ab8fa4 100644 --- a/cms/templates/ux-checklists.html +++ b/cms/templates/ux-checklists.html @@ -26,13 +26,14 @@
  • -
    -
    - - +
    +
    • @@ -40,29 +41,16 @@
  • +
  • -
    -
    - - -
    -

    Establish a course start and end date, course enrollment start and end dates, content release and due dates, and other important dates.

    -
    +
  • -
  • -
    -
    - - +
    +

    Add Course Team Members

    +

    Grant your collaborators permission to edit your course so you can work together.

    -

    Grant your collaborators permission to edit your course so you can work together.

    -
    +
    • @@ -70,48 +58,128 @@
  • -
  • -
    -
    - - -
    -

    Establish a course start and end date, course enrollment start and end dates, content release and due dates, and other important dates.

    -
    - -
  • -
  • -
    -
    - - -
    -

    Grant your collaborators permission to edit your course so you can work together.

    -
    - - -
  • -
    -
    - - +
    + +
  • + +
  • + + + +
  • +
+ + + + +
+
+

Getting Started with Studio

+ Tasks Completed: 2/5 +
+ +
    +
  • + + + +
  • + +
  • + + + +
  • + +
  • + + + +
  • + +
  • + + +
  • @@ -138,29 +206,68 @@ Tasks Completed: 5/5 -
      -
    • - -
    • +
      • + + +
      • -
      • +
      • + + + +
      • + +
      • + + +
      - +
    • + + Add a Task to This Checklist +
    • +
    • + Delete This Checklist +
    • +

@@ -233,12 +340,44 @@ From 16f6744aefcb53f8b1b59cdc82251288020540c6 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 13 Mar 2013 19:13:26 -0400 Subject: [PATCH 022/436] studio - checklists: revised task completion styles, added in checklist visual progress UI and demo/PoC JS and cleaned up some content/status states --- cms/static/sass/_checklists.scss | 49 ++++++++++++++++++++++++++++++-- cms/templates/ux-checklists.html | 25 ++++++++++++++-- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/cms/static/sass/_checklists.scss b/cms/static/sass/_checklists.scss index ca5424967e..7a55e07101 100644 --- a/cms/static/sass/_checklists.scss +++ b/cms/static/sass/_checklists.scss @@ -21,6 +21,32 @@ body.course.checklists { margin-bottom: 0; } + // visual status + .viz-checklist-status { + @include text-hide(); + @include size(100%,($baseline/4)); + position: relative; + display: block; + margin: 0; + background: $gray-l4; + + .viz-checklist-status-value { + @include transition(width 2s ease-in-out .25s); + position: absolute; + top: 0; + left: 0; + width: 50%; + height: ($baseline/4); + background: $green; + + .int { + @include text-sr(); + } + } + } + // 0% of checklist completed + + // header/title header { @include clearfix(); @include box-shadow(inset 0 -1px 1px $shadow-l1); @@ -127,6 +153,13 @@ body.course.checklists { // state - completed &.is-completed { + .viz-checklist-status { + + .viz-checklist-status-value { + width: 100%; + } + } + header { .checklist-title, .icon-confirm { @@ -273,10 +306,20 @@ body.course.checklists { color: $gray-l2; } + .task-actions { + + .action-primary { + @include grey-button; + @include font-size(12); + font-weight: 600; + text-align: center; + } + } + &:hover { - background: $blue-l5; - border-bottom-color: $blue-l4; - border-top-color: $blue-l4; + background: $gray-l5; + border-bottom-color: $gray-l4; + border-top-color: $gray-l4; .task-details { opacity:1.0; diff --git a/cms/templates/ux-checklists.html b/cms/templates/ux-checklists.html index 3f7a947111..833bbe000f 100644 --- a/cms/templates/ux-checklists.html +++ b/cms/templates/ux-checklists.html @@ -14,11 +14,17 @@
+
+

Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.Test Checklist Progress Visualization

+
+

Current Checklists

+ 0% of checklist completed +

Getting Started with Studio

Tasks Completed: 0/4 @@ -100,6 +106,8 @@
+ 0% of checklist completed +

Draft a Rough Course Outline

Tasks Completed: 0/7 @@ -238,6 +246,8 @@
+ 0% of checklist completed +

Explore edX's Support Tools

Tasks Completed: 0/4 @@ -319,9 +329,10 @@
+ 0% of checklist completed +

Draft your Course Introduction

- Tasks Completed: 0/4
    @@ -330,7 +341,7 @@
    -

    Drafting a Course Description/h4> +

    Drafting a Course Description

    Courses on edX each have their own introduction page, including a course video, description, and more. Draft the introduction students will read before deciding to enroll in your course.

    @@ -342,7 +353,7 @@
-
  • +
  • + + +
    <%block name="view_alerts"> - + +
    +
    + + +
    +

    Your policy changes have been saved.

    +

    Please note that validation of your policy key and value pairs is not currently in place yet. If you are having difficulties, please review your policy pairs.

    +
    + + + + close alert + +
    +
    + + +<%block name="jsextra"> + \ No newline at end of file diff --git a/cms/templates/widgets/header.html b/cms/templates/widgets/header.html index 7648f8b6f2..45ff66df85 100644 --- a/cms/templates/widgets/header.html +++ b/cms/templates/widgets/header.html @@ -58,7 +58,7 @@ diff --git a/cms/urls.py b/cms/urls.py index cba19310fe..18da7c7b71 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -83,7 +83,8 @@ urlpatterns = ('', # User creation and updating views urlpatterns += ( - url(r'^ux-checklists$', 'contentstore.views.ux_checklists', name='checklists'), + url(r'^(?P[^/]+)/(?P[^/]+)/checklists/(?P[^/]+)$', 'contentstore.views.get_checklists', name='checklists'), +# url(r'^ux-checklists$', 'contentstore.views.ux_checklists', name='checklists'), url(r'^howitworks$', 'contentstore.views.howitworks', name='howitworks'), url(r'^signup$', 'contentstore.views.signup', name='signup'), diff --git a/common/lib/xmodule/xmodule/templates/course/empty.yaml b/common/lib/xmodule/xmodule/templates/course/empty.yaml index cb2f3bcec6..61cc204254 100644 --- a/common/lib/xmodule/xmodule/templates/course/empty.yaml +++ b/common/lib/xmodule/xmodule/templates/course/empty.yaml @@ -2,5 +2,31 @@ metadata: display_name: Empty start: 2020-10-10T10:00 + checklists: [ + {"short_description" : "Getting Started With Studio", + "items" : [{"short_description": "Add Course Team Members", + "long_description": "Grant your collaborators permission to edit your course so you can work together.", + "is_checked": false, + "action_url": "/manage_users/", + "action_text": "Edit Course Team"}, + {"short_description": "Drink Whiskey", + "long_description": "Team-building activity.", + "is_checked": false, + "action_url": "/drink_alcohol", + "action_text": "Drink"}], + "completed" : false}, + {"short_description" : "Launching Your Course", + "items" : [{"short_description": "Add Content", + "long_description": "Create videos and problems.", + "is_checked": false, + "action_url": "", + "action_text": ""}, + {"short_description": "View Course as a Student", + "long_description": "Create a student account and view your course.", + "is_checked": false, + "action_url": "", + "action_text": ""}], + "completed" : false} + ] data: { 'textbooks' : [ ], 'wiki_slug' : null } children: [] From 065e85044923c2b14c46246aa92d83e4696396cc Mon Sep 17 00:00:00 2001 From: cahrens Date: Thu, 14 Mar 2013 09:32:54 -0400 Subject: [PATCH 026/436] Merge with Brian's changes. --- cms/templates/checklists.html | 28 +- cms/templates/ux-checklists.html | 618 ------------------------------- 2 files changed, 24 insertions(+), 622 deletions(-) delete mode 100644 cms/templates/ux-checklists.html diff --git a/cms/templates/checklists.html b/cms/templates/checklists.html index 109ac36df5..455dbe6b04 100644 --- a/cms/templates/checklists.html +++ b/cms/templates/checklists.html @@ -14,6 +14,10 @@
    +
    +

    Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.Test Checklist Progress Visualization

    +
    +

    Current Checklists

    @@ -21,6 +25,7 @@ % for checklist in checklists:
    + 0% of checklist completed

    ${checklist['short_description']}

    Tasks Completed: 0/4 @@ -53,6 +58,8 @@ % endfor
    + 0% of checklist completed +

    Getting Started with Studio

    Tasks Completed: 0/4 @@ -134,6 +141,8 @@
    + 0% of checklist completed +

    Draft a Rough Course Outline

    Tasks Completed: 0/7 @@ -272,6 +281,8 @@
    + 0% of checklist completed +

    Explore edX's Support Tools

    Tasks Completed: 0/4 @@ -353,9 +364,10 @@
    + 0% of checklist completed +

    Draft your Course Introduction

    - Tasks Completed: 0/4
      @@ -364,8 +376,8 @@
      -

      Drafting a Course Description/h4> -

      Courses on edX each have their own introduction page, including a course video, description, and more. Draft the introduction students will read before deciding to enroll in your course.

      +

      Drafting a Course Description

      +

      Courses on edX each have their own introduction page, including a course video, description, and more. Draft the introduction students will read before deciding to enroll in your course.

      @@ -376,7 +388,7 @@
    -
  • +
  • <%block name="view_alerts">
    -
    - +
    + -
    -

    Your policy changes have been saved.

    -

    Please note that validation of your policy key and value pairs is not currently in place yet. If you are having difficulties, please review your policy pairs.

    -
    - - - - close alert - +
    +

    Your policy changes have been saved.

    +

    Please note that validation of your policy key and value pairs is not currently in place yet. If you are having difficulties, please review your policy pairs.

    + + + + close alert + +
    @@ -598,56 +113,55 @@ // checklists - prototype/basic js $(document).ready(function() { - $('.course-checklist .checklist-title').each(function(e){ - $(this).addClass('is-selectable').attr('title','Collapse/Expand this Checklist').bind('click', toggleChecklist); - }); + $('.course-checklist .checklist-title').each(function(e){ + $(this).addClass('is-selectable').bind('click', toggleChecklist); + }); - $('.course-checklist .task label').each(function(e){ - $(this).bind('click', toggleTask); - }); + $('.course-checklist .task label').each(function(e){ + $(this).bind('click', toggleTask); + }); - // demo/proof of concept for visual progress - $('.demo-checklistviz').click(function(e){ - (e).preventDefault(); - $('#course-checklist1 .viz-checklist-status .viz-checklist-status-value').css('width','25%'); - }); + // demo/proof of concept for visual progress + $('.demo-checklistviz').click(function(e){ + (e).preventDefault(); + $('#course-checklist1 .viz-checklist-status .viz-checklist-status-value').css('width','25%'); + }); - function toggleChecklist(e) { - (e).preventDefault(); - $(this).closest('.course-checklist').toggleClass('is-collapsed'); - } + function toggleChecklist(e) { + (e).preventDefault(); + $(this).closest('.course-checklist').toggleClass('is-collapsed'); + } - function toggleTask(e) { - (e).preventDefault(); + function toggleTask(e) { + (e).preventDefault(); - var $taskInput = $(this).find('.task-input'); + var $taskInput = $(this).find('.task-input'); - if ($taskInput.attr('checked')) { - $taskInput.removeAttr('checked'); - console.log('removing check'); - } - else { - $taskInput.attr('checked', 'checked'); - console.log('adding check'); - } + if ($taskInput.attr('checked')) { + $taskInput.removeAttr('checked'); + console.log('removing check'); + } + else { + $taskInput.attr('checked', 'checked'); + console.log('adding check'); + } - $(this).closest('.task').toggleClass('is-completed'); - } + $(this).closest('.task').toggleClass('is-completed'); + } - // in-progress update checklist progress (based on checkbox check/uncheck events) - function updateChecklistProgress() { - var $statusCount = $(this).closest('.course-checklist').find('.status-count'); - var $statusAmount = $(this).closest('.course-checklist').find('.status-amount'); + // in-progress update checklist progress (based on checkbox check/uncheck events) + function updateChecklistProgress() { + var $statusCount = $(this).closest('.course-checklist').find('.status-count'); + var $statusAmount = $(this).closest('.course-checklist').find('.status-amount'); - if ($(this).attr('checked')) { - console.log('adding'); - } - - else { - console.log('subtracting'); - } - } + if ($(this).attr('checked')) { + console.log('adding'); + } + else { + console.log('subtracting'); + } + } }); \ No newline at end of file diff --git a/common/lib/xmodule/xmodule/templates/course/empty.yaml b/common/lib/xmodule/xmodule/templates/course/empty.yaml index 61cc204254..153d7859ee 100644 --- a/common/lib/xmodule/xmodule/templates/course/empty.yaml +++ b/common/lib/xmodule/xmodule/templates/course/empty.yaml @@ -3,30 +3,104 @@ metadata: display_name: Empty start: 2020-10-10T10:00 checklists: [ - {"short_description" : "Getting Started With Studio", - "items" : [{"short_description": "Add Course Team Members", - "long_description": "Grant your collaborators permission to edit your course so you can work together.", - "is_checked": false, - "action_url": "/manage_users/", - "action_text": "Edit Course Team"}, - {"short_description": "Drink Whiskey", - "long_description": "Team-building activity.", - "is_checked": false, - "action_url": "/drink_alcohol", - "action_text": "Drink"}], - "completed" : false}, - {"short_description" : "Launching Your Course", - "items" : [{"short_description": "Add Content", - "long_description": "Create videos and problems.", - "is_checked": false, - "action_url": "", - "action_text": ""}, - {"short_description": "View Course as a Student", - "long_description": "Create a student account and view your course.", - "is_checked": false, - "action_url": "", - "action_text": ""}], - "completed" : false} + {"short_description" : "Getting Started With Studio", + "items" : [{"short_description": "Add Course Team Members", + "long_description": "Grant your collaborators permission to edit your course so you can work together.", + "is_checked": false, + "action_url": "/manage_users/", + "action_text": "Edit Course Team"}, + {"short_description": "Set Important Dates for Your Course", + "long_description": "Establish your course's student enrollment and launch dates on the Schedule and Details Settings page.", + "is_checked": false, + "action_url": "/settings-details/", + "action_text": "Edit Course Details & Schedule"}, + {"short_description": "Draft Your Course's Grading Policy", + "long_description": "Regardless of whether you have all your course assignments written, you can immediately get started setting up assignment types and a grade computation scheme.", + "is_checked": false, + "action_url": "/settings-grading/", + "action_text": "Edit Grading Settings"}, + {"short_description": "Explore the other Studio Checklists", + "long_description": "They'll help you learn the other course authoring tools available to you, and will also help you find help when you need it.", + "is_checked": false, + "action_url": "", + "action_text": ""}], + "completed" : false}, + {"short_description" : "Draft a Rough Course Outline", + "items" : [{"short_description": "Create your first Section and Subsection", + "long_description": "Walk through the mechanics of building your course's first section and subsection through your course outline to start.", + "is_checked": false, + "action_url": "/course/", + "action_text": "Edit in Course Outline"}, + {"short_description": "Set your first Section's Release Date", + "long_description": "Sections are released sequentially to students, and you have complete control over they are released to students.", + "is_checked": false, + "action_url": "/course/", + "action_text": "Edit in Course Outline"}, + {"short_description": "Designate a Subsection as Graded", + "long_description": "Assignment types are defined in your grading settings but can be quickly associated with sections using your course outline.", + "is_checked": false, + "action_url": "/course/", + "action_text": "Edit in Course Outline"}, + {"short_description": "Reordering Course Content", + "long_description": "From the Course Outline, you can easily reorder your course content based on the progression you'd like students to walk through.", + "is_checked": false, + "action_url": "/course/", + "action_text": "Edit in Course Outline"}, + {"short_description": "Renaming Course Sections", + "long_description": "Learn how to rename Sections by clicking on its name from the Course Outline; this should open the editing mode.", + "is_checked": false, + "action_url": "/course/", + "action_text": "Edit in Course Outline"}, + {"short_description": "Deleting Course Content", + "long_description": "Try out deleting on a section, subsection, or unit you don't need anymore. Be careful though, anything inside the course content you delete is also removed.", + "is_checked": false, + "action_url": "/course/", + "action_text": "Edit in Course Outline"}, + {"short_description": "Add an Instructor-Only Section to Your Outline", + "long_description": "Some course authors find creating a section for unsorted, in-progress work useful. To do this, create a section and set the release date to the distant future.", + "is_checked": false, + "action_url": "/course/", + "action_text": "Edit in Course Outline"}], + "completed" : false}, + {"short_description" : "Explore edX's Support Tools", + "items" : [{"short_description": "Explore the Studio Help Forum", + "long_description": "Access the Studio Help forum from the menu that appears when you click your user name in the top right corner of Studio.", + "is_checked": false, + "action_url": "http://help.edge.edx.org/", + "action_text": "Visit Studio Help"}, + {"short_description": "Enroll in edX101", + "long_description": "Register for edX101, edX's primer for course creation.", + "is_checked": false, + "action_url": "/", + "action_text": "Register for edX 101"}, + {"short_description": "Download the Studio Documentation", + "long_description": "View the searchable Studio documentation to find answers to your questions or information about how to do specific tasks. Once you download the PDF, you can view it offline.", + "is_checked": false, + "action_url": "/", + "action_text": "Download Documentation"}], + "completed" : false}, + {"short_description" : "Draft your Course Introduction", + "items" : [{"short_description": "Drafting a Course Description", + "long_description": "Courses on edX each have their own introduction page, including a course video, description, and more. Draft the introduction students will read before deciding to enroll in your course.", + "is_checked": false, + "action_url": "/settings-details/", + "action_text": "Edit Course Details & Schedule"}, + {"short_description": "Adding Staff Bios", + "long_description": "Showing prospective students who will be their instructor is helpful, so we recommend including staff bios in the Course introduction page.", + "is_checked": false, + "action_url": "/settings-details/", + "action_text": "Edit Course Details & Schedule"}, + {"short_description": "Add Course FAQs", + "long_description": "Students often have questions about courses, and including a short list of frequently asked questions up front often reduces the number of students who are confused.", + "is_checked": false, + "action_url": "/settings-details/", + "action_text": "Edit Course Details & Schedule"}, + {"short_description": "Add Course Prerequisites", + "long_description": "Before a student jumps into a course without the necessary preparation, we'd like them to understand the prerequisites that will make them more likely to succeed.", + "is_checked": false, + "action_url": "/settings-details/", + "action_text": "Edit Course Details & Schedule"}], + "completed" : false} ] data: { 'textbooks' : [ ], 'wiki_slug' : null } children: [] From 4c8a45f85ecfb6422bd10de3b79f3a5ef51c70f9 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 14 Mar 2013 13:29:26 -0400 Subject: [PATCH 029/436] Code to add in an open ended tab automatically --- cms/djangoapps/contentstore/utils.py | 12 +++++++- cms/djangoapps/contentstore/views.py | 28 +++++++++++++++++-- .../models/settings/course_metadata.py | 9 ++++-- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index cba30131b5..4113361445 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -2,9 +2,10 @@ from django.conf import settings from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import ItemNotFoundError +import copy DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info'] - +OPEN_ENDED_PANEL = {"name" : "Open Ended Panel", "type" : "open_ended"} def get_modulestore(location): """ @@ -158,3 +159,12 @@ def update_item(location, value): get_modulestore(location).delete_item(location) else: get_modulestore(location).update_item(location, value) + +def add_open_ended_panel_tab(course): + course_tabs = copy.copy(course.tabs) + changed = False + if OPEN_ENDED_PANEL not in course_tabs: + course_tabs.append(OPEN_ENDED_PANEL) + changed = True + return changed, course_tabs + diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 6566350f8d..b066f476a3 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -47,6 +47,7 @@ from auth.authz import is_user_in_course_group_role, get_users_in_course_group_b from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME, create_all_course_groups from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state, get_date_display, UnitState, get_course_for_item +from .utils import add_open_ended_panel_tab from xmodule.modulestore.xml_importer import import_from_xml from contentstore.course_info_model import get_course_updates,\ @@ -68,7 +69,8 @@ log = logging.getLogger(__name__) COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video'] -ADVANCED_COMPONENT_TYPES = ['annotatable','combinedopenended', 'peergrading'] +OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"] +ADVANCED_COMPONENT_TYPES = ['annotatable'] + OPEN_ENDED_COMPONENT_TYPES ADVANCED_COMPONENT_CATEGORY = 'advanced' ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules' @@ -295,6 +297,9 @@ def edit_unit(request, location): # in ADVANCED_COMPONENT_TYPES that should be enabled for the course. course_metadata = CourseMetadata.fetch(course.location) course_advanced_keys = course_metadata.get(ADVANCED_COMPONENT_POLICY_KEY, []) + log.debug(course.tabs) + log.debug(type(course.tabs)) + log.debug("LOOK HERE NOW!!!!!") # Set component types according to course policy file component_types = list(COMPONENT_TYPES) @@ -1329,7 +1334,26 @@ def course_advanced_updates(request, org, course, name): return HttpResponse(json.dumps(CourseMetadata.delete_key(location, json.loads(request.body))), mimetype="application/json") elif real_method == 'POST' or real_method == 'PUT': # NOTE: request.POST is messed up because expect_json cloned_request.POST.copy() is creating a defective entry w/ the whole payload as the key - return HttpResponse(json.dumps(CourseMetadata.update_from_json(location, json.loads(request.body))), mimetype="application/json") + request_body = json.loads(request.body) + filter_tabs = True + if ADVANCED_COMPONENT_POLICY_KEY in request_body: + log.debug("Advanced component in.") + for oe_type in OPEN_ENDED_COMPONENT_TYPES: + log.debug(request_body[ADVANCED_COMPONENT_POLICY_KEY]) + if oe_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]: + log.debug("OE type in.") + course_module = modulestore().get_item(location) + changed, new_tabs = add_open_ended_panel_tab(course_module) + log.debug(new_tabs) + if changed: + request_body.update({'tabs' : new_tabs}) + filter_tabs = False + break + log.debug(request_body) + log.debug(filter_tabs) + log.debug("LOOK HERE FOR TAB SAVING!!!!") + response_json = json.dumps(CourseMetadata.update_from_json(location, request_body, filter_tabs=filter_tabs)) + return HttpResponse(response_json, mimetype="application/json") @login_required diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py index 24245a39d5..af0923213b 100644 --- a/cms/djangoapps/models/settings/course_metadata.py +++ b/cms/djangoapps/models/settings/course_metadata.py @@ -1,6 +1,7 @@ from xmodule.modulestore import Location from contentstore.utils import get_modulestore from xmodule.x_module import XModuleDescriptor +import copy class CourseMetadata(object): @@ -30,7 +31,7 @@ class CourseMetadata(object): return course @classmethod - def update_from_json(cls, course_location, jsondict): + def update_from_json(cls, course_location, jsondict, filter_tabs=True): """ Decode the json into CourseMetadata and save any changed attrs to the db. @@ -40,9 +41,13 @@ class CourseMetadata(object): dirty = False + filtered_list = copy.copy(cls.FILTERED_LIST) + if not filter_tabs: + filtered_list.remove("tabs") + for k, v in jsondict.iteritems(): # should it be an error if one of the filtered list items is in the payload? - if k not in cls.FILTERED_LIST and (k not in descriptor.metadata or descriptor.metadata[k] != v): + if k not in filtered_list and (k not in descriptor.metadata or descriptor.metadata[k] != v): dirty = True descriptor.metadata[k] = v From a717dffd4886a185ae2d4414f060e295871dbd82 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 14 Mar 2013 13:31:30 -0400 Subject: [PATCH 030/436] Remove debug statements --- cms/djangoapps/contentstore/views.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index b066f476a3..591ec7d7cf 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -297,10 +297,7 @@ def edit_unit(request, location): # in ADVANCED_COMPONENT_TYPES that should be enabled for the course. course_metadata = CourseMetadata.fetch(course.location) course_advanced_keys = course_metadata.get(ADVANCED_COMPONENT_POLICY_KEY, []) - log.debug(course.tabs) - log.debug(type(course.tabs)) - log.debug("LOOK HERE NOW!!!!!") - + # Set component types according to course policy file component_types = list(COMPONENT_TYPES) if isinstance(course_advanced_keys, list): @@ -1337,21 +1334,14 @@ def course_advanced_updates(request, org, course, name): request_body = json.loads(request.body) filter_tabs = True if ADVANCED_COMPONENT_POLICY_KEY in request_body: - log.debug("Advanced component in.") for oe_type in OPEN_ENDED_COMPONENT_TYPES: - log.debug(request_body[ADVANCED_COMPONENT_POLICY_KEY]) if oe_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]: - log.debug("OE type in.") course_module = modulestore().get_item(location) changed, new_tabs = add_open_ended_panel_tab(course_module) - log.debug(new_tabs) if changed: request_body.update({'tabs' : new_tabs}) filter_tabs = False break - log.debug(request_body) - log.debug(filter_tabs) - log.debug("LOOK HERE FOR TAB SAVING!!!!") response_json = json.dumps(CourseMetadata.update_from_json(location, request_body, filter_tabs=filter_tabs)) return HttpResponse(response_json, mimetype="application/json") From 017fd06bfc1ff28635bb54f427c5256ac64d89ba Mon Sep 17 00:00:00 2001 From: cahrens Date: Thu, 14 Mar 2013 13:37:22 -0400 Subject: [PATCH 031/436] Pre-merge. --- cms/static/js/models/checklists.js | 1 + cms/static/js/views/checklists_view.js | 57 ++++++++++--- cms/templates/checklists.html | 113 ++++++------------------- 3 files changed, 70 insertions(+), 101 deletions(-) create mode 100644 cms/static/js/models/checklists.js diff --git a/cms/static/js/models/checklists.js b/cms/static/js/models/checklists.js new file mode 100644 index 0000000000..368db944ed --- /dev/null +++ b/cms/static/js/models/checklists.js @@ -0,0 +1 @@ +if (!CMS.Models['Checklists']) CMS.Models.Checklists = new Object(); \ No newline at end of file diff --git a/cms/static/js/views/checklists_view.js b/cms/static/js/views/checklists_view.js index b33a5d66f4..0905b392e4 100644 --- a/cms/static/js/views/checklists_view.js +++ b/cms/static/js/views/checklists_view.js @@ -1,18 +1,49 @@ +if (!CMS.Views['Checklists']) CMS.Views.Checklists = {}; + CMS.Views.Checklists = Backbone.View.extend({ - // takes CMS.Models.CourseInfo as model - tagName: 'div', + // takes CMS.Models.Checklists as model - render: function() { - // instantiate the ClassInfoUpdateView and delegate the proper dom to it - new CMS.Views.ClassInfoUpdateView({ - el: $('body.updates'), - collection: this.model.get('updates') - }); + events : { + 'click .course-checklist .checklist-title' : "toggleChecklist", + 'click .course-checklist .task label' : "toggleTask", + 'click .demo-checklistviz' : "demoUpdateProgress" + }, - new CMS.Views.ClassInfoHandoutsView({ - el: this.$('#course-handouts-view'), - model: this.model.get('handouts') - }); - return this; + initialize : function() { + // adding class and title needs to happen in HTML +// $('.course-checklist .checklist-title').each(function(e){ +// $(this).addClass('is-selectable').attr('title','Collapse/Expand this Checklist').bind('click', this.toggleChecklist); +// }); + }, + + toggleChecklist : function(e) { + (e).preventDefault(); + $(e.target).closest('.course-checklist').toggleClass('is-collapsed'); + }, + + toggleTask : function (e) { + (e).preventDefault(); + $(e.target).closest('.task').toggleClass('is-completed'); + }, + + // TODO: remove + demoUpdateProgress : function(e) { + (e).preventDefault(); + $('#course-checklist0 .viz-checklist-status .viz-checklist-status-value').css('width','25%'); + }, + + // TODO: not used. In-progress update checklist progress (based on checkbox check/uncheck events) + updateChecklistProgress : function(e) { + var $statusCount = this.$el.closest('.course-checklist').find('.status-count'); + var $statusAmount = this.$el.closest('.course-checklist').find('.status-amount'); + + if (this.$el.attr('checked')) { + console.log('adding'); + } + + else { + console.log('subtracting'); + } } + }); \ No newline at end of file diff --git a/cms/templates/checklists.html b/cms/templates/checklists.html index 455dbe6b04..a9d7255d55 100644 --- a/cms/templates/checklists.html +++ b/cms/templates/checklists.html @@ -2,6 +2,27 @@ <%block name="title">Course Checklists <%block name="bodyclass">is-signedin course uxdesign checklists +<%namespace name='static' file='static_content.html'/> +<%block name="jsextra"> + + + + + + + <%block name="content"> @@ -564,7 +578,7 @@ @@ -574,80 +588,3 @@
    -<%block name="view_alerts"> - -
    -
    - - -
    -

    Your policy changes have been saved.

    -

    Please note that validation of your policy key and value pairs is not currently in place yet. If you are having difficulties, please review your policy pairs.

    -
    - - - - close alert - -
    -
    - - -<%block name="jsextra"> - - \ No newline at end of file From 10eb7e45ea58a776113087515c1a00748f954320 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 14 Mar 2013 13:42:41 -0400 Subject: [PATCH 032/436] Add in some docs --- cms/djangoapps/contentstore/utils.py | 8 ++++++++ cms/djangoapps/contentstore/views.py | 14 +++++++++++--- cms/djangoapps/models/settings/course_metadata.py | 2 ++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 4113361445..7e034d8da8 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -161,9 +161,17 @@ def update_item(location, value): get_modulestore(location).update_item(location, value) def add_open_ended_panel_tab(course): + """ + Used to add the open ended panel tab to a course if it does not exist. + @param course: A course object from the modulestore. + @return: Boolean indicating whether or not a tab was added and a list of tabs for the course. + """ + #Copy course tabs course_tabs = copy.copy(course.tabs) changed = False + #Check to see if open ended panel is defined in the course if OPEN_ENDED_PANEL not in course_tabs: + #Add panel to the tabs if it is not defined course_tabs.append(OPEN_ENDED_PANEL) changed = True return changed, course_tabs diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 591ec7d7cf..b7fcc9988e 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -297,7 +297,7 @@ def edit_unit(request, location): # in ADVANCED_COMPONENT_TYPES that should be enabled for the course. course_metadata = CourseMetadata.fetch(course.location) course_advanced_keys = course_metadata.get(ADVANCED_COMPONENT_POLICY_KEY, []) - + # Set component types according to course policy file component_types = list(COMPONENT_TYPES) if isinstance(course_advanced_keys, list): @@ -310,7 +310,6 @@ def edit_unit(request, location): templates = modulestore().get_items(Location('i4x', 'edx', 'templates')) for template in templates: category = template.location.category - if category in course_advanced_keys: category = ADVANCED_COMPONENT_CATEGORY @@ -1332,15 +1331,24 @@ def course_advanced_updates(request, org, course, name): elif real_method == 'POST' or real_method == 'PUT': # NOTE: request.POST is messed up because expect_json cloned_request.POST.copy() is creating a defective entry w/ the whole payload as the key request_body = json.loads(request.body) + #Whether or not to filter the tabs key out of the settings metadata filter_tabs = True + #Check to see if the user instantiated any advanced components. This is a hack to add the open ended panel tab + #to a course automatically if the user has indicated that they want to edit the combinedopenended or peergrading + #module. if ADVANCED_COMPONENT_POLICY_KEY in request_body: + #Check to see if the user instantiated any open ended components for oe_type in OPEN_ENDED_COMPONENT_TYPES: if oe_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]: + #Get the course so that we can scrape current tabs course_module = modulestore().get_item(location) + #Add an open ended tab to the course if needed changed, new_tabs = add_open_ended_panel_tab(course_module) + #If a tab has been added to the course, then send the metadata along to CourseMetadata.update_from_json if changed: request_body.update({'tabs' : new_tabs}) - filter_tabs = False + #Indicate that tabs should not be filtered out of the metadata + filter_tabs = False break response_json = json.dumps(CourseMetadata.update_from_json(location, request_body, filter_tabs=filter_tabs)) return HttpResponse(response_json, mimetype="application/json") diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py index af0923213b..2747cc0751 100644 --- a/cms/djangoapps/models/settings/course_metadata.py +++ b/cms/djangoapps/models/settings/course_metadata.py @@ -41,7 +41,9 @@ class CourseMetadata(object): dirty = False + #Copy the filtered list to avoid permanently changing the class attribute filtered_list = copy.copy(cls.FILTERED_LIST) + #Don't filter on the tab attribute if filter_tabs is False if not filter_tabs: filtered_list.remove("tabs") From ac25b02ce6842babfc1ed1107f49778e8d876f6b Mon Sep 17 00:00:00 2001 From: cahrens Date: Thu, 14 Mar 2013 13:48:17 -0400 Subject: [PATCH 033/436] Minor updates. --- cms/djangoapps/contentstore/views.py | 5 ++++- cms/static/js/views/checklists_view.js | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 62c8f8da97..89360c1d42 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -1322,7 +1322,10 @@ def get_checklists(request, org, course, name): return render_to_response('checklists.html', - {'checklists' : course_module.metadata[key]}) + { + 'context_course': course_module, + 'checklists' : course_module.metadata[key] + }) @login_required diff --git a/cms/static/js/views/checklists_view.js b/cms/static/js/views/checklists_view.js index 0905b392e4..1ed79c8fac 100644 --- a/cms/static/js/views/checklists_view.js +++ b/cms/static/js/views/checklists_view.js @@ -22,7 +22,6 @@ CMS.Views.Checklists = Backbone.View.extend({ }, toggleTask : function (e) { - (e).preventDefault(); $(e.target).closest('.task').toggleClass('is-completed'); }, From 13c38fa173cd988b44bc5a2a32f9abda3102a1a4 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Thu, 14 Mar 2013 16:17:47 -0400 Subject: [PATCH 034/436] studio - checklists: revised input/label HTML and adjusted styling for each --- cms/static/sass/_checklists.scss | 87 ++++++++++++++------------------ cms/templates/checklists.html | 26 +++++----- 2 files changed, 51 insertions(+), 62 deletions(-) diff --git a/cms/static/sass/_checklists.scss b/cms/static/sass/_checklists.scss index 7a5b5213ea..b49c75f9e6 100644 --- a/cms/static/sass/_checklists.scss +++ b/cms/static/sass/_checklists.scss @@ -56,7 +56,7 @@ body.course.checklists { .checklist-title { @include transition(color .15s .25s ease-in-out); - width: flex-grid(7, 9); + width: flex-grid(6, 9); margin: 0 flex-gutter() 0 0; float: left; @@ -64,14 +64,6 @@ body.course.checklists { @include font-size(14); display: inline-block; vertical-align: middle; - margin-right: $baseline; - color: $gray-l4; - } - - .icon-confirm { - @include font-size(20); - display: inline-block; - vertical-align: middle; margin-right: ($baseline/2); color: $gray-l4; } @@ -91,11 +83,20 @@ body.course.checklists { .checklist-status { @include font-size(13); - width: flex-grid(2, 9); + width: flex-grid(3, 9); float: right; margin-top: ($baseline/2); text-align: right; color: $gray-l2; + + + .icon-confirm { + @include font-size(20); + display: inline-block; + vertical-align: middle; + margin-left: ($baseline/2); + color: $gray-l4; + } .status-count { @include font-size(16); @@ -192,13 +193,6 @@ body.course.checklists { color: $green; } } - - .checklist-status .icon-confirm { - @include font-size(12); - display: inline-block; - vertical-align: middle; - margin-right: ($baseline/4); - } } } @@ -230,41 +224,38 @@ body.course.checklists { border-bottom: none; } - label { + .task-input { + display: inline-block; + vertical-align: text-top; float: left; - width: flex-grid(7,9); + margin: ($baseline/2) flex-gutter() 0 0; + } + + .task-details { + display: inline-block; + vertical-align: text-top; + float: left; + width: flex-grid(6,9); font-weight: 500; - .task-input { - display: inline-block; - vertical-align: text-top; - margin: ($baseline/2) flex-gutter() 0 0; + .task-name { + @include transition(color .15s .25s ease-in-out); + vertical-align: baseline; + cursor: pointer; + margin-bottom: 0; + } + + .task-description { + @include transition(color .15s .25s ease-in-out); + @include font-size(14); + color: $gray-l2; } - .task-details { - display: inline-block; - vertical-align: text-top; - width: flex-grid(6,7); - - .task-name { - @include transition(color .15s .25s ease-in-out); - vertical-align: baseline; - cursor: pointer; - margin-bottom: 0; - } - - .task-description { - @include transition(color .15s .25s ease-in-out); - @include font-size(14); - color: $gray-l2; - } - - .task-support { - @include transition(opacity .15s .25s ease-in-out); - @include font-size(12); - opacity: 0; - pointer-events: none; - } + .task-support { + @include transition(opacity .15s .25s ease-in-out); + @include font-size(12); + opacity: 0; + pointer-events: none; } } @@ -273,7 +264,7 @@ body.course.checklists { @include clearfix(); display: inline-block; vertical-align: middle; - float: left; + float: right; width: flex-grid(2,9); margin: ($baseline/2) 0 0 flex-gutter(); opacity: 0; diff --git a/cms/templates/checklists.html b/cms/templates/checklists.html index 0e9be39fed..8415338466 100644 --- a/cms/templates/checklists.html +++ b/cms/templates/checklists.html @@ -48,27 +48,25 @@
    0% of checklist completed
    -

    +

    - - ${checklist['short_description']}

    - - Tasks Completed: 0/4 + ${checklist['short_description']} + + Tasks Completed: 0/4 + +
      % for item in checklist['items']:
    • -
    diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index 158c2b98d0..4173f424b6 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -70,6 +70,7 @@ class @Problem @bind() @num_queued_items = @new_queued_items.length + @updateProgress response if @num_queued_items == 0 delete window.queuePollerID else From eda6169b8b4a11dea221df37ec3cdfb93e36c819 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Fri, 15 Mar 2013 09:45:31 -0400 Subject: [PATCH 067/436] Pass along a url creator as opposed to just a url through the ModuleSystem. --- common/lib/capa/capa/responsetypes.py | 3 ++- .../open_ended_module.py | 4 ++-- .../xmodule/tests/test_combined_open_ended.py | 5 +++- lms/djangoapps/courseware/module_render.py | 23 +++++++++++++------ 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 62da901656..f997829cd0 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1413,8 +1413,9 @@ class CodeResponse(LoncapaResponse): queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime + anonymous_student_id + self.answer_id) + callback_url = self.system.xqueue['construct_callback']() xheader = xqueue_interface.make_xheader( - lms_callback_url=self.system.xqueue['callback_url'], + lms_callback_url=callback_url, lms_key=queuekey, queue_name=self.queue_name) diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py index 1f84d2ab8c..8373700837 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py @@ -174,7 +174,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): str(len(self.child_history))) xheader = xqueue_interface.make_xheader( - lms_callback_url=system.xqueue['callback_url'], + lms_callback_url=system.xqueue['construct_callback'](), lms_key=queuekey, queue_name=self.message_queue_name ) @@ -224,7 +224,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): anonymous_student_id + str(len(self.child_history))) - xheader = xqueue_interface.make_xheader(lms_callback_url=system.xqueue['callback_url'], + xheader = xqueue_interface.make_xheader(lms_callback_url=system.xqueue['construct_callback'](), lms_key=queuekey, queue_name=self.queue_name) diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py index 09c86baf27..aa8a077cc1 100644 --- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py +++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py @@ -183,7 +183,10 @@ class OpenEndedModuleTest(unittest.TestCase): self.test_system.location = self.location self.mock_xqueue = MagicMock() self.mock_xqueue.send_to_queue.return_value = (None, "Message") - self.test_system.xqueue = {'interface': self.mock_xqueue, 'callback_url': '/', 'default_queuename': 'testqueue', + def constructed_callback(dispatch = "score_update"): + return dispatch + + self.test_system.xqueue = {'interface': self.mock_xqueue, 'construct_callback': constructed_callback, 'default_queuename': 'testqueue', 'waittime': 1} self.openendedmodule = OpenEndedModule(self.test_system, self.location, self.definition, self.descriptor, self.static_data, self.metadata) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 08df7bfb8c..0954f8d28c 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -181,12 +181,21 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours host=request.get_host(), proto=request.META.get('HTTP_X_FORWARDED_PROTO', 'https' if request.is_secure() else 'http') ) - xqueue_callback_url += reverse('xqueue_callback', - kwargs=dict(course_id=course_id, - userid=str(user.id), - id=descriptor.location.url(), - dispatch='score_update'), - ) + + def make_xqueue_callback(dispatch = 'score_update'): + # Fully qualified callback URL for external queueing system + xqueue_callback_url = '{proto}://{host}'.format( + host=request.get_host(), + proto=request.META.get('HTTP_X_FORWARDED_PROTO', 'https' if request.is_secure() else 'http') + ) + + xqueue_callback_url += reverse('xqueue_callback', + kwargs=dict(course_id=course_id, + userid=str(user.id), + id=descriptor.location.url(), + dispatch=dispatch), + ) + return xqueue_callback_url # Default queuename is course-specific and is derived from the course that # contains the current module. @@ -194,7 +203,7 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours xqueue_default_queuename = descriptor.location.org + '-' + descriptor.location.course xqueue = {'interface': xqueue_interface, - 'callback_url': xqueue_callback_url, + 'construct_callback': make_xqueue_callback, 'default_queuename': xqueue_default_queuename.replace(' ', '_'), 'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS } From 45d8086e1cd5ac4fb7a583a411a9b7bdda27491b Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Fri, 15 Mar 2013 11:40:22 -0400 Subject: [PATCH 068/436] Set up ajax to submit to XQueue. Add some unit tests to make sure this is working properly --- common/lib/capa/capa/inputtypes.py | 37 +++++++++-- common/lib/capa/capa/responsetypes.py | 5 +- common/lib/capa/capa/tests/__init__.py | 9 ++- common/lib/capa/capa/tests/test_inputtypes.py | 63 +++++++++++++++++++ 4 files changed, 105 insertions(+), 9 deletions(-) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index f49ad5b422..08c395692f 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -48,6 +48,8 @@ import pyparsing from .registry import TagRegistry from capa.chem import chemcalc +import xqueue_interface +from datetime import datetime log = logging.getLogger(__name__) @@ -639,6 +641,7 @@ class MatlabInput(CodeInput): # Check if problem has been queued self.queue_len = 0 + self.queuename = 'matlab' # Flag indicating that the problem has been queued, 'msg' is length of # queue if self.status == 'incomplete': @@ -650,20 +653,44 @@ class MatlabInput(CodeInput): def handle_ajax(self, dispatch, get): if dispatch == 'plot': - # put the data in the queue and ship it off - pass - elif dispatch == 'display': + return self.plot_data(get) + elif dispatch == 'xqueue_response': # render the response pass def plot_data(self, get): ''' send data via xqueue to the mathworks backend''' - # only send data if xqueue exists if self.system.xqueue is not None: - pass + # pull relevant info out of get + response = get['submission'] + + # construct xqueue headers + qinterface = self.system.xqueue['interface'] + qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat) + callback_url = self.system.xqueue['construct_callback']('input_ajax') + anonymous_student_id = self.system.anonymous_student_id + queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime + + anonymous_student_id + + self.id) + xheader = xqueue_interface.make_xheader( + lms_callback_url = callback_url, + lms_key = queuekey, + queue_name = self.queuename) + # construct xqueue body + student_info = {'anonymous_student_id': anonymous_student_id, + 'submission_time': qtime} + contents = {'grader_payload': self.plot_payload, + 'student_info': json.dumps(student_info), + 'student_response': response} + + (error, msg) = qinterface.send_to_queue(header=xheader, + body = json.dumps(contents)) + + return json.dumps({'success': error != 0, 'message': msg}) + return json.dumps({'success': False, 'message': 'Cannot connect to the queue'}) registry.register(MatlabInput) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index f997829cd0..bb202e6d6e 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1271,8 +1271,9 @@ class CodeResponse(LoncapaResponse): Expects 'xqueue' dict in ModuleSystem with the following keys that are needed by CodeResponse: system.xqueue = { 'interface': XqueueInterface object, - 'callback_url': Per-StudentModule callback URL - where results are posted (string), + 'construct_callback': Per-StudentModule callback URL + constructor, defaults to using 'score_update' + as the correct dispatch (function), 'default_queuename': Default queuename to submit request (string) } diff --git a/common/lib/capa/capa/tests/__init__.py b/common/lib/capa/capa/tests/__init__.py index 89cb5a5ee9..7b1bffce62 100644 --- a/common/lib/capa/capa/tests/__init__.py +++ b/common/lib/capa/capa/tests/__init__.py @@ -2,7 +2,7 @@ import fs import fs.osfs import os -from mock import Mock +from mock import Mock, MagicMock import xml.sax.saxutils as saxutils @@ -16,6 +16,11 @@ def tst_render_template(template, context): """ return '
    {0}
    '.format(saxutils.escape(repr(context))) +def calledback_url(dispatch = 'score_update'): + return dispatch + +xqueue_interface = MagicMock() +xqueue_interface.send_to_queue.return_value = (1, 'Success!') test_system = Mock( ajax_url='courses/course_id/modx/a_location', @@ -26,7 +31,7 @@ test_system = Mock( user=Mock(), filestore=fs.osfs.OSFS(os.path.join(TEST_DIR, "test_files")), debug=True, - xqueue={'interface': None, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 10}, + xqueue={'interface': xqueue_interface, 'construct_callback': calledback_url, 'default_queuename': 'testqueue', 'waittime': 10}, node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"), anonymous_student_id='student' ) diff --git a/common/lib/capa/capa/tests/test_inputtypes.py b/common/lib/capa/capa/tests/test_inputtypes.py index 360fd9f2f6..01801ac822 100644 --- a/common/lib/capa/capa/tests/test_inputtypes.py +++ b/common/lib/capa/capa/tests/test_inputtypes.py @@ -23,6 +23,7 @@ import xml.sax.saxutils as saxutils from . import test_system from capa import inputtypes +from mock import ANY # just a handy shortcut lookup_tag = inputtypes.registry.get_class_for_tag @@ -300,6 +301,68 @@ class CodeInputTest(unittest.TestCase): self.assertEqual(context, expected) +class MatlabTest(unittest.TestCase): + ''' + Test Matlab input types + ''' + def setUp(self): + self.rows = '10' + self.cols = '80' + self.tabsize = '4' + self.mode = "" + self.payload = "payload" + self.linenumbers = 'true' + self.xml = """ + + {payload} + + """.format(r = self.rows, + c = self.cols, + tabsize = self.tabsize, + m = self.mode, + payload = self.payload, + ln = self.linenumbers) + elt = etree.fromstring(self.xml) + state = {'value': 'print "good evening"', + 'status': 'incomplete', + 'feedback': {'message': '3'}, } + + self.input_class = lookup_tag('matlabinput') + self.the_input = self.input_class(test_system, elt, state) + + + def test_rendering(self): + context = self.the_input._get_render_context() + + expected = {'id': 'prob_1_2', + 'value': 'print "good evening"', + 'status': 'queued', + 'msg': self.input_class.submitted_msg, + 'mode': self.mode, + 'rows': self.rows, + 'cols': self.cols, + 'linenumbers': 'true', + 'hidden': '', + 'tabsize': int(self.tabsize), + 'queue_len': '3', + } + + self.assertEqual(context, expected) + + def test_plot_data(self): + get = {'submission': 'x = 1234;'} + response = json.loads(self.the_input.handle_ajax("plot", get)) + + test_system.xqueue['interface'].send_to_queue.assert_called_with(header=ANY, body=ANY) + + + self.assertTrue(response['success']) + + + class SchematicTest(unittest.TestCase): ''' From 521c469a355a22d3125b9a6ae82c934b2dc5f10c Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Mon, 18 Mar 2013 17:14:16 -0400 Subject: [PATCH 069/436] Add the ability for input types to have their own state and add in a handler for ungraded responses via xqueue --- common/lib/capa/capa/capa_problem.py | 24 ++++++++ common/lib/capa/capa/inputtypes.py | 56 +++++++++++++++++-- .../lib/capa/capa/templates/matlabinput.html | 3 + common/lib/capa/capa/tests/test_inputtypes.py | 32 ++++++++++- common/lib/xmodule/xmodule/capa_module.py | 17 +++++- 5 files changed, 124 insertions(+), 8 deletions(-) diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 42753fc90b..fbf911e500 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -111,6 +111,7 @@ class LoncapaProblem(object): if self.system is None: raise Exception() self.seed = seed + self.input_state = None if state: if 'seed' in state: @@ -121,11 +122,16 @@ class LoncapaProblem(object): self.correct_map.set_dict(state['correct_map']) if 'done' in state: self.done = state['done'] + if 'input_state' in state: + self.input_state = state['input_state'] # TODO: Does this deplete the Linux entropy pool? Is this fast enough? if not self.seed: self.seed = struct.unpack('i', os.urandom(4))[0] + if not self.input_state: + self.input_state = {} + # Convert startouttext and endouttext to proper problem_text = re.sub("startouttext\s*/", "text", problem_text) problem_text = re.sub("endouttext\s*/", "/text", problem_text) @@ -188,6 +194,7 @@ class LoncapaProblem(object): return {'seed': self.seed, 'student_answers': self.student_answers, 'correct_map': self.correct_map.get_dict(), + 'input_state': self.input_state, 'done': self.done} def get_max_score(self): @@ -237,6 +244,19 @@ class LoncapaProblem(object): self.correct_map.set_dict(cmap.get_dict()) return cmap + def ungraded_response(self, xqueue_msg, queuekey): + ''' + Handle any responses from the xqueue that are not related to grading + + Does not return any value + ''' + # check against each inputtype + for the_input in self.inputs.values(): + # if the input type has an xqueue_response function, pass in the values + if hasattr(the_input, 'ungraded_response'): + the_input.ungraded_response(xqueue_msg, queuekey) + + def is_queued(self): ''' Returns True if any part of the problem has been submitted to an external queue @@ -527,11 +547,15 @@ class LoncapaProblem(object): value = "" if self.student_answers and problemid in self.student_answers: value = self.student_answers[problemid] + + if input_id not in self.input_state: + self.input_state[input_id] = {} # do the rendering state = {'value': value, 'status': status, 'id': input_id, + 'input_state': self.input_state[input_id], 'feedback': {'message': msg, 'hint': hint, 'hintmode': hintmode, }} diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 08c395692f..303619c820 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -134,6 +134,8 @@ class InputTypeBase(object): * 'id' -- the id of this input, typically "{problem-location}_{response-num}_{input-num}" * 'status' (answered, unanswered, unsubmitted) + * 'input_state' -- dictionary containing any inputtype-specific state + that has been preserved * 'feedback' (dictionary containing keys for hints, errors, or other feedback from previous attempt. Specifically 'message', 'hint', 'hintmode'. If 'hintmode' is 'always', the hint is always displayed.) @@ -160,6 +162,7 @@ class InputTypeBase(object): self.msg = feedback.get('message', '') self.hint = feedback.get('hint', '') self.hintmode = feedback.get('hintmode', None) + self.input_state = state.get('input_state', {}) # put hint above msg if it should be displayed if self.hintmode == 'always': @@ -643,8 +646,11 @@ class MatlabInput(CodeInput): self.queue_len = 0 self.queuename = 'matlab' # Flag indicating that the problem has been queued, 'msg' is length of + self.queue_msg = None # queue if self.status == 'incomplete': + if 'queue_msg' in self.input_state: + self.queue_msg = self.input_state['queue_msg'] self.status = 'queued' self.queue_len = self.msg self.msg = self.submitted_msg @@ -652,13 +658,47 @@ class MatlabInput(CodeInput): def handle_ajax(self, dispatch, get): + ''' Handle AJAX calls directed to this input''' if dispatch == 'plot': - return self.plot_data(get) - elif dispatch == 'xqueue_response': - # render the response - pass + return self._plot_data(get) - def plot_data(self, get): + def ungraded_response(self, queue_msg, queuekey): + ''' Handle any XQueue responses that have to be saved and rendered ''' + # check the queuekey against the saved queuekey + if('queuestate' in self.input_state and self.input_state['queuestate'] == 'queued' + and self.input_state['queuekey'] == queuekey): + msg = _parse_message(queue_msg) + # save the queue message so that it can be rendered later + self.input_state['queue_msg'] = msg + self.input_state['queued'] = 'dequeued' + + def _extra_context(self): + ''' Set up additional context variables''' + extra_context = {'queue_len': self.queue_len} + if self.queue_msg is not None: + extra_context['queue_msg'] = self.queue_msg + else: + extra_context['queue_msg'] = '' + return extra_context + + def _parse_data(self, queue_msg): + ''' + takes a queue_msg returned from the queue and parses it and returns + whatever is stored in msg + returns string msg + ''' + try: + result = json.loads(queue_msg) + except (TypeError, ValueError): + log.error("External message should be a JSON serialized dict." + " Received queue_msg = %s" % queue_msg) + raise + # TODO: needs more error checking + msg = result['msg'] + return msg + + + def _plot_data(self, get): ''' send data via xqueue to the mathworks backend''' # only send data if xqueue exists if self.system.xqueue is not None: @@ -668,7 +708,7 @@ class MatlabInput(CodeInput): # construct xqueue headers qinterface = self.system.xqueue['interface'] qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat) - callback_url = self.system.xqueue['construct_callback']('input_ajax') + callback_url = self.system.xqueue['construct_callback']('ungraded_response') anonymous_student_id = self.system.anonymous_student_id queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime + anonymous_student_id + @@ -678,6 +718,10 @@ class MatlabInput(CodeInput): lms_key = queuekey, queue_name = self.queuename) + # save the input state + self.input_state['queuekey'] = queuekey + self.input_state['queuestate'] = 'queued' + # construct xqueue body student_info = {'anonymous_student_id': anonymous_student_id, diff --git a/common/lib/capa/capa/templates/matlabinput.html b/common/lib/capa/capa/templates/matlabinput.html index ba516be249..07433f0a3a 100644 --- a/common/lib/capa/capa/templates/matlabinput.html +++ b/common/lib/capa/capa/templates/matlabinput.html @@ -29,6 +29,9 @@
    ${msg|n}
    +
    + ${queue_msg|n} +
    diff --git a/common/lib/capa/capa/tests/test_inputtypes.py b/common/lib/capa/capa/tests/test_inputtypes.py index 01801ac822..97e27d5ffc 100644 --- a/common/lib/capa/capa/tests/test_inputtypes.py +++ b/common/lib/capa/capa/tests/test_inputtypes.py @@ -344,6 +344,35 @@ class MatlabTest(unittest.TestCase): 'mode': self.mode, 'rows': self.rows, 'cols': self.cols, + 'queue_msg': '', + 'linenumbers': 'true', + 'hidden': '', + 'tabsize': int(self.tabsize), + 'queue_len': '3', + } + + self.assertEqual(context, expected) + + + def test_rendering_with_state(self): + state = {'value': 'print "good evening"', + 'status': 'incomplete', + 'input_state': {'queue_msg': 'message'}, + 'feedback': {'message': '3'}, } + elt = etree.fromstring(self.xml) + + input_class = lookup_tag('matlabinput') + the_input = self.input_class(test_system, elt, state) + context = the_input._get_render_context() + + expected = {'id': 'prob_1_2', + 'value': 'print "good evening"', + 'status': 'queued', + 'msg': self.input_class.submitted_msg, + 'mode': self.mode, + 'rows': self.rows, + 'cols': self.cols, + 'queue_msg': 'message', 'linenumbers': 'true', 'hidden': '', 'tabsize': int(self.tabsize), @@ -358,8 +387,9 @@ class MatlabTest(unittest.TestCase): test_system.xqueue['interface'].send_to_queue.assert_called_with(header=ANY, body=ANY) - self.assertTrue(response['success']) + self.assertTrue(self.the_input.input_state['queuekey'] is not None) + self.assertEqual(self.the_input.input_state['queuestate'], 'queued') diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index e66b1d3495..1bdd62f5b7 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -93,6 +93,7 @@ class CapaFields(object): rerandomize = Randomization(help="When to rerandomize the problem", default="always", scope=Scope.settings) data = String(help="XML data for the problem", scope=Scope.content) correct_map = Object(help="Dictionary with the correctness of current student answers", scope=Scope.student_state, default={}) + input_state = Object(help="Dictionary for maintaining the state of inputtypes", scope=Scope.student_state, default={}) student_answers = Object(help="Dictionary with the current student responses", scope=Scope.student_state) done = Boolean(help="Whether the student has answered the problem", scope=Scope.student_state) display_name = String(help="Display name for this module", scope=Scope.settings) @@ -188,6 +189,7 @@ class CapaModule(CapaFields, XModule): 'done': self.done, 'correct_map': self.correct_map, 'student_answers': self.student_answers, + 'input_state': self.input_state, 'seed': self.seed, } @@ -195,6 +197,7 @@ class CapaModule(CapaFields, XModule): lcp_state = self.lcp.get_state() self.done = lcp_state['done'] self.correct_map = lcp_state['correct_map'] + self.input_state = lcp_state['input_state'] self.student_answers = lcp_state['student_answers'] self.seed = lcp_state['seed'] @@ -443,7 +446,8 @@ class CapaModule(CapaFields, XModule): 'problem_save': self.save_problem, 'problem_show': self.get_answer, 'score_update': self.update_score, - 'input_ajax': self.lcp.handle_input_ajax + 'input_ajax': self.lcp.handle_input_ajax, + 'ungraded_response': self.handle_ungraded_response } if dispatch not in handlers: @@ -537,6 +541,17 @@ class CapaModule(CapaFields, XModule): return dict() # No AJAX return is needed + def handle_ungraded_response(self, get): + ''' + Get the XQueue response + ''' + queuekey = get['queuekey'] + score_msg = get['xqueue_body'] + # pass along the xqueue message to the problem + self.lcp.ungraded_response(score_msg, queuekey) + + self.set_state_from_lcp() + def get_answer(self, get): ''' For the "show answer" button. From 8649d67b9d6fe56d807a23d64d9ec8ab2e49b61a Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Tue, 19 Mar 2013 13:11:48 -0400 Subject: [PATCH 070/436] Force the progress bar update when we get a code response answer. --- common/lib/xmodule/xmodule/js/src/capa/display.coffee | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index 4173f424b6..70704ab247 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -41,6 +41,11 @@ class @Problem @el.attr progress: response.progress_status @el.trigger('progressChanged') + forceUpdate: (response) => + @el.attr progress: response.progress_status + @el.trigger('progressChanged') + + queueing: => @queued_items = @$(".xqueue") @num_queued_items = @queued_items.length @@ -70,8 +75,8 @@ class @Problem @bind() @num_queued_items = @new_queued_items.length - @updateProgress response if @num_queued_items == 0 + @forceUpdate response delete window.queuePollerID else # TODO: Some logic to dynamically adjust polling rate based on queuelen From f4d68d77f671abf3e63e8788b68d6416b18d3287 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Tue, 19 Mar 2013 15:29:40 -0400 Subject: [PATCH 071/436] Add Javascript for new button and fix Python backend issues --- common/lib/capa/capa/capa_problem.py | 5 +-- common/lib/capa/capa/inputtypes.py | 24 +++++++--- .../lib/capa/capa/templates/matlabinput.html | 45 ++++++++++++++++++- common/lib/capa/capa/tests/__init__.py | 2 +- common/lib/capa/capa/tests/test_inputtypes.py | 4 +- common/lib/xmodule/xmodule/capa_module.py | 3 +- 6 files changed, 67 insertions(+), 16 deletions(-) diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index fbf911e500..911f210812 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -548,14 +548,11 @@ class LoncapaProblem(object): if self.student_answers and problemid in self.student_answers: value = self.student_answers[problemid] - if input_id not in self.input_state: - self.input_state[input_id] = {} - # do the rendering state = {'value': value, 'status': status, 'id': input_id, - 'input_state': self.input_state[input_id], + 'input_state': self.input_state, 'feedback': {'message': msg, 'hint': hint, 'hintmode': hintmode, }} diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 303619c820..42865a01b5 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -162,7 +162,7 @@ class InputTypeBase(object): self.msg = feedback.get('message', '') self.hint = feedback.get('hint', '') self.hintmode = feedback.get('hintmode', None) - self.input_state = state.get('input_state', {}) + self.input_state_dict = state.get('input_state', {}) # put hint above msg if it should be displayed if self.hintmode == 'always': @@ -635,6 +635,11 @@ class MatlabInput(CodeInput): ''' Handle matlab-specific parsing ''' + # if we don't have state for this input type yet, make one + if self.id not in self.input_state_dict: + self.input_state_dict[self.id] = {} + + self.input_state = self.input_state_dict[self.id] xml = self.xml self.plot_payload = xml.findtext('./plot_payload') # if no student input yet, then use the default input given by the @@ -647,10 +652,13 @@ class MatlabInput(CodeInput): self.queuename = 'matlab' # Flag indicating that the problem has been queued, 'msg' is length of self.queue_msg = None + if 'queue_msg' in self.input_state: + self.queue_msg = self.input_state['queue_msg'] + if 'queued' in self.input_state and self.input_state['queuestate'] is not None: + self.status = 'queued' + self.queue_len = 1 # queue if self.status == 'incomplete': - if 'queue_msg' in self.input_state: - self.queue_msg = self.input_state['queue_msg'] self.status = 'queued' self.queue_len = self.msg self.msg = self.submitted_msg @@ -667,10 +675,11 @@ class MatlabInput(CodeInput): # check the queuekey against the saved queuekey if('queuestate' in self.input_state and self.input_state['queuestate'] == 'queued' and self.input_state['queuekey'] == queuekey): - msg = _parse_message(queue_msg) + msg = self._parse_data(queue_msg) # save the queue message so that it can be rendered later self.input_state['queue_msg'] = msg - self.input_state['queued'] = 'dequeued' + self.input_state['queuestate'] = None + self.input_state['queuekey'] = None def _extra_context(self): ''' Set up additional context variables''' @@ -733,8 +742,9 @@ class MatlabInput(CodeInput): (error, msg) = qinterface.send_to_queue(header=xheader, body = json.dumps(contents)) - return json.dumps({'success': error != 0, 'message': msg}) - return json.dumps({'success': False, 'message': 'Cannot connect to the queue'}) + + return {'success': error == 0, 'message': msg} + return {'success': False, 'message': 'Cannot connect to the queue'} registry.register(MatlabInput) diff --git a/common/lib/capa/capa/templates/matlabinput.html b/common/lib/capa/capa/templates/matlabinput.html index 07433f0a3a..cbfc4b119f 100644 --- a/common/lib/capa/capa/templates/matlabinput.html +++ b/common/lib/capa/capa/templates/matlabinput.html @@ -34,7 +34,7 @@
    - +
    diff --git a/common/lib/capa/capa/tests/__init__.py b/common/lib/capa/capa/tests/__init__.py index 7b1bffce62..72d82c683b 100644 --- a/common/lib/capa/capa/tests/__init__.py +++ b/common/lib/capa/capa/tests/__init__.py @@ -20,7 +20,7 @@ def calledback_url(dispatch = 'score_update'): return dispatch xqueue_interface = MagicMock() -xqueue_interface.send_to_queue.return_value = (1, 'Success!') +xqueue_interface.send_to_queue.return_value = (0, 'Success!') test_system = Mock( ajax_url='courses/course_id/modx/a_location', diff --git a/common/lib/capa/capa/tests/test_inputtypes.py b/common/lib/capa/capa/tests/test_inputtypes.py index 97e27d5ffc..b9da9df03f 100644 --- a/common/lib/capa/capa/tests/test_inputtypes.py +++ b/common/lib/capa/capa/tests/test_inputtypes.py @@ -357,7 +357,7 @@ class MatlabTest(unittest.TestCase): def test_rendering_with_state(self): state = {'value': 'print "good evening"', 'status': 'incomplete', - 'input_state': {'queue_msg': 'message'}, + 'input_state': {'prob_1_2': {'queue_msg': 'message'}}, 'feedback': {'message': '3'}, } elt = etree.fromstring(self.xml) @@ -383,7 +383,7 @@ class MatlabTest(unittest.TestCase): def test_plot_data(self): get = {'submission': 'x = 1234;'} - response = json.loads(self.the_input.handle_ajax("plot", get)) + response = self.the_input.handle_ajax("plot", get) test_system.xqueue['interface'].send_to_queue.assert_called_with(header=ANY, body=ANY) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 1bdd62f5b7..a522c796bb 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -460,6 +460,7 @@ class CapaModule(CapaFields, XModule): 'progress_changed': after != before, 'progress_status': Progress.to_js_status_str(after), }) + self.set_state_from_lcp() return json.dumps(d, cls=ComplexEncoder) def is_past_due(self): @@ -549,8 +550,8 @@ class CapaModule(CapaFields, XModule): score_msg = get['xqueue_body'] # pass along the xqueue message to the problem self.lcp.ungraded_response(score_msg, queuekey) - self.set_state_from_lcp() + return dict() def get_answer(self, get): ''' From a2957cb3b72726e164824afea7be32ed453eaeba Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Tue, 19 Mar 2013 16:36:33 -0400 Subject: [PATCH 072/436] Add in some JS messages for when things go wrong. --- common/lib/capa/capa/inputtypes.py | 1 - .../lib/capa/capa/templates/matlabinput.html | 23 ++++++++++++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 42865a01b5..5a47456fab 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -702,7 +702,6 @@ class MatlabInput(CodeInput): log.error("External message should be a JSON serialized dict." " Received queue_msg = %s" % queue_msg) raise - # TODO: needs more error checking msg = result['msg'] return msg diff --git a/common/lib/capa/capa/templates/matlabinput.html b/common/lib/capa/capa/templates/matlabinput.html index cbfc4b119f..e52a5297d1 100644 --- a/common/lib/capa/capa/templates/matlabinput.html +++ b/common/lib/capa/capa/templates/matlabinput.html @@ -60,9 +60,21 @@ $("#textbox_${id}").find('.CodeMirror-scroll').height(${int(13.5*eval(rows))}); + var gentle_alert = function (parent_elt, msg) { + if($(parent_elt).find('.capa_alert').length) { + $(parent_elt).find('.capa_alert').remove(); + } + var alert_elem = "
    " + msg + "
    "; + alert_elem = $(alert_elem).addClass('capa_alert'); + $(parent_elt).find('.action').after(alert_elem); + $(parent_elt).find('.capa_alert').css({opacity: 0}).animate({opacity: 1}, 700); + } + + // hook up the plot button var plot = function(event) { - url = $(this).closest('.problems-wrapper').data('url'); + var problem_elt = $(event.target).closest('.problems-wrapper'); + url = $(event.target).closest('.problems-wrapper').data('url'); input_id = "${id}"; // save the codemirror text to the textarea @@ -73,31 +85,30 @@ answer = input.serialize(); - // setup callback for + // setup callback for after we send information to plot var plot_callback = function(response) { if(response.success) { window.location.reload(); } else { - // TODO: show message + gentle_alert(problem_elt, msg); } } var save_callback = function(response) { if(response.success) { + // send information to the problem's plot functionality Problem.inputAjax(url, input_id, 'plot', {'submission': submission}, plot_callback); } else { - // TODO: show any messages + gentle_alert(problem_elt, msg); } } // save the answer $.postWithPrefix(url + '/problem_save', answer, save_callback); - - } $('#plot_${id}').click(plot); From 57f7acf86398698d1f54d15e4702092042aa7d3c Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Tue, 19 Mar 2013 17:10:38 -0400 Subject: [PATCH 073/436] Unbreak grading for capa problems Clean up some pylint errors --- common/lib/capa/capa/capa_problem.py | 8 +------- common/lib/capa/capa/inputtypes.py | 8 +++++++- .../lib/capa/capa/templates/matlabinput.html | 2 +- common/lib/xmodule/xmodule/capa_module.py | 19 ++++++++++++++++--- 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 911f210812..f1fea4d8e3 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -16,7 +16,6 @@ This is used by capa_module. from __future__ import division from datetime import datetime -import json import logging import math import numpy @@ -32,8 +31,6 @@ from xml.sax.saxutils import unescape from copy import deepcopy import chem -import chem.chemcalc -import chem.chemtools import chem.miller import verifiers import verifiers.draganddrop @@ -70,9 +67,6 @@ global_context = {'random': random, 'scipy': scipy, 'calc': calc, 'eia': eia, - 'chemcalc': chem.chemcalc, - 'chemtools': chem.chemtools, - 'miller': chem.miller, 'draganddrop': verifiers.draganddrop} # These should be removed from HTML output, including all subelements @@ -371,7 +365,7 @@ class LoncapaProblem(object): dispatch = get['dispatch'] return self.inputs[input_id].handle_ajax(dispatch, get) else: - log.warning("Could not find matching input for id: %s" % problem_id) + log.warning("Could not find matching input for id: %s" % input_id) return {} diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 5a47456fab..a62e696b20 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -37,7 +37,6 @@ graded status as'status' # makes sense, but a bunch of problems have markup that assumes block. Bigger TODO: figure out a # general css and layout strategy for capa, document it, then implement it. -from collections import namedtuple import json import logging from lxml import etree @@ -623,6 +622,13 @@ registry.register(CodeInput) class MatlabInput(CodeInput): ''' InputType for handling Matlab code input + + Example: + + + %api_key=API_KEY + + ''' template = "matlabinput.html" tags = ['matlabinput'] diff --git a/common/lib/capa/capa/templates/matlabinput.html b/common/lib/capa/capa/templates/matlabinput.html index e52a5297d1..6c02e8e68e 100644 --- a/common/lib/capa/capa/templates/matlabinput.html +++ b/common/lib/capa/capa/templates/matlabinput.html @@ -44,7 +44,7 @@ % if linenumbers == 'true': lineNumbers: true, % endif - mode: "${mode}", + mode: "matlab", matchBrackets: true, lineWrapping: true, indentUnit: "${tabsize}", diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index a522c796bb..6ce8d3a805 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -446,7 +446,7 @@ class CapaModule(CapaFields, XModule): 'problem_save': self.save_problem, 'problem_show': self.get_answer, 'score_update': self.update_score, - 'input_ajax': self.lcp.handle_input_ajax, + 'input_ajax': self.handle_input_ajax, 'ungraded_response': self.handle_ungraded_response } @@ -460,7 +460,6 @@ class CapaModule(CapaFields, XModule): 'progress_changed': after != before, 'progress_status': Progress.to_js_status_str(after), }) - self.set_state_from_lcp() return json.dumps(d, cls=ComplexEncoder) def is_past_due(self): @@ -544,7 +543,10 @@ class CapaModule(CapaFields, XModule): def handle_ungraded_response(self, get): ''' - Get the XQueue response + Delivers a response to the capa problem where the expectation where this response does + not have relevant grading information + + No ajax return is needed, so an empty dict is returned ''' queuekey = get['queuekey'] score_msg = get['xqueue_body'] @@ -553,6 +555,17 @@ class CapaModule(CapaFields, XModule): self.set_state_from_lcp() return dict() + def handle_input_ajax(self, get): + ''' + Passes information down to the capa problem so that it can handle its own ajax calls + Returns the response from the capa problem + ''' + response = self.lcp.handle_input_ajax(get) + # save any state changes that may occur + self.set_state_from_lcp() + return response + + def get_answer(self, get): ''' For the "show answer" button. From 74653ff8add08a1d63902e910736dbf41a6c0258 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Tue, 19 Mar 2013 15:18:29 -0400 Subject: [PATCH 074/436] Factory refactor working for lms lettuce tests. --- .../contentstore/features/common.py | 12 +- .../contentstore/features/factories.py | 34 --- common/djangoapps/student/tests/__init__.py | 0 common/djangoapps/student/tests/factories.py | 198 +++++++++++++++ common/djangoapps/student/tests/tests.py | 107 ++++++++ common/djangoapps/terrain/factories.py | 229 ++++-------------- common/djangoapps/terrain/steps.py | 4 +- 7 files changed, 362 insertions(+), 222 deletions(-) delete mode 100644 cms/djangoapps/contentstore/features/factories.py create mode 100644 common/djangoapps/student/tests/__init__.py create mode 100644 common/djangoapps/student/tests/factories.py create mode 100644 common/djangoapps/student/tests/tests.py 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/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..8dd70a7a61 --- /dev/null +++ b/common/djangoapps/student/tests/factories.py @@ -0,0 +1,198 @@ +from student.models import (User, UserProfile, Registration, + CourseEnrollmentAllowed) +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 + + +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 CourseEnrollmentAllowedFactory(Factory): + FACTORY_FOR = CourseEnrollmentAllowed + + email = 'test@edx.org' + course_id = 'edX/test/2012_Fall' + + +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): + """ + Factory for XModule courses. + """ + + 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): + """ + Factory for XModule items. + """ + + ABSTRACT_FACTORY = True + _creation_function = (XMODULE_ITEM_CREATION,) + + @classmethod + def _create(cls, target_class, *args, **kwargs): + """ + 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) + + # 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) + + # 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 + + # 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()]) + + return new_item + + +class Item: + pass + + +class ItemFactory(XModuleItemFactory): + FACTORY_FOR = Item + + parent_location = 'i4x://MITx/999/course/Robot_Super_Course' + template = 'i4x://edx/templates/chapter/Empty' + display_name = 'Section One' diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py new file mode 100644 index 0000000000..6a2d75e3d8 --- /dev/null +++ b/common/djangoapps/student/tests/tests.py @@ -0,0 +1,107 @@ +""" +This file demonstrates writing tests using the unittest module. These will pass +when you run "manage.py test". + +Replace this with more appropriate tests for your application. +""" +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 + +COURSE_1 = 'edX/toy/2012_Fall' +COURSE_2 = 'edx/full/6.002_Spring_2012' + +log = logging.getLogger(__name__) + + +class CourseEndingTest(TestCase): + """Test things related to course endings: certificates, surveys, etc""" + + def test_process_survey_link(self): + username = "fred" + user = Mock(username=username) + id = unique_id_for_user(user) + link1 = "http://www.mysurvey.com" + self.assertEqual(process_survey_link(link1, user), link1) + + link2 = "http://www.mysurvey.com?unique={UNIQUE_ID}" + link2_expected = "http://www.mysurvey.com?unique={UNIQUE_ID}".format(UNIQUE_ID=id) + self.assertEqual(process_survey_link(link2, user), link2_expected) + + def test_cert_info(self): + user = Mock(username="fred") + survey_url = "http://a_survey.com" + course = Mock(end_of_course_survey_url=survey_url) + + self.assertEqual(_cert_info(user, course, None), + {'status': 'processing', + 'show_disabled_download_button': False, + 'show_download_url': False, + 'show_survey_button': False, }) + + cert_status = {'status': 'unavailable'} + self.assertEqual(_cert_info(user, course, cert_status), + {'status': 'processing', + 'show_disabled_download_button': False, + 'show_download_url': False, + 'show_survey_button': False}) + + cert_status = {'status': 'generating', 'grade': '67'} + self.assertEqual(_cert_info(user, course, cert_status), + {'status': 'generating', + 'show_disabled_download_button': True, + 'show_download_url': False, + 'show_survey_button': True, + 'survey_url': survey_url, + 'grade': '67' + }) + + cert_status = {'status': 'regenerating', 'grade': '67'} + self.assertEqual(_cert_info(user, course, cert_status), + {'status': 'generating', + 'show_disabled_download_button': True, + 'show_download_url': False, + 'show_survey_button': True, + 'survey_url': survey_url, + 'grade': '67' + }) + + download_url = 'http://s3.edx/cert' + cert_status = {'status': 'downloadable', 'grade': '67', + 'download_url': download_url} + self.assertEqual(_cert_info(user, course, cert_status), + {'status': 'ready', + 'show_disabled_download_button': False, + 'show_download_url': True, + 'download_url': download_url, + 'show_survey_button': True, + 'survey_url': survey_url, + 'grade': '67' + }) + + cert_status = {'status': 'notpassing', 'grade': '67', + 'download_url': download_url} + self.assertEqual(_cert_info(user, course, cert_status), + {'status': 'notpassing', + 'show_disabled_download_button': False, + 'show_download_url': False, + 'show_survey_button': True, + 'survey_url': survey_url, + 'grade': '67' + }) + + # Test a course that doesn't have a survey specified + course2 = Mock(end_of_course_survey_url=None) + cert_status = {'status': 'notpassing', 'grade': '67', + 'download_url': download_url} + self.assertEqual(_cert_info(user, course2, cert_status), + {'status': 'notpassing', + 'show_disabled_download_button': False, + 'show_download_url': False, + 'show_survey_button': False, + 'grade': '67' + }) diff --git a/common/djangoapps/terrain/factories.py b/common/djangoapps/terrain/factories.py index c36bf935f1..62b133217d 100644 --- a/common/djangoapps/terrain/factories.py +++ b/common/djangoapps/terrain/factories.py @@ -1,190 +1,61 @@ -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 +from student.tests.factories import (UserFactory, UserProfileFactory, + RegistrationFactory, GroupFactory, + CourseEnrollmentAllowed, + CourseFactory, ItemFactory) +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(UserFactory): """ - Factory for XModule courses. - """ - - 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: + User account for lms / cms + """ 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(UserProfileFactory): """ - Factory for XModule items. - """ - - ABSTRACT_FACTORY = True - _creation_function = (XMODULE_ITEM_CREATION,) - - @classmethod - def _create(cls, target_class, *args, **kwargs): - """ - 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) - - # 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) - - # 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 - - # 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()]) - - return new_item - - -class Item: + Demographics etc for the User + """ pass -class ItemFactory(XModuleItemFactory): - FACTORY_FOR = Item +@world.absorb +class RegistrationFactory(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(GroupFactory): + """ + Groups for user permissions for courses + """ + pass + + +@world.absorb +class CourseEnrollmentAllowed(CourseEnrollmentAllowed): + """ + Users allowed to enroll in the course outside of the usual window + """ + pass + + +@world.absorb +class CourseFactory(CourseFactory): + """ + Courseware courses + """ + pass + + +@world.absorb +class ItemFactory(ItemFactory): + """ + Everything included inside a course + """ + pass diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index 52eeb23c4a..e9dcd7db31 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -119,11 +119,11 @@ def create_user(uname): 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 From daadaffb61fcbffb46ed555e8efb73740aa62853 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Tue, 19 Mar 2013 15:44:01 -0400 Subject: [PATCH 075/436] Add mongo contentstore to acceptance.py for lettuce tests for lms --- lms/envs/acceptance.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index 3dac545367..5280c7d288 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -29,6 +29,14 @@ MODULESTORE = { } } +CONTENTSTORE = { + 'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore', + 'OPTIONS': { + 'host': 'localhost', + 'db': 'test_xcontent', + } +} + # Set this up so that rake lms[acceptance] and running the # harvest command both use the same (test) database # which they can flush without messing up your dev db From 1c4ffcf122e53ad916cba195b8bd5328e48e8b60 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Tue, 19 Mar 2013 16:00:15 -0400 Subject: [PATCH 076/436] Fix import statement for test.py --- common/djangoapps/student/tests.py | 107 ----------------------- common/djangoapps/student/tests/tests.py | 4 +- 2 files changed, 2 insertions(+), 109 deletions(-) delete mode 100644 common/djangoapps/student/tests.py diff --git a/common/djangoapps/student/tests.py b/common/djangoapps/student/tests.py deleted file mode 100644 index 6a2d75e3d8..0000000000 --- a/common/djangoapps/student/tests.py +++ /dev/null @@ -1,107 +0,0 @@ -""" -This file demonstrates writing tests using the unittest module. These will pass -when you run "manage.py test". - -Replace this with more appropriate tests for your application. -""" -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 - -COURSE_1 = 'edX/toy/2012_Fall' -COURSE_2 = 'edx/full/6.002_Spring_2012' - -log = logging.getLogger(__name__) - - -class CourseEndingTest(TestCase): - """Test things related to course endings: certificates, surveys, etc""" - - def test_process_survey_link(self): - username = "fred" - user = Mock(username=username) - id = unique_id_for_user(user) - link1 = "http://www.mysurvey.com" - self.assertEqual(process_survey_link(link1, user), link1) - - link2 = "http://www.mysurvey.com?unique={UNIQUE_ID}" - link2_expected = "http://www.mysurvey.com?unique={UNIQUE_ID}".format(UNIQUE_ID=id) - self.assertEqual(process_survey_link(link2, user), link2_expected) - - def test_cert_info(self): - user = Mock(username="fred") - survey_url = "http://a_survey.com" - course = Mock(end_of_course_survey_url=survey_url) - - self.assertEqual(_cert_info(user, course, None), - {'status': 'processing', - 'show_disabled_download_button': False, - 'show_download_url': False, - 'show_survey_button': False, }) - - cert_status = {'status': 'unavailable'} - self.assertEqual(_cert_info(user, course, cert_status), - {'status': 'processing', - 'show_disabled_download_button': False, - 'show_download_url': False, - 'show_survey_button': False}) - - cert_status = {'status': 'generating', 'grade': '67'} - self.assertEqual(_cert_info(user, course, cert_status), - {'status': 'generating', - 'show_disabled_download_button': True, - 'show_download_url': False, - 'show_survey_button': True, - 'survey_url': survey_url, - 'grade': '67' - }) - - cert_status = {'status': 'regenerating', 'grade': '67'} - self.assertEqual(_cert_info(user, course, cert_status), - {'status': 'generating', - 'show_disabled_download_button': True, - 'show_download_url': False, - 'show_survey_button': True, - 'survey_url': survey_url, - 'grade': '67' - }) - - download_url = 'http://s3.edx/cert' - cert_status = {'status': 'downloadable', 'grade': '67', - 'download_url': download_url} - self.assertEqual(_cert_info(user, course, cert_status), - {'status': 'ready', - 'show_disabled_download_button': False, - 'show_download_url': True, - 'download_url': download_url, - 'show_survey_button': True, - 'survey_url': survey_url, - 'grade': '67' - }) - - cert_status = {'status': 'notpassing', 'grade': '67', - 'download_url': download_url} - self.assertEqual(_cert_info(user, course, cert_status), - {'status': 'notpassing', - 'show_disabled_download_button': False, - 'show_download_url': False, - 'show_survey_button': True, - 'survey_url': survey_url, - 'grade': '67' - }) - - # Test a course that doesn't have a survey specified - course2 = Mock(end_of_course_survey_url=None) - cert_status = {'status': 'notpassing', 'grade': '67', - 'download_url': download_url} - self.assertEqual(_cert_info(user, course2, cert_status), - {'status': 'notpassing', - 'show_disabled_download_button': False, - 'show_download_url': False, - 'show_survey_button': False, - 'grade': '67' - }) diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py index 6a2d75e3d8..4638da44b2 100644 --- a/common/djangoapps/student/tests/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' From 3576a3154b6386f95f8a48182791e1084d093756 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Tue, 19 Mar 2013 16:38:51 -0400 Subject: [PATCH 077/436] Repoint factory references in lettuce tests to world. --- .../features/studio-overview-togglesection.py | 21 +++++++++---------- common/djangoapps/student/tests/factories.py | 11 ++++++++-- common/djangoapps/terrain/factories.py | 10 ++++++++- lms/djangoapps/courseware/features/common.py | 9 ++++---- .../courseware/features/problems.py | 3 +-- .../django_comment_client/tests/test_utils.py | 16 +------------- 6 files changed, 34 insertions(+), 36 deletions(-) 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/common/djangoapps/student/tests/factories.py b/common/djangoapps/student/tests/factories.py index 8dd70a7a61..2acc235ce2 100644 --- a/common/djangoapps/student/tests/factories.py +++ b/common/djangoapps/student/tests/factories.py @@ -1,8 +1,8 @@ from student.models import (User, UserProfile, Registration, - CourseEnrollmentAllowed) + CourseEnrollmentAllowed, CourseEnrollment) from django.contrib.auth.models import Group from datetime import datetime -from factory import Factory +from factory import Factory, SubFactory from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore from time import gmtime @@ -50,6 +50,13 @@ class UserFactory(Factory): 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 diff --git a/common/djangoapps/terrain/factories.py b/common/djangoapps/terrain/factories.py index 62b133217d..3a3381c74c 100644 --- a/common/djangoapps/terrain/factories.py +++ b/common/djangoapps/terrain/factories.py @@ -1,6 +1,6 @@ from student.tests.factories import (UserFactory, UserProfileFactory, RegistrationFactory, GroupFactory, - CourseEnrollmentAllowed, + CourseEnrollmentAllowed, CourseEnrollment, CourseFactory, ItemFactory) from lettuce import world @@ -37,6 +37,14 @@ class GroupFactory(GroupFactory): pass +@world.absorb +class CourseEnrollment(CourseEnrollment): + """ + Courses that the user is enrolled in + """ + pass + + @world.absorb class CourseEnrollmentAllowed(CourseEnrollmentAllowed): """ diff --git a/lms/djangoapps/courseware/features/common.py b/lms/djangoapps/courseware/features/common.py index 8fb2843656..bdbd952caa 100644 --- a/lms/djangoapps/courseware/features/common.py +++ b/lms/djangoapps/courseware/features/common.py @@ -5,7 +5,6 @@ from lettuce.django import django_url from django.conf import settings from django.contrib.auth.models import User from student.models import CourseEnrollment -from terrain.factories import CourseFactory, ItemFactory from xmodule.modulestore import Location from xmodule.modulestore.django import _MODULESTORES, modulestore from xmodule.templates import update_templates @@ -101,15 +100,15 @@ def create_course(step, course): # Create the course # We always use the same org and display name, # but vary the course identifier (e.g. 600x or 191x) - course = CourseFactory.create(org=TEST_COURSE_ORG, + 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 = ItemFactory.create(parent_location=course.location, + section = world.ItemFactory.create(parent_location=course.location, display_name=TEST_SECTION_NAME) - problem_section = ItemFactory.create(parent_location=section.location, + problem_section = world.ItemFactory.create(parent_location=section.location, template='i4x://edx/templates/sequential/Empty', display_name=TEST_SECTION_NAME) @@ -131,7 +130,7 @@ def i_am_registered_for_the_course(step, course): @step(u'The course "([^"]*)" has extra tab "([^"]*)"$') def add_tab_to_course(step, course, extra_tab_name): - section_item = ItemFactory.create(parent_location=course_location(course), + section_item = world.ItemFactory.create(parent_location=course_location(course), template="i4x://edx/templates/static_tab/Empty", display_name=str(extra_tab_name)) diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index a6575c3d22..a2d37ff7d8 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -4,7 +4,6 @@ from selenium.webdriver.support.ui import Select import random import textwrap from common import i_am_registered_for_the_course, TEST_SECTION_NAME, section_location -from terrain.factories import ItemFactory from capa.tests.response_xml_factory import OptionResponseXMLFactory, \ ChoiceResponseXMLFactory, MultipleChoiceResponseXMLFactory, \ StringResponseXMLFactory, NumericalResponseXMLFactory, \ @@ -92,7 +91,7 @@ def add_problem_to_course(course, problem_type): # Create a problem item using our generated XML # We set rerandomize=always in the metadata so that the "Reset" button # will appear. - problem_item = ItemFactory.create(parent_location=section_location(course), + 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, diff --git a/lms/djangoapps/django_comment_client/tests/test_utils.py b/lms/djangoapps/django_comment_client/tests/test_utils.py index cec006e630..fe580f1ebe 100644 --- a/lms/djangoapps/django_comment_client/tests/test_utils.py +++ b/lms/djangoapps/django_comment_client/tests/test_utils.py @@ -8,6 +8,7 @@ import factory from django.contrib.auth.models import User from student.models import UserProfile, CourseEnrollment from django_comment_client.models import Role, Permission +from student.tests.factories import UserFactory, CourseEnrollmentFactory import django_comment_client.models as models import django_comment_client.utils as utils @@ -15,21 +16,6 @@ import django_comment_client.utils as utils import xmodule.modulestore.django as django -class UserFactory(factory.Factory): - FACTORY_FOR = User - username = 'robot' - password = '123456' - email = 'robot@edx.org' - is_active = True - is_staff = False - - -class CourseEnrollmentFactory(factory.Factory): - FACTORY_FOR = CourseEnrollment - user = factory.SubFactory(UserFactory) - course_id = 'edX/toy/2012_Fall' - - class RoleFactory(factory.Factory): FACTORY_FOR = Role name = 'Student' From 16773aac638a3055df3cae4185455ec5df5b6201 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Tue, 19 Mar 2013 17:09:57 -0400 Subject: [PATCH 078/436] More factory refactoring. --- common/djangoapps/terrain/factories.py | 4 ++-- lms/djangoapps/courseware/features/common.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/common/djangoapps/terrain/factories.py b/common/djangoapps/terrain/factories.py index 3a3381c74c..88aa94ef00 100644 --- a/common/djangoapps/terrain/factories.py +++ b/common/djangoapps/terrain/factories.py @@ -38,7 +38,7 @@ class GroupFactory(GroupFactory): @world.absorb -class CourseEnrollment(CourseEnrollment): +class CourseEnrollmentFactory(CourseEnrollment): """ Courses that the user is enrolled in """ @@ -46,7 +46,7 @@ class CourseEnrollment(CourseEnrollment): @world.absorb -class CourseEnrollmentAllowed(CourseEnrollmentAllowed): +class CourseEnrollmentAllowedFactory(CourseEnrollmentAllowed): """ Users allowed to enroll in the course outside of the usual window """ diff --git a/lms/djangoapps/courseware/features/common.py b/lms/djangoapps/courseware/features/common.py index bdbd952caa..eff3ce3743 100644 --- a/lms/djangoapps/courseware/features/common.py +++ b/lms/djangoapps/courseware/features/common.py @@ -1,8 +1,6 @@ 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 @@ -123,6 +121,7 @@ def i_am_registered_for_the_course(step, course): u = User.objects.get(username='robot') # 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@edx.org', 'test') From 10c6e7615bbea19e0687e62e36b26d7515ea603c Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Wed, 20 Mar 2013 09:42:42 -0400 Subject: [PATCH 079/436] More polish for matlab input type --- common/lib/capa/capa/inputtypes.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index a62e696b20..0208f32503 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -657,8 +657,8 @@ class MatlabInput(CodeInput): self.queue_len = 0 self.queuename = 'matlab' # Flag indicating that the problem has been queued, 'msg' is length of - self.queue_msg = None - if 'queue_msg' in self.input_state: + self.queue_msg = '' + if 'queue_msg' in self.input_state and self.status in ['incomplete', 'unsubmitted']: self.queue_msg = self.input_state['queue_msg'] if 'queued' in self.input_state and self.input_state['queuestate'] is not None: self.status = 'queued' @@ -689,11 +689,10 @@ class MatlabInput(CodeInput): def _extra_context(self): ''' Set up additional context variables''' - extra_context = {'queue_len': self.queue_len} - if self.queue_msg is not None: - extra_context['queue_msg'] = self.queue_msg - else: - extra_context['queue_msg'] = '' + extra_context = { + 'queue_len': self.queue_len, + 'queue_msg': self.queue_msg + } return extra_context def _parse_data(self, queue_msg): @@ -747,7 +746,6 @@ class MatlabInput(CodeInput): (error, msg) = qinterface.send_to_queue(header=xheader, body = json.dumps(contents)) - return {'success': error == 0, 'message': msg} return {'success': False, 'message': 'Cannot connect to the queue'} From 8cf2cc6f78c34b8f16506e291c7993f71b3ccb32 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 20 Mar 2013 10:42:35 -0400 Subject: [PATCH 080/436] studio - sass cleanup: local merge with master and small comment/organization tweaks to variables and base-styles --- cms/static/sass/_variables.scss | 4 ++-- cms/static/sass/base-style.scss | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cms/static/sass/_variables.scss b/cms/static/sass/_variables.scss index 78b6f2b221..15654fe3c4 100644 --- a/cms/static/sass/_variables.scss +++ b/cms/static/sass/_variables.scss @@ -15,7 +15,6 @@ $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); @@ -97,4 +96,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/base-style.scss b/cms/static/sass/base-style.scss index b092f0054b..c7ec38e756 100644 --- a/cms/static/sass/base-style.scss +++ b/cms/static/sass/base-style.scss @@ -46,5 +46,6 @@ @import 'assets/content-types'; +// xblock-related @import 'module/module-styles.scss'; @import 'descriptor/module-styles.scss'; From e45ccbf38929274d0be1a974925b2169c6f58225 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Wed, 20 Mar 2013 11:12:54 -0400 Subject: [PATCH 081/436] Leave CourseEnrollment factor refactoring as a TODO. --- common/djangoapps/terrain/factories.py | 10 +--------- .../django_comment_client/tests/test_utils.py | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/common/djangoapps/terrain/factories.py b/common/djangoapps/terrain/factories.py index 88aa94ef00..1dedff133b 100644 --- a/common/djangoapps/terrain/factories.py +++ b/common/djangoapps/terrain/factories.py @@ -1,6 +1,6 @@ from student.tests.factories import (UserFactory, UserProfileFactory, RegistrationFactory, GroupFactory, - CourseEnrollmentAllowed, CourseEnrollment, + CourseEnrollmentAllowed, CourseFactory, ItemFactory) from lettuce import world @@ -37,14 +37,6 @@ class GroupFactory(GroupFactory): pass -@world.absorb -class CourseEnrollmentFactory(CourseEnrollment): - """ - Courses that the user is enrolled in - """ - pass - - @world.absorb class CourseEnrollmentAllowedFactory(CourseEnrollmentAllowed): """ diff --git a/lms/djangoapps/django_comment_client/tests/test_utils.py b/lms/djangoapps/django_comment_client/tests/test_utils.py index fe580f1ebe..cec006e630 100644 --- a/lms/djangoapps/django_comment_client/tests/test_utils.py +++ b/lms/djangoapps/django_comment_client/tests/test_utils.py @@ -8,7 +8,6 @@ import factory from django.contrib.auth.models import User from student.models import UserProfile, CourseEnrollment from django_comment_client.models import Role, Permission -from student.tests.factories import UserFactory, CourseEnrollmentFactory import django_comment_client.models as models import django_comment_client.utils as utils @@ -16,6 +15,21 @@ import django_comment_client.utils as utils import xmodule.modulestore.django as django +class UserFactory(factory.Factory): + FACTORY_FOR = User + username = 'robot' + password = '123456' + email = 'robot@edx.org' + is_active = True + is_staff = False + + +class CourseEnrollmentFactory(factory.Factory): + FACTORY_FOR = CourseEnrollment + user = factory.SubFactory(UserFactory) + course_id = 'edX/toy/2012_Fall' + + class RoleFactory(factory.Factory): FACTORY_FOR = Role name = 'Student' From 8f055ab0377726f4ec0b8729eeef4fd4a805136e Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 20 Mar 2013 11:14:42 -0400 Subject: [PATCH 082/436] Moved mock_xqueue_server to its own subpackage within lms/djangoapps/courseware/ Separated tests of the mock server into test_mock_xqueue_server.py --- .../courseware/features/xqueue_setup.py | 3 +- .../mock_xqueue_server.py | 70 +------------------ 2 files changed, 2 insertions(+), 71 deletions(-) rename lms/djangoapps/courseware/{features => mock_xqueue_server}/mock_xqueue_server.py (74%) diff --git a/lms/djangoapps/courseware/features/xqueue_setup.py b/lms/djangoapps/courseware/features/xqueue_setup.py index 1261f72e76..23706941a9 100644 --- a/lms/djangoapps/courseware/features/xqueue_setup.py +++ b/lms/djangoapps/courseware/features/xqueue_setup.py @@ -1,9 +1,8 @@ -from mock_xqueue_server import MockXQueueServer +from courseware.mock_xqueue_server.mock_xqueue_server import MockXQueueServer from lettuce import before, after, world from django.conf import settings import threading - @before.all def setup_mock_xqueue_server(): diff --git a/lms/djangoapps/courseware/features/mock_xqueue_server.py b/lms/djangoapps/courseware/mock_xqueue_server/mock_xqueue_server.py similarity index 74% rename from lms/djangoapps/courseware/features/mock_xqueue_server.py rename to lms/djangoapps/courseware/mock_xqueue_server/mock_xqueue_server.py index 37678ce7c2..7bc1b95662 100644 --- a/lms/djangoapps/courseware/features/mock_xqueue_server.py +++ b/lms/djangoapps/courseware/mock_xqueue_server/mock_xqueue_server.py @@ -2,7 +2,7 @@ from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler import json import urllib import urlparse -import time +import threading from logging import getLogger logger = getLogger(__name__) @@ -209,71 +209,3 @@ class MockXQueueServer(HTTPServer): # Save the response dictionary self._grade_response = grade_response_dict - - -# ---------------------------- -# Tests - -import mock -import threading -import unittest - - -class MockXQueueServerTest(unittest.TestCase): - - def setUp(self): - - # Create the server - server_port = 8034 - self.server_url = 'http://127.0.0.1:%d' % server_port - self.server = MockXQueueServer(server_port, - {'correct': True, 'score': 1, 'msg': ''}) - - # Start the server in a separate daemon thread - server_thread = threading.Thread(target=self.server.serve_forever) - server_thread.daemon = True - server_thread.start() - - def tearDown(self): - - # Stop the server, freeing up the port - self.server.shutdown() - - def test_grade_request(self): - - # Patch post_to_url() so we can intercept - # outgoing POST requests from the server - MockXQueueRequestHandler.post_to_url = mock.Mock() - - # Send a grade request - callback_url = 'http://127.0.0.1:8000/test_callback' - - grade_header = json.dumps({'lms_callback_url': callback_url, - 'lms_key': 'test_queuekey', - 'queue_name': 'test_queue'}) - - grade_body = json.dumps({'student_info': 'test', - 'grader_payload': 'test', - 'student_response': 'test'}) - - grade_request = {'xqueue_header': grade_header, - 'xqueue_body': grade_body} - - response_handle = urllib.urlopen(self.server_url + '/xqueue/submit', - urllib.urlencode(grade_request)) - - response_dict = json.loads(response_handle.read()) - - # Expect that the response is success - self.assertEqual(response_dict['return_code'], 0) - - # Wait a bit before checking that the server posted back - time.sleep(3) - - # Expect that the server tries to post back the grading info - xqueue_body = json.dumps({'correct': True, 'score': 1, - 'msg': '
    '}) - expected_callback_dict = {'xqueue_header': grade_header, - 'xqueue_body': xqueue_body} - MockXQueueRequestHandler.post_to_url.assert_called_with(callback_url, - expected_callback_dict) From 3a774264172bd5b616b865c0f7191fa2e7927d3a Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 20 Mar 2013 11:16:30 -0400 Subject: [PATCH 083/436] Cale said, 'we should delete prod-requirements.txt' --- prod-requirements.txt | 53 ------------------------------------------- 1 file changed, 53 deletions(-) delete mode 100644 prod-requirements.txt diff --git a/prod-requirements.txt b/prod-requirements.txt deleted file mode 100644 index 20c3e9b068..0000000000 --- a/prod-requirements.txt +++ /dev/null @@ -1,53 +0,0 @@ -Django==1.3.1 -flup==1.0.3.dev-20110405 -lxml==2.3.4 -Mako==0.7.0 -Markdown==2.1.1 -markdown2==1.4.2 -python-memcached==1.48 -numpy==1.6.1 -Pygments==1.5 -boto==2.3.0 -django-storages==1.1.4 -django-masquerade==0.1.5 -fs==0.4.0 -django-jasmine==0.3.2 -path.py==2.2.2 -requests==0.12.1 -BeautifulSoup==3.2.1 -BeautifulSoup4==4.1.1 -newrelic==1.3.0.289 -ipython==0.12.1 -django-pipeline==1.2.12 -django-staticfiles==1.2.1 -glob2==0.3 -sympy==0.7.1 -pymongo==2.2.1 -rednose==0.3.3 -mock==0.8.0 -GitPython==0.3.2.RC1 -PyYAML==3.10 -feedparser==5.1.2 -MySQL-python==1.2.3 -matplotlib==1.1.0 -scipy==0.10.1 -akismet==0.2.0 -Coffin==0.3.6 -django-celery==2.2.7 -django-countries==1.0.5 -django-followit==0.0.3 -django-keyedcache==1.4-6 -django-kombu==0.9.2 -django-mako==0.1.5pre -django-recaptcha-works==0.3.4 -django-robots==0.8.1 -django-ses==0.4.1 -django-threaded-multihost==1.4-1 -html5lib==0.90 -Jinja2==2.6 -oauth2==1.5.211 -pystache==0.3.1 -python-openid==2.2.5 -South==0.7.5 -Unidecode==0.04.9 -dogstatsd-python==0.2.1 From 2867476115f54e7e008339e74949b480d1282ca3 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 20 Mar 2013 11:18:41 -0400 Subject: [PATCH 084/436] Added __init__.py and test_mock_xqueue_server.py, which should have been included in the last commit --- .../courseware/mock_xqueue_server/__init__.py | 0 .../test_mock_xqueue_server.py | 78 +++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 lms/djangoapps/courseware/mock_xqueue_server/__init__.py create mode 100644 lms/djangoapps/courseware/mock_xqueue_server/test_mock_xqueue_server.py diff --git a/lms/djangoapps/courseware/mock_xqueue_server/__init__.py b/lms/djangoapps/courseware/mock_xqueue_server/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/courseware/mock_xqueue_server/test_mock_xqueue_server.py b/lms/djangoapps/courseware/mock_xqueue_server/test_mock_xqueue_server.py new file mode 100644 index 0000000000..4e4d95f23b --- /dev/null +++ b/lms/djangoapps/courseware/mock_xqueue_server/test_mock_xqueue_server.py @@ -0,0 +1,78 @@ +import mock +import unittest +import threading +import json +import urllib +import urlparse +import time +from mock_xqueue_server import MockXQueueServer, MockXQueueRequestHandler + + +class MockXQueueServerTest(unittest.TestCase): + ''' + A mock version of the XQueue server that listens on a local + port and responds with pre-defined grade messages. + + Used for lettuce BDD tests in lms/courseware/features/problems.feature + and lms/courseware/features/problems.py + + This is temporary and will be removed when XQueue is + rewritten using celery. + ''' + + def setUp(self): + + # Create the server + server_port = 8034 + self.server_url = 'http://127.0.0.1:%d' % server_port + self.server = MockXQueueServer(server_port, + {'correct': True, 'score': 1, 'msg': ''}) + + # Start the server in a separate daemon thread + server_thread = threading.Thread(target=self.server.serve_forever) + server_thread.daemon = True + server_thread.start() + + def tearDown(self): + + # Stop the server, freeing up the port + self.server.shutdown() + + def test_grade_request(self): + + # Patch post_to_url() so we can intercept + # outgoing POST requests from the server + MockXQueueRequestHandler.post_to_url = mock.Mock() + + # Send a grade request + callback_url = 'http://127.0.0.1:8000/test_callback' + + grade_header = json.dumps({'lms_callback_url': callback_url, + 'lms_key': 'test_queuekey', + 'queue_name': 'test_queue'}) + + grade_body = json.dumps({'student_info': 'test', + 'grader_payload': 'test', + 'student_response': 'test'}) + + grade_request = {'xqueue_header': grade_header, + 'xqueue_body': grade_body} + + response_handle = urllib.urlopen(self.server_url + '/xqueue/submit', + urllib.urlencode(grade_request)) + + response_dict = json.loads(response_handle.read()) + + # Expect that the response is success + self.assertEqual(response_dict['return_code'], 0) + + # Wait a bit before checking that the server posted back + time.sleep(3) + + # Expect that the server tries to post back the grading info + xqueue_body = json.dumps({'correct': True, 'score': 1, + 'msg': '
    '}) + expected_callback_dict = {'xqueue_header': grade_header, + 'xqueue_body': xqueue_body} + MockXQueueRequestHandler.post_to_url.assert_called_with(callback_url, + expected_callback_dict) From f181a454e4c0a1d101cabeda4e25e0a8f0bff9ac Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 20 Mar 2013 11:20:16 -0400 Subject: [PATCH 085/436] studio - added in basic JS confirm UI when deleting course updates --- cms/static/js/views/course_info_edit.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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; From 5411fc765e6a45ad11f4f730ae51eb8a4a569d8b Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 20 Mar 2013 11:27:28 -0400 Subject: [PATCH 086/436] Refactored lettuce test of capa problems to use world.css_click() helper instead of directly calling splinter. --- common/djangoapps/terrain/steps.py | 13 +++++++++++++ lms/djangoapps/courseware/features/problems.py | 14 ++------------ 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index 52eeb23c4a..371496f823 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -9,6 +9,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__) @@ -214,3 +215,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/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index 314ffa0dbe..715e2689fb 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -1,7 +1,5 @@ from lettuce import world, step from lettuce.django import django_url -from selenium.webdriver.support.ui import Select -from selenium.common.exceptions import WebDriverException import random import textwrap import time @@ -208,20 +206,12 @@ def answer_problem(step, problem_type, correctness): @step(u'I check a problem') def check_problem(step): - try: - world.browser.find_by_css("input.check").click() - - except WebDriverException: - # Occassionally, MathJax or other JavaScript can cover up - # the 'Check' input temporarily. - # If this happens, wait a second, then try again - time.sleep(1) - world.browser.find_by_css("input.check").click() + world.css_click("input.check") @step(u'I reset the problem') def reset_problem(step): - world.browser.find_by_css('input.reset').click() + world.css_click('input.reset') @step(u'My "([^"]*)" answer is marked "([^"]*)"') From 6048bc28320225db3b2f09cf9ff4742ce36519d1 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Wed, 20 Mar 2013 11:31:12 -0400 Subject: [PATCH 087/436] Pep8 fixes for factory refactor --- common/djangoapps/student/tests/factories.py | 12 +++--- common/djangoapps/terrain/factories.py | 22 +++++----- lms/djangoapps/courseware/features/common.py | 16 +++---- lms/djangoapps/courseware/features/courses.py | 9 ++-- .../courseware/features/problems.py | 42 +++++++++---------- .../courseware/features/smart-accordion.py | 6 +-- 6 files changed, 53 insertions(+), 54 deletions(-) diff --git a/common/djangoapps/student/tests/factories.py b/common/djangoapps/student/tests/factories.py index 2acc235ce2..1f2378a8c9 100644 --- a/common/djangoapps/student/tests/factories.py +++ b/common/djangoapps/student/tests/factories.py @@ -1,4 +1,4 @@ -from student.models import (User, UserProfile, Registration, +from student.models import (User, UserProfile, Registration, CourseEnrollmentAllowed, CourseEnrollment) from django.contrib.auth.models import Group from datetime import datetime @@ -101,10 +101,10 @@ class XModuleCourseFactory(Factory): 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"}] + {"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)) @@ -170,7 +170,7 @@ class XModuleItemFactory(Factory): # 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) + name=dest_name) new_item = store.clone_item(template, dest_location) diff --git a/common/djangoapps/terrain/factories.py b/common/djangoapps/terrain/factories.py index 1dedff133b..88e29a18ef 100644 --- a/common/djangoapps/terrain/factories.py +++ b/common/djangoapps/terrain/factories.py @@ -1,7 +1,7 @@ from student.tests.factories import (UserFactory, UserProfileFactory, - RegistrationFactory, GroupFactory, - CourseEnrollmentAllowed, - CourseFactory, ItemFactory) + RegistrationFactory, GroupFactory, + CourseEnrollmentAllowed, + CourseFactory, ItemFactory) from lettuce import world @@ -9,7 +9,7 @@ from lettuce import world class UserFactory(UserFactory): """ User account for lms / cms - """ + """ pass @@ -17,7 +17,7 @@ class UserFactory(UserFactory): class UserProfileFactory(UserProfileFactory): """ Demographics etc for the User - """ + """ pass @@ -25,7 +25,7 @@ class UserProfileFactory(UserProfileFactory): class RegistrationFactory(RegistrationFactory): """ Activation key for registering the user account - """ + """ pass @@ -33,7 +33,7 @@ class RegistrationFactory(RegistrationFactory): class GroupFactory(GroupFactory): """ Groups for user permissions for courses - """ + """ pass @@ -41,7 +41,7 @@ class GroupFactory(GroupFactory): class CourseEnrollmentAllowedFactory(CourseEnrollmentAllowed): """ Users allowed to enroll in the course outside of the usual window - """ + """ pass @@ -49,13 +49,13 @@ class CourseEnrollmentAllowedFactory(CourseEnrollmentAllowed): class CourseFactory(CourseFactory): """ Courseware courses - """ + """ pass - + @world.absorb class ItemFactory(ItemFactory): """ Everything included inside a course - """ + """ pass diff --git a/lms/djangoapps/courseware/features/common.py b/lms/djangoapps/courseware/features/common.py index eff3ce3743..d03a59f776 100644 --- a/lms/djangoapps/courseware/features/common.py +++ b/lms/djangoapps/courseware/features/common.py @@ -99,16 +99,16 @@ def create_course(step, 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) + 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) + 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) + template='i4x://edx/templates/sequential/Empty', + display_name=TEST_SECTION_NAME) @step(u'I am registered for the course "([^"]*)"$') @@ -130,8 +130,8 @@ def i_am_registered_for_the_course(step, course): @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)) + template="i4x://edx/templates/static_tab/Empty", + display_name=str(extra_tab_name)) @step(u'I am an edX user$') @@ -159,7 +159,7 @@ def flush_xmodule_store(): def course_id(course_num): return "%s/%s/%s" % (TEST_COURSE_ORG, course_num, - TEST_COURSE_NAME.replace(" ", "_")) + TEST_COURSE_NAME.replace(" ", "_")) def course_location(course_num): diff --git a/lms/djangoapps/courseware/features/courses.py b/lms/djangoapps/courseware/features/courses.py index 4fbbfd24f2..c99fb58b85 100644 --- a/lms/djangoapps/courseware/features/courses.py +++ b/lms/djangoapps/courseware/features/courses.py @@ -83,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 @@ -168,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/problems.py b/lms/djangoapps/courseware/features/problems.py index a2d37ff7d8..9f3a483c57 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -5,9 +5,9 @@ import random import textwrap 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 + ChoiceResponseXMLFactory, MultipleChoiceResponseXMLFactory, \ + StringResponseXMLFactory, NumericalResponseXMLFactory, \ + FormulaResponseXMLFactory, CustomResponseXMLFactory # Factories from capa.tests.response_xml_factory that we will use # to generate the problem XML, with the keyword args used to configure @@ -77,7 +77,7 @@ PROBLEM_FACTORY_DICT = { a2=0 return (a1+a2)==int(expect) """)}}, - } +} def add_problem_to_course(course, problem_type): @@ -92,10 +92,10 @@ def add_problem_to_course(course, problem_type): # 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'}) + 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') @@ -201,21 +201,21 @@ def assert_answer_mark(step, problem_type, correctness): # depending on whether the user selects an incorrect # item or submits without selecting any item) correct_selectors = {'drop down': ['span.correct'], - 'multiple choice': ['label.choicegroup_correct'], - 'checkbox': ['span.correct'], - 'string': ['div.correct'], - 'numerical': ['div.correct'], - 'formula': ['div.correct'], - 'script': ['div.correct'], } + 'multiple choice': ['label.choicegroup_correct'], + 'checkbox': ['span.correct'], + 'string': ['div.correct'], + 'numerical': ['div.correct'], + 'formula': ['div.correct'], + 'script': ['div.correct'], } incorrect_selectors = {'drop down': ['span.incorrect'], 'multiple choice': ['label.choicegroup_incorrect', - 'span.incorrect'], - 'checkbox': ['span.incorrect'], - 'string': ['div.incorrect'], - 'numerical': ['div.incorrect'], - 'formula': ['div.incorrect'], - 'script': ['div.incorrect']} + 'span.incorrect'], + 'checkbox': ['span.incorrect'], + 'string': ['div.incorrect'], + 'numerical': ['div.incorrect'], + 'formula': ['div.incorrect'], + 'script': ['div.incorrect']} assert(correctness in ['correct', 'incorrect', 'unanswered']) assert(problem_type in correct_selectors and problem_type in incorrect_selectors) @@ -257,7 +257,7 @@ def inputfield(problem_type, choice=None, input_num=1): of checkboxes. """ sel = ("input#input_i4x-edx-model_course-problem-%s_2_%s" % - (problem_type.replace(" ", "_"), str(input_num))) + (problem_type.replace(" ", "_"), str(input_num))) if choice is not None: base = "_choice_" if problem_type == "multiple choice" else "_" diff --git a/lms/djangoapps/courseware/features/smart-accordion.py b/lms/djangoapps/courseware/features/smart-accordion.py index 7c4770d632..a7eb782722 100644 --- a/lms/djangoapps/courseware/features/smart-accordion.py +++ b/lms/djangoapps/courseware/features/smart-accordion.py @@ -81,7 +81,7 @@ def browse_course(course_id): num_rendered_sections = len(rendered_sections) msg = ('%d sections expected, %d sections found on page, %s - %d - %s' % - (num_sections, num_rendered_sections, course_id, chapter_it, chapters[chapter_it]['chapter_name'])) + (num_sections, num_rendered_sections, course_id, chapter_it, chapters[chapter_it]['chapter_name'])) #logger.debug(msg) assert num_sections == num_rendered_sections, msg @@ -112,7 +112,7 @@ def browse_course(course_id): num_rendered_tabs = 0 msg = ('%d tabs expected, %d tabs found, %s - %d - %s' % - (num_tabs, num_rendered_tabs, course_id, section_it, sections[section_it]['section_name'])) + (num_tabs, num_rendered_tabs, course_id, section_it, sections[section_it]['section_name'])) #logger.debug(msg) # Save the HTML to a file for later comparison @@ -137,7 +137,7 @@ def browse_course(course_id): rendered_items = world.browser.find_by_css('div#seq_content > section > ol > li > section') num_rendered_items = len(rendered_items) msg = ('%d items expected, %d items found, %s - %d - %s - tab %d' % - (tab_children, num_rendered_items, course_id, section_it, sections[section_it]['section_name'], tab_it)) + (tab_children, num_rendered_items, course_id, section_it, sections[section_it]['section_name'], tab_it)) #logger.debug(msg) assert tab_children == num_rendered_items, msg From 192b99133461ec107a0af4240d54d24b45872746 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Wed, 20 Mar 2013 11:52:15 -0400 Subject: [PATCH 088/436] Pylint for lettuce factory refactor. --- common/djangoapps/terrain/browser.py | 14 +++++++++---- common/djangoapps/terrain/factories.py | 27 +++++++++++++++----------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 6394959532..c8cc0c9e4b 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -1,7 +1,6 @@ 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 @@ -16,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') @@ -24,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 88e29a18ef..d7a1de6780 100644 --- a/common/djangoapps/terrain/factories.py +++ b/common/djangoapps/terrain/factories.py @@ -1,12 +1,17 @@ -from student.tests.factories import (UserFactory, UserProfileFactory, - RegistrationFactory, GroupFactory, - CourseEnrollmentAllowed, - CourseFactory, ItemFactory) +''' +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. + +TODO: move the course and item factories out of student and into +xmodule/modulestore +''' +import student.tests.factories as sf from lettuce import world @world.absorb -class UserFactory(UserFactory): +class UserFactory(sf.UserFactory): """ User account for lms / cms """ @@ -14,7 +19,7 @@ class UserFactory(UserFactory): @world.absorb -class UserProfileFactory(UserProfileFactory): +class UserProfileFactory(sf.UserProfileFactory): """ Demographics etc for the User """ @@ -22,7 +27,7 @@ class UserProfileFactory(UserProfileFactory): @world.absorb -class RegistrationFactory(RegistrationFactory): +class RegistrationFactory(sf.RegistrationFactory): """ Activation key for registering the user account """ @@ -30,7 +35,7 @@ class RegistrationFactory(RegistrationFactory): @world.absorb -class GroupFactory(GroupFactory): +class GroupFactory(sf.GroupFactory): """ Groups for user permissions for courses """ @@ -38,7 +43,7 @@ class GroupFactory(GroupFactory): @world.absorb -class CourseEnrollmentAllowedFactory(CourseEnrollmentAllowed): +class CourseEnrollmentAllowedFactory(sf.CourseEnrollmentAllowed): """ Users allowed to enroll in the course outside of the usual window """ @@ -46,7 +51,7 @@ class CourseEnrollmentAllowedFactory(CourseEnrollmentAllowed): @world.absorb -class CourseFactory(CourseFactory): +class CourseFactory(sf.CourseFactory): """ Courseware courses """ @@ -54,7 +59,7 @@ class CourseFactory(CourseFactory): @world.absorb -class ItemFactory(ItemFactory): +class ItemFactory(sf.ItemFactory): """ Everything included inside a course """ From 5eba299dca0d3d3904d3b9e8c6295acc59a656cd Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Wed, 20 Mar 2013 12:10:18 -0400 Subject: [PATCH 089/436] Move course and item factories to xmodule.modulestore.tests --- common/djangoapps/student/tests/factories.py | 146 ------------------ common/djangoapps/terrain/factories.py | 8 +- .../xmodule/modulestore/tests/factories.py | 41 ++++- 3 files changed, 36 insertions(+), 159 deletions(-) diff --git a/common/djangoapps/student/tests/factories.py b/common/djangoapps/student/tests/factories.py index 1f2378a8c9..f74188725a 100644 --- a/common/djangoapps/student/tests/factories.py +++ b/common/djangoapps/student/tests/factories.py @@ -3,12 +3,7 @@ from student.models import (User, UserProfile, Registration, from django.contrib.auth.models import Group from datetime import datetime from factory import Factory, SubFactory -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 class GroupFactory(Factory): @@ -62,144 +57,3 @@ class CourseEnrollmentAllowedFactory(Factory): email = 'test@edx.org' course_id = 'edX/test/2012_Fall' - - -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): - """ - Factory for XModule courses. - """ - - 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): - """ - Factory for XModule items. - """ - - ABSTRACT_FACTORY = True - _creation_function = (XMODULE_ITEM_CREATION,) - - @classmethod - def _create(cls, target_class, *args, **kwargs): - """ - 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) - - # 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) - - # 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 - - # 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()]) - - return new_item - - -class Item: - pass - - -class ItemFactory(XModuleItemFactory): - FACTORY_FOR = Item - - parent_location = 'i4x://MITx/999/course/Robot_Super_Course' - template = 'i4x://edx/templates/chapter/Empty' - display_name = 'Section One' diff --git a/common/djangoapps/terrain/factories.py b/common/djangoapps/terrain/factories.py index d7a1de6780..768c51b25e 100644 --- a/common/djangoapps/terrain/factories.py +++ b/common/djangoapps/terrain/factories.py @@ -2,11 +2,9 @@ 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. - -TODO: move the course and item factories out of student and into -xmodule/modulestore ''' import student.tests.factories as sf +import xmodule.modulestore.tests.factories as xf from lettuce import world @@ -51,7 +49,7 @@ class CourseEnrollmentAllowedFactory(sf.CourseEnrollmentAllowed): @world.absorb -class CourseFactory(sf.CourseFactory): +class CourseFactory(xf.CourseFactory): """ Courseware courses """ @@ -59,7 +57,7 @@ class CourseFactory(sf.CourseFactory): @world.absorb -class ItemFactory(sf.ItemFactory): +class ItemFactory(xf.ItemFactory): """ Everything included inside a course """ 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()]) From 36443163f6f291f34d1715ccb20e660346a25ca9 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 20 Mar 2013 12:59:34 -0400 Subject: [PATCH 090/436] Force instantiation of the user before querying Without accessing .pk, it's possible for the user object to still be a SimpleLazyObject, which breaks Django's query engine http://stackoverflow.com/questions/11875737/django-filtering-drafts-by-user-causes-error --- lms/djangoapps/courseware/model_data.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lms/djangoapps/courseware/model_data.py b/lms/djangoapps/courseware/model_data.py index 35deda5d6b..7b0c7cd74e 100644 --- a/lms/djangoapps/courseware/model_data.py +++ b/lms/djangoapps/courseware/model_data.py @@ -121,7 +121,7 @@ class ModelDataCache(object): 'module_state_key__in', (descriptor.location.url() for descriptor in self.descriptors), course_id=self.course_id, - student=self.user, + student=self.user.pk, ) elif scope == Scope.content: return self._chunked_query( @@ -145,13 +145,13 @@ class ModelDataCache(object): XModuleStudentPrefsField, 'module_type__in', set(descriptor.location.category for descriptor in self.descriptors), - student=self.user, + student=self.user.pk, field_name__in=set(field.name for field in fields), ) elif scope == Scope.student_info: return self._query( XModuleStudentInfoField, - student=self.user, + student=self.user.pk, field_name__in=set(field.name for field in fields), ) else: @@ -214,7 +214,7 @@ class ModelDataCache(object): if key.scope == Scope.student_state: field_object, _ = StudentModule.objects.get_or_create( course_id=self.course_id, - student=self.user, + student=self.user.pk, module_type=key.block_scope_id.category, module_state_key=key.block_scope_id.url(), defaults={'state': json.dumps({})}, @@ -233,12 +233,12 @@ class ModelDataCache(object): field_object, _= XModuleStudentPrefsField.objects.get_or_create( field_name=key.field_name, module_type=key.block_scope_id, - student=self.user, + student=self.user.pk, ) elif key.scope == Scope.student_info: field_object, _ = XModuleStudentInfoField.objects.get_or_create( field_name=key.field_name, - student=self.user, + student=self.user.pk, ) cache_key = self._cache_key_from_kvs_key(key) From cbed66280a12f86827f9288bf2b4d92d3935cb57 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 20 Mar 2013 13:20:28 -0400 Subject: [PATCH 091/436] Fix pep8 and pylint errors --- .pylintrc | 3 ++- lms/djangoapps/courseware/model_data.py | 32 ++++++++++++++++++++++--- 2 files changed, 31 insertions(+), 4 deletions(-) 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/lms/djangoapps/courseware/model_data.py b/lms/djangoapps/courseware/model_data.py index 7b0c7cd74e..cb24501aef 100644 --- a/lms/djangoapps/courseware/model_data.py +++ b/lms/djangoapps/courseware/model_data.py @@ -1,3 +1,7 @@ +""" +Classes to provide the LMS runtime data storage to XBlocks +""" + import json from collections import namedtuple, defaultdict from itertools import chain @@ -14,10 +18,16 @@ from xblock.core import Scope class InvalidWriteError(Exception): - pass + """ + Raised to indicate that writing to a particular key + in the KeyValueStore is disabled + """ def chunks(items, chunk_size): + """ + Yields the values from items in chunks of size chunk_size + """ items = list(items) return (items[i:i + chunk_size] for i in xrange(0, len(items), chunk_size)) @@ -67,6 +77,15 @@ class ModelDataCache(object): """ def get_child_descriptors(descriptor, depth, descriptor_filter): + """ + Return a list of all child descriptors down to the specified depth + that match the descriptor filter. Includes `descriptor` + + descriptor: The parent to search inside + depth: The number of levels to descend, or None for infinite depth + descriptor_filter(descriptor): A function that returns True + if descriptor should be included in the results + """ if descriptor_filter(descriptor): descriptors = [descriptor] else: @@ -168,6 +187,9 @@ class ModelDataCache(object): return scope_map def _cache_key_from_kvs_key(self, key): + """ + Return the key used in the ModelDataCache for the specified KeyValueStore key + """ if key.scope == Scope.student_state: return (key.scope, key.block_scope_id.url()) elif key.scope == Scope.content: @@ -180,6 +202,10 @@ class ModelDataCache(object): return (key.scope, key.field_name) def _cache_key_from_field_object(self, scope, field_object): + """ + Return the key used in the ModelDataCache for the specified scope and + field + """ if scope == Scope.student_state: return (scope, field_object.module_state_key) elif scope == Scope.content: @@ -230,7 +256,7 @@ class ModelDataCache(object): usage_id='%s-%s' % (self.course_id, key.block_scope_id.url()), ) elif key.scope == Scope.student_preferences: - field_object, _= XModuleStudentPrefsField.objects.get_or_create( + field_object, _ = XModuleStudentPrefsField.objects.get_or_create( field_name=key.field_name, module_type=key.block_scope_id, student=self.user.pk, @@ -276,6 +302,7 @@ class LmsKeyValueStore(KeyValueStore): Scope.student_info, Scope.children, ) + def __init__(self, descriptor_model_data, model_data_cache): self._descriptor_model_data = descriptor_model_data self._model_data_cache = model_data_cache @@ -357,4 +384,3 @@ class LmsKeyValueStore(KeyValueStore): LmsUsage = namedtuple('LmsUsage', 'id, def_id') - From 154a441033900fc2f1469a406213c33aebeea41e Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 20 Mar 2013 13:24:25 -0400 Subject: [PATCH 092/436] Only use .pk for queries, and not on inserts --- lms/djangoapps/courseware/model_data.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lms/djangoapps/courseware/model_data.py b/lms/djangoapps/courseware/model_data.py index cb24501aef..b725f64308 100644 --- a/lms/djangoapps/courseware/model_data.py +++ b/lms/djangoapps/courseware/model_data.py @@ -240,7 +240,7 @@ class ModelDataCache(object): if key.scope == Scope.student_state: field_object, _ = StudentModule.objects.get_or_create( course_id=self.course_id, - student=self.user.pk, + student=self.user, module_type=key.block_scope_id.category, module_state_key=key.block_scope_id.url(), defaults={'state': json.dumps({})}, @@ -259,12 +259,12 @@ class ModelDataCache(object): field_object, _ = XModuleStudentPrefsField.objects.get_or_create( field_name=key.field_name, module_type=key.block_scope_id, - student=self.user.pk, + student=self.user, ) elif key.scope == Scope.student_info: field_object, _ = XModuleStudentInfoField.objects.get_or_create( field_name=key.field_name, - student=self.user.pk, + student=self.user, ) cache_key = self._cache_key_from_kvs_key(key) From 83665515d8330e8709bd4893429539975a8c0049 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Wed, 20 Mar 2013 13:45:01 -0400 Subject: [PATCH 093/436] Remove director of engineering job posting per request from Tess --- lms/templates/static_templates/jobs.html | 35 ------------------------ 1 file changed, 35 deletions(-) diff --git a/lms/templates/static_templates/jobs.html b/lms/templates/static_templates/jobs.html index 988a6b18d0..e33ff62e9a 100644 --- a/lms/templates/static_templates/jobs.html +++ b/lms/templates/static_templates/jobs.html @@ -334,40 +334,6 @@ -
    -
    -

    DIRECTOR ENGINEERING, OPEN SOURCE COMMUNITY MANAGER

    -

    In edX courses, students make (and break) electronic circuits, they manipulate molecules on the fly and they do it all at once, in their tens of thousands. We have great Professors and great Universities. But we can’t possibly keep up with all the great ideas out there, so we’re making our platform open source, to turn up the volume on great education. To do that well, we’ll need a Director of Engineering who can lead our Open Source Community efforts.

    -

    Responsibilities:

    -
      -
    • Define and implement software design standards that make the open source community most welcome and productive.
    • -
    • Work with others to establish the governance standards for the edX Open Source Platform, establish the infrastructure, and manage the team to deliver releases and leverage our University partners and stakeholders to
    • make the edX platform the world’s best learning platform. -
    • Help the organization recognize the benefits and limitations inherent in open source solutions.
    • -
    • Establish best practices and key tool usage, especially those based on industry standards.
    • -
    • Provide visibility for the leadership team into the concerns and challenges faced by the open source community.
    • -
    • Foster a thriving community by providing the communication, documentation and feedback that they need to be enthusiastic.
    • -
    • Maximize the good code design coming from the open source community.
    • -
    • Provide the wit and firmness that the community needs to channel their energy productively.
    • -
    • Tactfully balance the internal needs of the organization to pursue new opportunities with the community’s need to participate in the platform’s evolution.
    • -
    • Shorten lines of communication and build trust across entire team
    • -
    -

    Qualifications:

    -
      - -
    • Bachelors, preferably Masters in Computer Science
    • -
    • Solid communication skills, especially written
    • -
    • Committed to Agile practice, Scrum and Kanban
    • -
    • Charm and humor
    • -
    • Deep familiarity with Open Source, participant and contributor
    • -
    • Python, Django, Javascript
    • -
    • Commitment to support your technical recommendations, both within and beyond the organization.
    • -
    - -

    If you are interested in this position, please send an email to jobs@edx.org.

    -
    -
    - -

    SOFTWARE ENGINEER

    @@ -413,7 +379,6 @@ Project Manager (PMO) Director of Product Management Content Engineer - Director Engineering, Open Source Community Manager Software Engineer

    How to Apply

    From af1af8c6d1f57fb45435ef037253531ce8f5e551 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Wed, 20 Mar 2013 14:08:15 -0400 Subject: [PATCH 094/436] Address code review feedback: - improve docstrings - only pass in the state for a particular input and not the whole dictionary - refactor some common code - minor syntax cleanup --- common/lib/capa/capa/capa_problem.py | 43 +++--- common/lib/capa/capa/inputtypes.py | 136 ++++++++++-------- common/lib/capa/capa/responsetypes.py | 16 +-- common/lib/capa/capa/tests/test_inputtypes.py | 2 +- common/lib/xmodule/xmodule/capa_module.py | 20 ++- .../xmodule/tests/test_combined_open_ended.py | 2 +- lms/djangoapps/courseware/module_render.py | 2 +- 7 files changed, 126 insertions(+), 95 deletions(-) diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index f1fea4d8e3..27f1066030 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -91,8 +91,12 @@ class LoncapaProblem(object): - problem_text (string): xml defining the problem - id (string): identifier for this problem; often a filename (no spaces) - - state (dict): student state - - seed (int): random number generator seed (int) + - state (dict): containing the following keys: + - 'seed' - (int) random number generator seed + - 'student_answers' - (dict) maps input id to the stored answer for that input + - 'correct_map' (CorrectMap) a map of each input to their 'correctness' + - 'done' - (bool) indicates whether or not this problem is considered done + - 'input_state' - (dict) maps input_id to a dictionary that holds the state for that input - system (ModuleSystem): ModuleSystem instance which provides OS, rendering, and user context @@ -104,27 +108,16 @@ class LoncapaProblem(object): self.system = system if self.system is None: raise Exception() - self.seed = seed - self.input_state = None - if state: - if 'seed' in state: - self.seed = state['seed'] - if 'student_answers' in state: - self.student_answers = state['student_answers'] - if 'correct_map' in state: - self.correct_map.set_dict(state['correct_map']) - if 'done' in state: - self.done = state['done'] - if 'input_state' in state: - self.input_state = state['input_state'] + state = state if state else {} + self.seed = seed if seed else state.get('seed', struct.unpack('i', os.urandom(4))[0]) + self.student_answers = state.get('student_answers', {}) + if 'correct_map' in state: + self.correct_map.set_dict(state['correct_map']) + self.done = state.get('done', False) + self.input_state = state.get('input_state', {}) - # TODO: Does this deplete the Linux entropy pool? Is this fast enough? - if not self.seed: - self.seed = struct.unpack('i', os.urandom(4))[0] - if not self.input_state: - self.input_state = {} # Convert startouttext and endouttext to proper problem_text = re.sub("startouttext\s*/", "text", problem_text) @@ -240,13 +233,14 @@ class LoncapaProblem(object): def ungraded_response(self, xqueue_msg, queuekey): ''' - Handle any responses from the xqueue that are not related to grading + Handle any responses from the xqueue that do not contain grades + Will try to pass the queue message to all inputtypes that can handle ungraded responses Does not return any value ''' # check against each inputtype for the_input in self.inputs.values(): - # if the input type has an xqueue_response function, pass in the values + # if the input type has an ungraded function, pass in the values if hasattr(the_input, 'ungraded_response'): the_input.ungraded_response(xqueue_msg, queuekey) @@ -542,11 +536,14 @@ class LoncapaProblem(object): if self.student_answers and problemid in self.student_answers: value = self.student_answers[problemid] + if input_id not in self.input_state: + self.input_state[input_id] = {} + # do the rendering state = {'value': value, 'status': status, 'id': input_id, - 'input_state': self.input_state, + 'input_state': self.input_state[input_id], 'feedback': {'message': msg, 'hint': hint, 'hintmode': hintmode, }} diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 0208f32503..d5268fed89 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -161,7 +161,7 @@ class InputTypeBase(object): self.msg = feedback.get('message', '') self.hint = feedback.get('hint', '') self.hintmode = feedback.get('hintmode', None) - self.input_state_dict = state.get('input_state', {}) + self.input_state = state.get('input_state', {}) # put hint above msg if it should be displayed if self.hintmode == 'always': @@ -591,14 +591,14 @@ class CodeInput(InputTypeBase): Attribute('tabsize', 4, transform=int), ] - def setup(self): + def setup_code_response_rendering(self): """ Implement special logic: handle queueing state, and default input. """ # if no student input yet, then use the default input given by the # problem - if not self.value: - self.value = self.xml.text + if not self.value and self.xml.text: + self.value = self.xml.text.strip() # Check if problem has been queued self.queue_len = 0 @@ -609,6 +609,11 @@ class CodeInput(InputTypeBase): self.queue_len = self.msg self.msg = self.submitted_msg + + def setup(self): + ''' setup this input type ''' + self.setup_code_response_rendering() + def _extra_context(self): """Defined queue_len, add it """ return {'queue_len': self.queue_len, } @@ -623,8 +628,10 @@ class MatlabInput(CodeInput): ''' InputType for handling Matlab code input + TODO: API_KEY will go away once we have a way to specify it per-course Example: + Initial Text %api_key=API_KEY @@ -633,51 +640,56 @@ class MatlabInput(CodeInput): template = "matlabinput.html" tags = ['matlabinput'] - # pulled out for testing - submitted_msg = ("Submitted. As soon as your submission is" - " graded, this message will be replaced with the grader's feedback.") + plot_submitted_msg = ("Submitted. As soon as a response is returned, " + "this message will be replaced by that feedback.") def setup(self): ''' Handle matlab-specific parsing ''' - # if we don't have state for this input type yet, make one - if self.id not in self.input_state_dict: - self.input_state_dict[self.id] = {} + self.setup_code_response_rendering() - self.input_state = self.input_state_dict[self.id] xml = self.xml self.plot_payload = xml.findtext('./plot_payload') - # if no student input yet, then use the default input given by the - # problem - if not self.value: - self.value = self.xml.text # Check if problem has been queued - self.queue_len = 0 self.queuename = 'matlab' - # Flag indicating that the problem has been queued, 'msg' is length of self.queue_msg = '' if 'queue_msg' in self.input_state and self.status in ['incomplete', 'unsubmitted']: self.queue_msg = self.input_state['queue_msg'] if 'queued' in self.input_state and self.input_state['queuestate'] is not None: self.status = 'queued' self.queue_len = 1 - # queue - if self.status == 'incomplete': - self.status = 'queued' - self.queue_len = self.msg - self.msg = self.submitted_msg - + self.msg = self.plot_submitted_msg def handle_ajax(self, dispatch, get): - ''' Handle AJAX calls directed to this input''' + ''' + Handle AJAX calls directed to this input + + Args: + - dispatch (str) - indicates how we want this ajax call to be handled + - get (dict) - dictionary of key-value pairs that contain useful data + Returns: + + ''' + if dispatch == 'plot': return self._plot_data(get) + return {} def ungraded_response(self, queue_msg, queuekey): - ''' Handle any XQueue responses that have to be saved and rendered ''' + ''' + Handle the response from the XQueue + Stores the response in the input_state so it can be rendered later + + Args: + - queue_msg (str) - message returned from the queue. The message to be rendered + - queuekey (str) - a key passed to the queue. Will be matched up to verify that this is the response we're waiting for + + Returns: + nothing + ''' # check the queuekey against the saved queuekey if('queuestate' in self.input_state and self.input_state['queuestate'] == 'queued' and self.input_state['queuekey'] == queuekey): @@ -697,9 +709,11 @@ class MatlabInput(CodeInput): def _parse_data(self, queue_msg): ''' - takes a queue_msg returned from the queue and parses it and returns - whatever is stored in msg - returns string msg + Parses the message out of the queue message + Args: + queue_msg (str) - a JSON encoded string + Returns: + returns the value for the the key 'msg' in queue_msg ''' try: result = json.loads(queue_msg) @@ -712,42 +726,50 @@ class MatlabInput(CodeInput): def _plot_data(self, get): - ''' send data via xqueue to the mathworks backend''' + ''' + AJAX handler for the plot button + Args: + get (dict) - should have key 'submission' which contains the student submission + Returns: + dict - 'success' - whether or not we successfully queued this submission + - 'message' - message to be rendered in case of error + ''' # only send data if xqueue exists - if self.system.xqueue is not None: - # pull relevant info out of get - response = get['submission'] + if self.system.xqueue is None: + return {'success': False, 'message': 'Cannot connect to the queue'} - # construct xqueue headers - qinterface = self.system.xqueue['interface'] - qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat) - callback_url = self.system.xqueue['construct_callback']('ungraded_response') - anonymous_student_id = self.system.anonymous_student_id - queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime + - anonymous_student_id + - self.id) - xheader = xqueue_interface.make_xheader( - lms_callback_url = callback_url, - lms_key = queuekey, - queue_name = self.queuename) + # pull relevant info out of get + response = get['submission'] - # save the input state - self.input_state['queuekey'] = queuekey - self.input_state['queuestate'] = 'queued' + # construct xqueue headers + qinterface = self.system.xqueue['interface'] + qtime = datetime.strftime(datetime.utcnow(), xqueue_interface.dateformat) + callback_url = self.system.xqueue['construct_callback']('ungraded_response') + anonymous_student_id = self.system.anonymous_student_id + queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime + + anonymous_student_id + + self.id) + xheader = xqueue_interface.make_xheader( + lms_callback_url = callback_url, + lms_key = queuekey, + queue_name = self.queuename) + + # save the input state + self.input_state['queuekey'] = queuekey + self.input_state['queuestate'] = 'queued' - # construct xqueue body - student_info = {'anonymous_student_id': anonymous_student_id, - 'submission_time': qtime} - contents = {'grader_payload': self.plot_payload, - 'student_info': json.dumps(student_info), - 'student_response': response} + # construct xqueue body + student_info = {'anonymous_student_id': anonymous_student_id, + 'submission_time': qtime} + contents = {'grader_payload': self.plot_payload, + 'student_info': json.dumps(student_info), + 'student_response': response} - (error, msg) = qinterface.send_to_queue(header=xheader, - body = json.dumps(contents)) + (error, msg) = qinterface.send_to_queue(header=xheader, + body = json.dumps(contents)) - return {'success': error == 0, 'message': msg} - return {'success': False, 'message': 'Cannot connect to the queue'} + return {'success': error == 0, 'message': msg} registry.register(MatlabInput) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index bb202e6d6e..8ab716735c 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1147,10 +1147,10 @@ def sympy_check2(): correct = [] messages = [] for input_dict in input_list: - correct.append('correct' if input_dict[ - 'ok'] else 'incorrect') - msg = self.clean_message_html(input_dict[ - 'msg']) if 'msg' in input_dict else None + correct.append('correct' + if input_dict['ok'] else 'incorrect') + msg = (self.clean_message_html(input_dict['msg']) + if 'msg' in input_dict else None) messages.append(msg) # Otherwise, we do not recognize the dictionary @@ -1164,8 +1164,8 @@ def sympy_check2(): # indicating whether all inputs should be marked # correct or incorrect else: - correct = ['correct'] * len( - idset) if ret else ['incorrect'] * len(idset) + n = len(idset) + correct = ['correct'] * n if ret else ['incorrect'] * n # build map giving "correct"ness of the answer(s) correct_map = CorrectMap() @@ -1174,8 +1174,8 @@ def sympy_check2(): correct_map.set_overall_message(overall_message) for k in range(len(idset)): - npoints = self.maxpoints[idset[ - k]] if correct[k] == 'correct' else 0 + npoints = (self.maxpoints[idset[k]] + if correct[k] == 'correct' else 0) correct_map.set(idset[k], correct[k], msg=messages[k], npoints=npoints) return correct_map diff --git a/common/lib/capa/capa/tests/test_inputtypes.py b/common/lib/capa/capa/tests/test_inputtypes.py index b9da9df03f..250cedd549 100644 --- a/common/lib/capa/capa/tests/test_inputtypes.py +++ b/common/lib/capa/capa/tests/test_inputtypes.py @@ -357,7 +357,7 @@ class MatlabTest(unittest.TestCase): def test_rendering_with_state(self): state = {'value': 'print "good evening"', 'status': 'incomplete', - 'input_state': {'prob_1_2': {'queue_msg': 'message'}}, + 'input_state': {'queue_msg': 'message'}, 'feedback': {'message': '3'}, } elt = etree.fromstring(self.xml) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 6ce8d3a805..da8b5b4f96 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -543,8 +543,16 @@ class CapaModule(CapaFields, XModule): def handle_ungraded_response(self, get): ''' - Delivers a response to the capa problem where the expectation where this response does - not have relevant grading information + Delivers a response from the XQueue to the capa problem + + The score of the problem will not be updated + + Args: + - get (dict) must contain keys: + queuekey - a key specific to this response + xqueue_body - the body of the response + Returns: + empty dictionary No ajax return is needed, so an empty dict is returned ''' @@ -557,8 +565,12 @@ class CapaModule(CapaFields, XModule): def handle_input_ajax(self, get): ''' - Passes information down to the capa problem so that it can handle its own ajax calls - Returns the response from the capa problem + Handle ajax calls meant for a particular input in the problem + + Args: + - get (dict) - data that should be passed to the input + Returns: + - dict containing the response from the input ''' response = self.lcp.handle_input_ajax(get) # save any state changes that may occur diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py index aa8a077cc1..55c31ded58 100644 --- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py +++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py @@ -183,7 +183,7 @@ class OpenEndedModuleTest(unittest.TestCase): self.test_system.location = self.location self.mock_xqueue = MagicMock() self.mock_xqueue.send_to_queue.return_value = (None, "Message") - def constructed_callback(dispatch = "score_update"): + def constructed_callback(dispatch="score_update"): return dispatch self.test_system.xqueue = {'interface': self.mock_xqueue, 'construct_callback': constructed_callback, 'default_queuename': 'testqueue', diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 0954f8d28c..973940d784 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -182,7 +182,7 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours proto=request.META.get('HTTP_X_FORWARDED_PROTO', 'https' if request.is_secure() else 'http') ) - def make_xqueue_callback(dispatch = 'score_update'): + def make_xqueue_callback(dispatch='score_update'): # Fully qualified callback URL for external queueing system xqueue_callback_url = '{proto}://{host}'.format( host=request.get_host(), From 5dbb153abe607df7f5d7376fe4af973c807956d3 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 20 Mar 2013 14:32:18 -0400 Subject: [PATCH 095/436] ooops. didn't clean up the merge conflicts --- common/lib/xmodule/xmodule/modulestore/mongo.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py index cc4c06d23a..d2fe524a0e 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -622,11 +622,7 @@ class MongoModuleStore(ModuleStoreBase): self._update_single_item(location, {'metadata': metadata}) # recompute (and update) the metadata inheritance tree which is cached -<<<<<<< HEAD self.refresh_cached_metadata_inheritance_tree(loc) -======= - self.get_cached_metadata_inheritance_tree(loc, force_refresh = True) ->>>>>>> fbd409c914c2dc005fd6b46af6daaee262205e0e def delete_item(self, location): """ @@ -649,12 +645,7 @@ class MongoModuleStore(ModuleStoreBase): # from overriding our default value set in the init method. safe=self.collection.safe) # recompute (and update) the metadata inheritance tree which is cached -<<<<<<< HEAD self.refresh_cached_metadata_inheritance_tree(Location(location)) -======= - self.get_cached_metadata_inheritance_tree(Location(location), force_refresh = True) ->>>>>>> fbd409c914c2dc005fd6b46af6daaee262205e0e - def get_parent_locations(self, location, course_id): '''Find all locations that are the parents of this location in this From c5385ff42c77c184b4927f5b3c8859f3b56ed7bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Wed, 20 Mar 2013 14:46:08 -0400 Subject: [PATCH 096/436] Update Berkeley logos LMS Lighthouse [#260] --- lms/static/images/BerkeleyX-on-edx-logo.png | Bin 5011 -> 3493 bytes .../images/university/berkeley/berkeley.png | Bin 12172 -> 11091 bytes .../university/berkeley/berkeley_bw.png | Bin 8330 -> 4912 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/lms/static/images/BerkeleyX-on-edx-logo.png b/lms/static/images/BerkeleyX-on-edx-logo.png index 6c5a828503a1906fa5c2bf536d8f61478a9fd9a0..cb765ce2fa4a683eff09bbe8ead0892e202c6088 100644 GIT binary patch delta 3454 zcmV-^4T18LC#4&ZNPi7SNklNP{ z04h){Xi%UPtE5O%)GAW?RSJbviZ!+nsGnd_0Rb(bLa;@!JOlC!n&k2@kq`(eA><~x zJ?Gwk$z<5QkC=qux99s#?%v(m-PxIc{`t?Dy|LKsc43-nw|}Ggm`z}s30~7oGr?<` zX(o70`;MERw&5Q$BO~`*t-WF{z5YIlLp*qBtx7|dgx8NoFJNatzAo6#}2VnnJ^6}(l zzh0*l2`FV|hL za7h|{Jj2^|Axh@tv0EP%R@U!ZR_>aj zg&W!`O@DmUpZ%-z^k($Yo;-y79LGPyv(xP^G;S-DXFHcVJ7p(*?xg*r>2u1RO>CSo z60w%6&u5F^-%F>)IizZG^p^_oXDPQh#afrjT~-6O+E}y{)U9oxHuaU{gQ-XB9HMNPMc1L-=%;#Pk%VGSUK|kC$4`v?P&Ao((dIH zq@5&S?x#-+&P$YzbE-B;1FomwwY!+UIU2uCo=kqx1=x6G@u>WYJXhyBoSw760{~2$ z8`+=-Ue43{BWq&Ewp}U5x$7Rd=P}{TJt!Y=)1Kt$asukZw6#~7!P}t?x+oM1M>f11iu16PQfVT1rc6B8-OWgD9&Rh z;1#b?(oA620oFG156S<8y8T)L?o0SSOd2?2Aa}-Ks@&`*yAcYo`=f3pVAaJum13Yg z1ORfnaUEuZFHv4gE#~2w`=Tva-+w*sVz#NNEf*}9OCO^Fdd`Q=O-)TbMEh4_eD|Bdte zqI?^}J0?eeLjWF){s_X}Zb~WcpFilG?g@AqP?J%%8FUawd-tQF>j4T`Y=6gM`q?1? zm+iQ4E^vH?3YCF%FN4J{0=yFe?q87KN&beY&hPq4j98(=k`Z9|#iDw_AqgEhN6C<^ z2TQ&soU4k%q{i2iO^YzFdIPxH@f5VpqhQvd7^LqwraLEqFH6#*N*Wa9sGFz2)c~46 zfXt=uhXB+BTqgm~n~Pb7kAKa?F(cN!7bKLgf@}h@ z435=`c4okx#&hx1p6Wt2^n?`nSkzn!_s4zrN>XW(G}CC);V7MJGYCHs&i6z-0q$T7 z5N29U=I~(YMaN>LeLHD;CbKd6FW1ebr%2m8z>*Z;I;Y_5be2RDvi;9$16PzEJH`Dx zS^=)ZXkRgAqhdGrUw@0ZLA6&1xTQuXdBVI#Ip4+5ZchV%tH}F66DOezM~kh|c`YF~ zu7lnFj7}@kxz`!xUU>#-G8w$>z@YD;ZkqK*y)Ht^1Y#?5PzXxsY z%I=l68MKp#Wep3LvTf{S~;al#Os1mHqq;8nnhk^J)uT zSC)p341-i)?f4NuT?QRw1t{{N`AR&vu8#-D?9!B;2$tu_c9T+GTOYp&FM-a?NBzp& z)TLZvRDowGgnw2qkiHw`oL2z6cLRKx*xwJ=_U6T(SO*}g8CGNOJle z%uyg*FP893a8_q}PQrC;UOamy7&SqX5xpe#PQd3OgYVv^L*35Ac2Q*)?iY*v41k-U zlU-S-$czLB0XTENbqZdgiSI&=Oq<0lti&ssRQ@p+*Oi(42xd@24;egY zZ^+PljL;4=m@g`okI z31HrE4$73{gO%@TRF>Q4vLZE9`$S5UaT}hc;B22^;5co`WB{21m`;ZiMSC&Zk&C~% z?7}7Oh4RMX6`vqKWMX2Z-RB&^9p`_Uj54gxA(dLKi!-c8g#LHHqow5twqV1e*x3;2vx*#lk`t1W;2A z(m@>%IF%{^nB4>Lsv2lF^1{q=&miJYhqhgbed8blW{H~bENg?5S9-p}{k1j-l(g%! zh>DkDf4YulZ3YQ*MkjDf0<-ZT+a?-`5z8JVP_B?PIcHKnr+w>?F{y<1

    12pMTv~ z#QH=Z&h;w@35e_5Ns}7{)-{Mf>{7~5Yrl%Z3g06|<-S$tOE1J*{7Vjspx{t@ctj_} z4?PH&1q5I#%8x{>l!UC1mTG;U%JCY(c0UCatx?eqN)Xmrnf@aXBs_)A;}xwJl)POc z7^W|BpgW*xF5x~LV4Xmkqy@B~rhk8a067oS7K0Y0#!9N+U7pPoYbf#Hib$>RFVI6c( zghyyV9SJF-fp?zJ|0P8i1bJgE+JDUPj~Nn->7yldo(+I*h3_XYRfme|7hAR5IDAFr91xy9$7a8F2tW&AM`# zaIV|)qR10y08Kkz0U))d>VE|oG(lkBCb-uYv`-8M$4Dh0&4b=vro-x*asD+014Z{m zrd#m?Ozmet#I)SEyejsHn^Snuk3T#ut`IZb2sh;~P^SfIKLfg91YUc5-^3bt@#1Gd zGI+Joqxa50C_o$prn(?J|Gk=M7g6(#H349@M}JnL6e+yoT3kCE z*9k>?oc18?H4n4a45I?K61MLCT zqNmbg{-eFRH5VkX9CxfV-qrc$|84Nx{LCh3n1CKSa)8+sb!X-|wHck!lOZ5D{^IInCn&7=%j2Ct`;gf&cHpgs|x;Hc1V4BIi grkU2z_J08e0N5P|spNyWv;Y7A07*qoM6N<$g1(BAVgLXD literal 5011 zcmaJ_c|26@`#v*vA&MwujIv}IgRzgumaVKIVul$zGsYN3LV7J_&z2TMA(dnq>uU*F zBTLFYvKt{}4PU+6_xH#9{hiM_=Xsv{b6?keJ(Hp3G3?agM_Zs)I%Ze7$nphVIpgS*T%ZJ zUk)N*ErPCCItO_=t6-oPH6ZGNsz(IgSRxt{=ry7Wa{=utyH2)QbK_P#n5IvF5 zKSkM?m_oF11S|w0BQNbN3x`7#6=mQG2t`G>6huxIE(eo6x)r723aSW2RXHWdUl;VK z8Uf>?YObUASFNKJ66!`I;#Fa=fPes*0C^c4!4(EqQBnEDAtxt&lp*a$_93DJrG5NF z{$|j@`Z*Ka@kDo=59Ajk+6m`RL_&|0{<#Hj{6Dllet#X)(SgAN(RdhKM)ud1{!TP8 z`TtYBz5mJfBbsCXtM^}t{Vd6NEX*A1hw~>mA2rTJ9ixzENWo11@Z6zIT#HD}N@+TJaFIxV>D*sO`>_`mkS8M-ItN#ui z`RCX4ALAY^{;@vR=g9AbBV%(6dLjUTlfgho%QA3y$)1JC^XYWm`uH&x1G=_TL*O=O zj$4^O187E|!b3~?0@TCa)Q;9y=(`Ypy^xt-ut8A<9;w3ztE-WBh z3kvh?>EAOB4yS5X=VrURV(y1!2K4O6C#waxERXIi2OJ*O1Y}N;Gk5b|nCyjIcycLO z+r@jDoi^J>Am+z1?BDc;xq|m-BCS9geQE%xpK{4cv z4>!!&086`J_70zaEU;R4RE8z%IM@w)hH{5Xs+M{o<24F)HFV^)z|H zCE8!ADda&XYOs}*$xD?9WGsE;m33KL9$^~H8%eYV-penp2>eD~Hn&DXYC~{^ihK%6%hp0s^F_(O9FRVXL^C)hXk|Lk81+BRE_{dHY4*jIxGt~(o1+BDNJ2UKR3oW z$mgvuQO{+2*4DGc<^9`8HaXFxF)c-*!#B{Es%?!A$Llmn{FpSV0?*JijAT5_DFG98hP zbJ3eg3aZoym=^CMU4=CZ7Mpk}3->rXtnAIMlz0Y+B|P3h*;Y_SzAu>QFK*;pkQ~6PPeiG^Go;7#Np%Ht$KdR#^}7mA{~kT%iZ+YRIsw zPMCsI%H3`Iis2KSy3L9npeV7$XPqsnw@4p0W6eP3rBjQv#h@X}B6|4;mJnrIrjVWj+W##gwtdpqxTh`u_I&Bh??UWX5SNG@p z?C{~6;-CQA8_(o=Ecpr=xB9M?ei#k&OEBM47?RH3`^LD3fO<+g5V(_Wo#)FE%7aD# zqVee0g5cNmdJ*i5d{dQkEQuq5(bwbG;Jxlmd0zgf*}H=6O9eiU+zYPbl~j;UYW>`! zcW_-JYdER#BYn?OgO#0UFl_#4y0c64IE4KEnBY#Wb;MxdO6fSB|H1bnz5LUc z6$Q59!8-c@+u%JhYHfPb7RTa-lF%$7KMEPk_U7ryEH;-X(R&Fk`;qsN`kN{aZWg1o zOJ*x9%)#j>Vl}t-$sf*+ppRfMMqZl0VCfzxGU3bvD#INm5 z>NoZDj_+#0FFIZ>mh_IL(A&<`?qs=csie|^Kn!k&Cub(avbIL1_2zIZjBko z+*g+D+6sOym(@1HaO~%8FX)g>NkAik?(S*I_3%eGCt+i(tMik)inR@%jdek)6xJdw z#OQCS(dufsOE-Zkl@pbEoateQ3f%X{rF>sW041hCdVeVJj9HWPl+_{mrA~GWN}1QI z#%dtWHkh$Np5a>w{a%ob@q9c$io5Nv-+yQ|&zE@|pTSuK>Q93{y48Jh-bO4*DQa^C zrO0?EEwXso-zm$Mlnolil zj(1LBvT5CK`ThF(4>92qjWgL<5-E?5nH@;edum#{;|yMXXxT`M z=B>#5f=W)4EATaHa2nm4fY#h!;t{xbRzr+QdOYusGon(t-9WGbzZ&VJotR`11pQE+If&~7@EgE8j;dU75uGsi1CYSR9HnEm)oI? zILB~JH8E81k(xdxr@x3**|n}d{{~5go6XK6b*Z2Q`@1)r7u}l_oReNdS~jB#=jOG& zQ$_d$GCyd*Oneg_+26r^UTDGMeZNGFR(v5=Y@5%W}!2yxSA&~}_r09h`y~SM!J=KujjI{~Hv@~}w zz4lnci_XIGXl%h|@M8ayC8hauD$E~Cq_snIip!_y?8Pwm8@zI7hsU(@f5dW0e$7p- zXC%viYuy==bfzD`3K1)Ua;XJ0}IB@XQWJe*BI0>7`sp z0gTH2UhqTt>3BqiaSIcN5ftPBMJq*5;jYX)Th2ev_U><0+$ zNa-)}mgV`ObCZu7S6go!Qwx$H3Ro8poE3*!pQe(6M`1TySQkgn!Xoc9AE(L{SY~D2 zU@RH(7w6AY{MiD@UEnT$f=L<}zJKe>`oeQ$g-KwZljP7iEKT11cU=f_7(9B#)UgC} zP-@b>Ep0FU0>65FJPx`7*Vc$RH`DdCM@Tkk+-A6?HBe_8shV>{lk?%pRaSQekTdRC*_ zhVBsFIIT^?fdTDII-zBmHr?R;9{iHvK-GEGQA;)~xiUS?hY`R_Qafwr&mrMp6Fq9% zT-qe|2cyjjFWam979+r+Xe%b%-+S3sJ5O!3E1&smHQP1G8GW^~VU5)&?1d8xQ10xP z5f^cCPBRb6^&i_f$4k8Y4*v8if^kt7ud5I!WWEWLs1|9fX;vw@;V6CFzq!ndUdx7i zmzG{>XCclu6o|F}$?1QxH|^(wc_$WD;aw##lzcdm5;pOf^>eJ#JE!BA0yt0|!?+Yh zTkvPTo+T)e7bC>cOXiicQDo z17)lhJy&zia!nS_ugp}MxN>UZZc}Kd4=P3ysh_eyT>Z07hJkKv5&{Yib%B8igjiLy z5VP0QsP?`r(6_7f4sSk75N2$iXwW&Wb}&B~!qqr59UFTqAL++ijjKXM7fgCP*odQ}i0gLVGPjlRR$pN_3H>GXL+d^X zomXIJFYjV6e=K+K+d$fStQBT2op6?Qm34xmv@!1|3(CDgxhhLA7PZd3zD=2H6jb9c zjTlbYXBH&GkjCv=Ot8bzWEEoucDwgmR_LUl>>xDo$?D+ZGS^0VIQe4z8-Zu>h2Uw~ z&-!}Fb}C#>v@kR5q2)!;XAdAH^1<^u13Bds>g)8IKfUfb)JWw)hLs)imk}fSo>eW_ z-bB>Jq=VNbyGC%(I9oC9W~IC~wMfV6gD!1vrhEgJ`dks7c;!d>wDrl_9;5SzFE^!m zKFFpph+&9^`KnjUpYh)2POAVc2>e}BPF;ihYE>Fjj zy`-L%Y$Pl{AB$|d@cq6{8ar7pai%_s!L(eG3t^f1t5C*PF(h&j5P=DN&B*_->OWPu z2{X()`jCw>aYC)l86>dEcJ|zKDx@eB^cWpBFDUu)m&JP?IGj3svoAlHKNPa?k)j|w zZ;jb7ihJ>}6PjAM-=7=5cBrmDs~@NOz`u8EKvc|Q+9`O1%mu2AP7<;zFGqLJwI>-j o9d`+PPmU`qQ@~qXJ>*3Jhb_ZKyuWX1{`yU3pnFB9=n^XYKV?e~-2eap diff --git a/lms/static/images/university/berkeley/berkeley.png b/lms/static/images/university/berkeley/berkeley.png index ca85266538ca132ebe566e3614993ae3f5516057..9fb7ffa6e7777755c5ef92fff6b13a8e29111f87 100644 GIT binary patch literal 11091 zcma)?Wl$Yq)24BEcXubayPV(>Jh;0%2M7|}-Q_?axCeI+8r)ri93*gXHhJHwt*>fp zxAxCWSIyJ&%#XQyru)94)m0VHQAkjrprFtJin5weP|(JI;|wH(f5+n;pT8$!PdPnL zEf;G~A2W9=C@D)9b1N!VC=?XS9Y9t}+js5!GjbOGaEmxB+D|H4 zs%M5=4Q}Tj;Jq&~@>Uvibn>ZKxS4eJdj#<_gv&%mB}4cSS!_Z)U3J(?h7tHud2Pwi z`j~Sx*q_Iqw(`@mr>#p7AM5G&pu5fN9M@2am})0N+~tg@Bm@lYb9HAI_d zS=5>EQ0R>#q>I(}KK%_NaO(bsOxG;`7DVg*4byz_|AxgwRex80?E2r~|4-6?^8bI5 z{=2EBWK|$gTdep+^4Nm&O?+^&DqnlDcQUUzd3hqPa1YF$Ei7Camb%8%;-OiVx+Yx7 z_b&g&E+}a!TBkAM;E>lm0Eo@!odi&doWe4`=FaG*G0fJA2%nlVlJ9~N^E`WWl%ymj z)}{b5`JQp9Tdwn~2JENBHwW>fQU%tH*PMUh3}2>h7)4HT?`p7qz8s2mlnsCHvyRQ@qOnAK_CB#-OUH0u;&bHc5C! z7gf0H(3Z1u1D_4~3?h2MmS*yrG1>btsz;}**S)cOXKe8^YzM7BT(6h=OSg;sZWS)vsy@Z08dGWPy`#TOdzm$t@0eLitN~~rMGaq;0hxlQ)J{)B+_jQT z_g;+TphZ1Q*!({L=!u`o1_Y$dEV+?Y(0Zc>!))F(SxQz=Gtk9p3#=8tlW++F9OAo{c3x`URCg`X4bHDq!@lF;piH!;H1rHixu z75iM(+Vf8SBrbS6O}V=uaRR?g;nU8SpNQT0o^7cKG1s}lf~SIv>>AvPcr{|6kk3EROda(Z(eI-yj#4Psr{e07C*SCgwoyyqO7oU1Do1X z3lgx5)!`%JoLs$ zBOM6yaC_AiLY_yA?oc1NY4(^D28rjAzjq06&HX+eOAACv~g&XDs{ddZl8;ec4~B5+o>?ACOnj z{70*(h%~9IPuYJj{?ql=z;&4=;T7vYUAtgk7g&jJiv3MV^Tiz?%y{HAP538!6{Obw z8JkK)ULw!Tbm{0eV)J4gmoRA`edaXZTp~x99@>;O6nx}=H6RzQL+`*(dsymUiD39z z;jvo3;n)}>6DAE?%=&M2NkU1lF)y3hT1CtQuv91c@9j>ZYOO%v97t(Yi?!8>H>SvB(q@cGNQ?Kq%6W)q# znHgvvJlTOkswtm)UsPb*bX9268e0EJ81e5*sQP=#p`}`l?COz|v3nO(N9q?W-G1*q z4^&+PG_ppxs_rtBRwwPY<<#S^=G=w6b#`E})}%!Vq+Y$&dd*`OI6#!{ksiz))i$R9 z$LdX&WGOC!5>miA!BcTYmGpdmLzA-3^pDVx3H_cGmmuhS`MaQ3 zdeJ}zgg9z8LI$=5IIvK(xR{`mF08cQ)^AWgeLyX#B2*_7oZ;M$Alxs-}t1S6lv$kE5ioDZOowoMb$RKr?L||+WOQ!f`L%DOKQWm zx(ml#VTel&YSm^zi^(Uf52vk+;*0P_u5t0Np1exRiKTW+2w*7F2Hs|LD+u97pO z73=-B>dQt-V^gxbhlK^>7M&W?Ls%*Y@0)^x=r#a1Mm+&qwPOUpsOZcPj1IVSvTy0m z^Tca06;MO$kdseFDy~t(dycZ(=2jHtXf4Nj#^zK#{1HO#qBP^Rv&l`AD*9 z+pm4__$;V{fRLvIkaa?L50igpX<17kE5cwTMv}m^>X-+#37&cbw(CAeeSuDC-vC&| zxraC!TjZ!YOtb5f%J5K3{7FxBcAf9W0iLiw4eCW3yW7SF+1hOoT!|e#&>A%WJJa!cXRK-h3VviOrd(%IL&D zGfvy3KfzM;4wz zTd_Ot+Sh#Wb1@@WuS37ck?i=fma-Cgj18b?2GIzoEDqu4!hFw+CcuipJa7@{jR|p>ikFXj@}zLi9giHaQS&KQ!fcwkgli;%anU|vXIgP@Nrs8QRtg$ ziceZOW_y72`HZCOb6lz=40}8hwYJ_*39hq$A80>Z`AR5*y?%IGC1QKi%P4^!MNq8G zTl`vjv=W=*^5O(%>5+GvW6xX>UyDWFm%xq{#2Ys?--5wu&om7Q;K3yZOmFE~k!WH^ zh~1N8LC-L+4j&upJh@&bKj>LPwPV2JqK=N3bVES^EehlS#f@2h`YZ57Xk*i+=q;VjQ%|~*eK%XG zxF;PHAs-ya5w(ibHP*8s!{VeEX)?F5&M@rLGx^n%+IM-lYbJ;;tSC~fpxhu+bjyvS z<_pyyi*LU9YhHc@UURHPgJq}oNm1_bc39RLT z>cO9nWIDjZRG$g!)OBA>GQTLJW7_iMvhPtPo(0vK5J|I`h1}f* zjXP^VNLT#l4Frv1g5{}rXIaITKd?E7Y`-PepJ!7A-wDrvgtwaQb6<||*F(*!HTY9(m~taCUE?i&cY4xSamBjEJ& zEU({SajFxYY}>PcNa2VlHfTrrSWy@ZA)p`#uUtBwFloozX2Tc&2=(nfS%u`1mwo2ztXA z}+xY2>GLbq{4BA#h{Y1Up+xVKH*q(>kR0!Tue)eqWS3m87{?1BL|EA zLc|d@+c=4CQx8;b!f{X`)IR{rzt3s;+4tIfeEkG<#8fzhLH|%w8$AZQC&o1*R+>T3 zEwADecKBL8ci6Bn>unQxFe8gKloM`QjN39&K;hnS_X(p+sV^3SvH+!GL{Ti2FH6jd zr*Y=H>qBws@ygzC8@i75eu(X-8ms=L{5Gh>h#t`2d5XgEW9XRjW3+GTisZGR%C!&M z?O-B=U%}YWv~;jex}Xq`BA)8n-ChllePu!4Q+N)$v@0>ZO=%w+N4H<-t?qrd^- zOgvCVdLFcw*SBz6w^%GX927}u1E{=<)Q1H0stSM(pR3;hHq1+_i7>S?Uol@K}mWD$Kshe-A&-mo4P+1v09b3j;we|yAXzg$pw zr;|9tOM9AMcHJEHRyD8}+VPgEO&xvwHK>(+{+p)nP%9N{Nt5H{q-SWfO>VRv8~A*r z5&eUu9~9rp4VHfHRAbLD5cWdC-Wi%M3b0vparWa)tD#WxuB6J0z1`s6k$&ritB@h? zqWXY?S0zljYfvVtA!A@AlmtWmErIOySrb_|CMFQtoPt7rv(wmop?CL66fn#tH}*(5~k)nb!YY*m8oZcMC1@DE0eW zUvr$Eh{s`bF3g#ycMGIQZj+D*hZ@*S=nsFena>(ce~?P(1(5|?r}0%nTLr>hn^O(N zq~+fJXghuCH=1&=<5#Y-o(FL6i?c{i@~RN99SV((`rx-tVdpo*nhDnx6`qim!!Ot$ zeV8g$=g5qNBoioAsh|+;yji{^(ktl__|AyQ^f)lLtDxsrjMrFqa#)E>^Q?Js)Ul@@ zcx%%~Z_-Sxt=8}9@UUC>5j4b^+MzuagTP{nvM7f##Ss#okyWnTs zi)c@{$f9MxO|-P(hb%$TuIM@UD?@9n3;>}ub__!XX!^JEp)z6?7jDH$z%@l*QOut= zCZqkH?1u8z!l5mPS(eVJGW=# z$d}$>sx1mfBM;=cC2A6$z>kDVZ*C|#u&r!YkQ1UTQ)2t98`~K>EL;ROHl?Qnzo8f`((^ z1tUk2CDk&qjD_K4t5askg(Qq^n{!v`!XPGyjo{Z*NW3;rj7G?jH|U;``^G)FW1E})KVZqiE)=t1M)=3;b4?jTzDetcLF zvzQQmDxU3VXsomB8}I|RA8R`6ZDgBsgBPLrVxinit%Qh@T<8oYsrI^_ZFCt@b&npQ zR_QR5?xNxZA%1Uht|L$~)TChWQ6QsVtZFD1W6v=*zVy&^SxZ;rAXbMCPLEg*C0zZ> z3Vg(8Z9U;uu7fr`1Z`WS!HSk%u^jc!HZ+z!Hnhx$IyqgG$;S%VWQg-#naJX7r*Qka zd+ENDX7*yzBm`ffNtPuw4p7fwa;0%^lv{CCP2?At;fg6}cO7JliA}WoZcAH_E@u_3 zHeL3m7wE3;;}uI24P0U`UJ(s{5PvVL$8AjaMdf1d*4w4tDkF%m9#i}Eh0Oesf+o%^ zRCLeE)UTlP5WfK!Oz8IRz^*(=W%@Y#lmv!2hu?K`ONPa+oy=xU#FSZST(^aNftG6W zQDf?_q~VEu+?$H`-6z94<#Tx_O@3EDTvBgt;DQ9purG7&U=iG@#c`#Ar#zsHaLne; zRcgAJcXFc|OWxM`QW}dkOENOZIo~c*W#aj}dsjHOHlo^G;<~oVY&_@#$+?zZF*Jeb zvf4KCbnT?{fhO;(I6y2*e3g+OtmGx;l!K!IdbA6^uQcEL-E%2#I)ROk`d(#mh;*Wt zathbmf?hNd*v&Tf4JIA!*kB)vgV%3~q=_&?wB|Qlz&Nl@#Wt0;c}?OlUswTl+Qnk1Efz{Y~UIum0G{l zP0OTm$SG%@^j+#Ib&a1y4#naGNh4W8-=fvf3G7CCb$XEOMJPr1?!GGpz<#*^fRNbS zbrp|a)P}&jRiZNO++I7pUn+jtU@rih4_|j6fkHHx_wOdlaKABf;Y(DNw-OK5iJ)}* zVX^(NP;iy-8iYsC!Tu&P-T$650@P`Nfd48s*=5^T^tyJ&PD0f8{bPgsZZOSFT;@rsmFc``_9P^o@}1J|;mRgCaPa5RJsF$RhO*ww<99i6IJ4f8 zp|eh}a6>r8rY<|TLQ?vdq;XAXgdIkU0|mTBgji0akEsrJPLo3#)cm7wrU*o%Z)|a6 zZ9nHX;2voZuF&UsU%mirP3=rc*4Zcah+UY$SMY0jpb%&hM4Ee!xhIaqq|UhFSGir1 zac3Q-yP99EwXoF**(YJP583nw3m!aH&AE1%Z ze`N_DC$d&-6@+b} z9%>iXQWC{n|G9vKYJ)qAF7IbRe7dhgYFNnllny$R1HR_N_lzp~wf6+6E11}T&jc1& zmp{Gxb`w@Uw*?efL1&W#gkCU_8)oCt_(>x<2%bZ3Hc$K?(hRP4L=O=z3(d}Pna)&F zKEvpt)d0$rs(X@Oo_G8h6&;h8Q*A6Yw-$@++Kcj7md}*$R<^w}8;w zHRk$9!CRp1(D2{g?ACEBsRtDK$4QsJxFh}uZ`?dfbqGSldHz0csTnOxS<*qcA|qX zy|`P?xXq>3Dqtf$DX%!#)V)is50a&>xqW6&0W8zx`Aw9_+3&)GN8w1QqR)LNd9XVTdn{6U+Y^!2o-adGd7I%7gN;j7`^kEYla2d~vHpVha63LvnE4 z{#ZxKKyzexLyq6t$IGnb8rv9e(9y~COIvYytHnr&TyR|}!dGaA=mS;h+VUflQGVA<@)oNA$5X&X*34q{ z7OL-?InFQRE*tXo?ga5(1M#+z*^=Ns zaQ1P>+C<=A`R{d*hM^tzkQoTuI7Mr=OU0P|1VhFTG15KbeuMS5>f8^lcIao0_!0sn zm&G3v7xk^0O|s_{2$deb=lPtv3uax$nc?)X(4(C(_3kszoRtY??ND#I03PT=rTL6~ z_GMU0HI|8cJ{mTIVvBG$3A(fVZ{En6L~EG!YN2Xriu+{q)t2EDeKdX^e~WmdlSYEl zar$FzV~f3m@EEgpp`EZ&23+vAkmO96VJQ8zd<{mKa4y@;4L^6R|KUQ|dbZ2!^sJ%! z`jUNSw-na3yBw%7FsPnE($Q4?mBh-4Qe!FDtv)b2sBtAE_sF8uX}9M^ZXT0)ahy-? zwI4pe1rafe#h&`)U|U^}Fl{WPIBGS~w*BtFMggO?Wucisox8#g#>%PxLx=lD=rTXC z)vF6~nwLOIF!8oiA(lhUTssNgeMK36i(*yDIJL~AgFX+F137+#%f}ZnHU16OnQu}Z z5edeB-N3mtD9HTj$a`CQ$!gJGi%!8Qg-Jy{|Hw9;PWR7YEMbNc|IDMx-wKp4`p#MP&w0#& zsqgkLx_&25Q3lEU$F97BPH-0RDWj&C6f1ifuyZQvqj=b-@9cf{8b=uAnp10B$82aJ z;#S+-p(1L)LW1w?s#n|W6bO#8q%Fw)25Pk&9$e`2HXO2*bp37dKt6yCq_`@GVhyS3 zihZCUiURg+KM%wAWgIfn`_S=vvrv-|@hkk8`;k;n_(n-h%TW+kGbHm^JshY82jZt? zB&S=gFh>I2pwwNA3$J0hgX05dE&~y^FemN1b|HToXl}&%8XULKcdOMN{4UNt0@GLl zb9hsc^INu9LJ^+#$PnRSy0NsZn~0i@=cc$7;RGnDDENBb1D`15PS|ZfgzeQjHOT_u z2Gyz0Lw0Z!gWKnOw;Q%>G>an5#UK%ZX@D)OVxq-4L{sfpi6yW0BA^{2_)AQRplxo zaxWx4)Je(uEhqjQhm+>LaJpCFBY#F+^erodYP@*JUg+kD81n_|m3b*h~G?`?GM>d!~%vVFg zMsDG~t-92H2_Wzm+|0P0{X$VMc44Z}^Ij{y&2AA8AoIZ-3Ry(;c#deOll5!^{HUZ~ zunvPT?UqJa>==(mq>c5V0aL{DZ~q4n+L9GAmZRy?9vF1(vBLt_rb+ZxxQ~VLRVETI zbIoq!xSF6gZCS|JvH*y(fCdHA=ZTX8BqSrb-> z=ftD<48DDuQf2&wRQ!&VeN{PwAKy5hmERYG2`Kv$L%=KT_cPJLzFvCL3p@1fW7D2} zW$&(!DL_8HO9w@|LBXBw;b%|kYm{im4!O>|J*dS*OP=f;4215~3VL&C3-(*TNev}- z=k(p9@1ReXVNMmFAVnEM**VTO8JLV-RKN?_)j$XDU;}*)4Epxz+hqD)e~8S;m@c7Y z_&b;7*rM+y|D}@9g&g`l@e$=1^bb-*ZY@KvLfErizE%%>E5CrF?O)$soB$aL470)1 zA_~Os;K`16(3t2KSWMNw({L@42hkQy24m7bkTlfq*xQ#^Ivf{Wdk^EOTR|X@V)nY& zs(^goua{5JN!L4u#U7sHn&#t@>zi1pGddyp>TXY*?=-oxGvWg#G8lL*@%rG5ax%@9 z_?%!9RmU4rr_{xdCd%xyO2On2ig9zy=dVas$rM2E-)U;|CT zi2P+vd0XoKS_#mk4Cqa{iZo15G>4~NG%}HIT11ERG91@VM~C<_#?9Qv_BbQb?ydQ& zXT)Lmv}^ylVV}rzaUr+f))DpR;t`~QV3r*S;|(su-6XWThO(no+OZqvDavOU0g|QF z9`QTdr9)4PzEi@R4GG2F(dI<{4Wd-EWmEm+-?e;*88v}vDe~Ci-?gI&4YtX0E`opF zQk=z2`$Z|N+$U{Rj;P6IB|jFi^TFuya0eyTwth{GhX9Id9m8L_?UlLwu#3o3C9Z;J z(s#6wx>hpQ(!!J0Jt8wYeFpA?SBRPmVjc~f@1Q+HMIFY5-^_jJG)Y#rlEwkheg%9R zHUSR{rTUrq^n}$LV6y&1IuVWK+zgo>sLD=8tuCteUs;Gsf2d0V(3d>m${|#ACG}_l zwV(Xq`&cP*;`vMXwpn|Fac8wh2u*+Tu{?fLcfc{IPonaW76UF7KaexFhmkXq`u#>L z346y##ko?T(1B~4p+rkvs#%wa4)T?_U09+*_v<;(!GFlVS&Q_#xH}GPyb8`U(mK`&rt4*c7L^ORQ?FoT(ApPS?WSN)I}~z<3}Xes>g3 zdu14$N(0YbXT?^4ChakvXYyzau+F6 z<*)RJ@Af&=3>_A1u=GEXT;hW@5L*^r)`>Y#+1k43L3K8YKWApW+zN(7JaLqcFw6}_ z?TiW)_1`gi3v;ADo9}e%y6;k`Y^-KAZ$|!QBVsPfMx@KU@`%Au_6vu@xXCTYPPo4t-mx>e8Gk5 z$=LYvDz#A5eWFAd1NQ^A2wQ^cvBv%(rqD^qce?Fm6B*%DOmb>Y?0%~z7FUyI3saE* z$d%bpEdG9)`mIokw<&1tM$&*X>AKj{A};f4f3mPlI&_hrP_YZS0bBmPTgCmWLxzVb z)3=RpQe8yqFAR=^eEd!VD!MW?y96iaw?Q@*c!~2Ju4I3DS2Q|~t~tOToxkFQ4}1tf(#`jkD zVdLX$N+O*U`MA`N5nnLBT1DX1bkT00$GcRtb{TTCYsXzX%XI$uOrl`Smop?guDc&Z^51I=br(6A&O78J zUtRV#`C?{eqE^U#HaC(e43AI^xBC0X-&ft%8Hp>BU%+JMN9>@7DUgiyHl~_LJGq`( z#0fZkq>4S{wPUQucprOui-Y|GjQq4;yE)uxRyvN;Iti2Wdj{YD3Lry7uZ{qw#Yl;C;5h!io$);UgWDM|;bYhV_kp73< z;@K_>0rSmPyJyoJi&Od0q>4mSTgQs#RL^GZXRY`;g`6rK)%laC zom)$ENHM^m115t67M>UAQ}w-*zL|Y|7smem(*l+P;1y8W80DUhuWzg~=NX(lBj3(p zNi2oa^zbSIeKB9s=M#y~7gt!L_tp0;vC=P8R&7^uHZ_Ot$})=Jy?WQ#R_(@pyUiyA zt@&;tSij(KfXbZ=vHpX73}Nz*wfSz3I0bJ*IMG{Ji&=49_gtn4Y0E=tWC$YaS!w>R zHIndL@$`wV>*1fNrg(_yvl)ija#En0(woBzDcN5(G0@?=zqDW8d5~QWhUxyy;mL!X zWEo}&Gw;-K0Pm10-HrfS@GorMx~0E#$e0_+eu^i<#54> z0FTSmqTvPFleWfUvlPuC@CMWJ&E8($0X1!%zZxR8qEjX0+U<-5&|L~%;CZem^ZMDA z2b^G`BzCo6SN>MM82dIwVqaB-zIQ5J^_w>KrWwTCD{<&sqfzJ(^xK_M+#zP6koHt5 zyZ?0yv_Uoi$s^u*6H2l2&Y(`o0NUE|B3M;v6U%%ZHj=|+IqvLzt3_^&$ zI|y$lGy0|(y*fJ!XW(};lJcKEC<=ycZ<4O9^}&#ZZsyk~AvJyz&E*{?mj4i(G{Gmx z|3J?FNpJp_^ZYOJ`CrcSzx3sQInVzfoBt>B`F~6Le{!P#GwFZ8rM%EDg;20i_WLQ* V0N3cuzmzN#Ku%S*R@(H-{{qXoe;5D& literal 12172 zcmb`tRajh2&^3y?ySoMt8r&sV2*EwLI}9$t-3b=lWpEwb-Gckz?mEby_xrz_bDnc~ zZr1LuU0v0?YdyVND)O6(96AaK3KSF+x`Mp)cPJ=m!++&8B!vI)aN83KibGvNTH*&_ z^=w5|KG~UU_}cO#Rl%A^nKdZ_1KXTBn8@z$pGnDM2_92)-NE+8pAPf9H?=f^iwZ~I zFxuU3wz*8)Fd3-bkPqsz4BC;m{4teW#`pisNu0k0Em@!4?5_CuJXq1dKv@-}i9tb8 zfuO_z&`=?SP)Hn5fmbk4JMd6ghW~$y-&?Le50Mx_+KyEQzujaG=`ca>7k~eOz@sVD zH@-|cP~44^?n}>JubejsWBX&uM1h&Pp4><_B%Nz~Ey`BohfU*m086)7F-4yufEaSB zQNR*A_ZP`AwP*;5!_>zqItNIYN$?)fq5p^xh;2CZz$9Sar{_+=^`H9MqU@tl&9yX?D}va`^j@@)Zk>DTqXYz8kji#=ConRm6=J zv`~q=sS=BA=(>sfi$ax?Yky81nA3Y_>BH1vjVh9qTfflv#rfzMs3OvI*7bVz9<+1Z z#b7w07{xOh_Wc{$zn0Ov!bq1Kw>j(g9Gg49IwIW_r2d?S*1Ux>=cTL9wtpQ(0AhOA) zFGjt4)X$_zTdpp-kZe-~ATelV$vz9s!E|RY_kByvrXkTvc3Bqg$50({?)K}!dn<(q zDx;PFEw7C*_&{^``btostBfKJAzc0VzBq}H+=ubQzbT-T6Ehxp` ztq;dgOXiL7qj_X z8=JU!^x)l7$wXa>*p=utLC6i5+~)oD2xL#gkRst6^OHt~(pbA6PEt#C_LkD3W3vt% z_mRUX(1e+5Ny9HF)!SWCD8?2&L7kw~fJK$n=nqa@s8x7lj#EkBwTASvSrJ?GtWsndRl%(pxnI}ZAw8up%HB4X`B)6!=IFYvZfB!M z)`@!`p}m1yVys3iGzUHq3D+VA%PpI%g{V?>lU93W7ph0oddPYS`#BLHqjvXV(soOm z5E_kK>Iy!eG3mECh~h#6AFe?kDwmU{UBCbJ<6l@-ql*hYj}TSffvAEI(`F;y>@Bba zvoU890Y9BxQ(^@=&CrkDM}v+6JY2I--8h*y3D<4XF8o3L;lhhNoue=2_^a_o2+l%( zoiKw&@SmR=8nz^V9PyzOygrX(TwIYHi67Y%7zGQ2U3IpuVIH=4Q++N&i9be_-5lAn zlXj~sc~d0G4<0j~J>KFC;ez=67oa6HLv%Mk*@&fP8hayD*b1=s1g5;sLKtV!52{(V zC6k&>3Y7$_za?*QDJMMGxo$Zz&AVy&`(-i-a5@yS9b)IPjI7*+TdS~^QU!{s|$++QBq90sNary|W~8+{1ts<7Ang;4)q znwah}us^hYdq-{YfHbs|bsewk zb-kk^F|$0GzSbMua6_NQQzUCjLS=<37Hoz|9a87657W0S5o3iV#zZZQdlXn@)j2(2 z4#%&>70AteT5h}xTJ2!pM@*#P@xI!NPw+U$dQkZkNpcREyWUn^>E#%c?y2-!=W)#v z`(Gx1XytuDD^nz0VnOY5{?#tb&gDfw`ale=O$PoEVL`WAnu~iQ6icUc_;aG5y6l`7 zA?P?FRL^8X5DUgMG6mu-_;bD1`x|C;bVy+(HkTvT5N0(Z=B8n8qCIu#PA%43`mEFT zT+UIIA%-H15rv`y%lsIsBFraLKpWi)Ta3?P^DW#r7^B9=Kr+jf3C}3xAb!boF@gB! zpZ`(ok`YjC)AG)l+wgJj3#YqX+b$q;*cWN8>?GYIcXUytn4hnw6*2V*xxpb>v&Zzs zCgyhwu@~}K0sH2WSPynIOvUb13Rs}2hI`6^GPq$4Vf(#}yw#shi4OL~qm;(O!b=ud z%sC8Xpw;%ov*Y69Mjjrf`0U662f`uOYkxAC2^`8$;O`Uj(3Dw^(hDT#CN2D7%MtXN z^*ZS*l!XM)IDpIgZ(hpmg#@rS+A8BNH(i`ZwNid+Xf@#~)MtrUB;jon$oyvXR(5Z7 zm)%TKEToKe)a&z3@Z;nQ{sQted2dq!XdPN%Jc}kH)p}HnudjMJ6w7wGsh_$vz=MVT zoH%m+ROprnak4H>UR$5?m+-loOo4VEKd_LJm5Kf?yB)!R2tNIkhrkw=#p<<3Q(kAD zfKjv6xR*$1L?MuVT!B*Pj?D&UyNO>BJpj}2R$&xv^AQCEPcA|lecF>LgDO<&7e+|j zQq&rh=uooN>@|&nMece1VD&Vy5u-zjnddal^^|~pEG}E5%f@G-W{@3y_N&27JR4Fh z2JY|fT~Yp|FPbO&(qxtWS`M$`cqY;9AeOxbRzUKUILjVO*bJLv~C#Dhm@U zj)q}R;p}U{8G8eC57ADXvOpL)ZP-f{nq%P#H@z633~m1}FPG3ZKic+O?>6YL?MALL zSs^*UXsPLWQ!N&?xocqH#=P3-n49gi&~lJ=uQY~5m$?clBv$dkEy_`~ayOq5-#dh+ zq$`$l^AlUCh>jiPPIj1EK^$0w&FB*Al0uqoyuW(BSQj@)58uHDL=OyZ?eGL#9+o#< zDTf<6QM3`ob?u~(zf$|0me~yA1~tD-l033s^ws1gF)6bx2RHL)h2_{OVS^5)%>O6$ z6v0b#{xhC)g!%eey;#cp=6-Tk8>l{I{fkFOaf^lNR{F?|zuj}AZtM}LTzqvRidh|| z2$cZ%mW_Id%%mG-{hH!RM~nh>6{#sW-0k<`eIQ+dsjn>!St3}|6d0q5UBlRtJdt^# zO+)xxCi~h6YGLu}A>ZDJzVAG!xl-cUNL)I06CWw1ONUY<&klCtH&TF!1liaxiRno= zds!p-E;(^iNvhBV@-^3Jwdp_je(^7jk85Q5`PjbO+mmcBbOvw(VhPS)&c;}fc?B_Q zD03u?n*#|P{=`;UL48c_$k`!~u@7P@=C01*oM)@x6oO z{mw6kh5~K%Ul7oo#Os=Ow>ykc>EjB^jzwW_*2QAW;q0$vQ66hq%6L$|Ku~;*bo~xB zZ^Vc|HDMP^RFv6vZc@=HyD4$H0k%4pp|5(B+7LVc9(z|b!+l2TH9Nkh77e7z5xYio zjZx7UG{fCXqh~DWH9PXkfHn(aQDgh>+ic&fC7@wY@mDd78!|7Qy*{+9&h_+`H|wZ^ zG|Ob5G%sSM2I92wta%Yk{6oy;t%F-|=V~YhnxygpZzsqU#j@1956;2`TMzHWet`EY z;m#Jh>#I3KVUuUa)q8E>)<#s-i;}D5Eu_^#fXgA>-g`{eHQ?=r%YnYS=T@#fAwp*z zFW1=!-$zNNr{Fxn@9TozWSk$hvZwT=h?Kn1=5DzE8~GqMwL+U3g9T&5bT1+kFH2GD z-}V<&+c;0&D?H=-t!ylu2GQ;Zsig63W$wFHH|VgleGVvm2CDh%Y?rLnvTfei_fGd7 z>@V7z-djFn>8nLOdOfq1l{}nUUIKG$GZJQjN>}yT1vW|hKqjM(zNE1v*3HT#}GVl=h0F8s^SjRQrM`dTJD!QDhncr>u-s65bAG1 zxX4`{*-O>1FkgMT!Ro=c><%rjd@NwrJ|p%J8zqAZ?&Uq9N` z*kody6<2HcL~68w;|{Vu$Cp;bZttmt@02u_V}^2cc+l}!KxBg-WPdS4OvUcWx_vwK z?=HJ@+nPt!h2_tv=T?Yq0W={{*4;g%;KiAs{f>CMWAc0>^AQf@1~hymHxTDfV+JIer>#UyG54Y)>2 z`5KKy&_QGQrBl{F*=+~xSf4({THVWISj!?f zhTtGstEQpQA=67~^t+IPbiYuAL*lZ@;lSBQdz|_x3TCj() z8)-a}@G>qbAE*s*zXM{1#F!U8n?>Wu3{6O?Q*?>JSY~?bDE=g{!VClKd-FLQd%u_KEN_F_L`rT>o~6zs=$X0) zN&u^c=n-rqlBs%5jg0%0@4m|;1I=%t?wmZ+LNXVJ2F;bPFy>`J88u;*h~>9KQ4>@u z@UnGvpv`!QW%KrE;W&Ubk)v>p@lj=d!AYDhT#0k*t2lyrgxC|2ZaH-*Ji#?t8Z#tu zNO)r-ORB&Dy0W=nN7a*XM0AhNiPnNIsn51hThURWzH^NJs$|qpP;)>}vinms`@a3Q z727AmyZXO&JxpJxUj^u~TRPdVPU4kP`=xdaHNHd;qzXWtxj-4x2kbWLU;QZ^eNVls zFpbXm__oB$vS9xtIl!(=f6-7bKG_ z2%ojNgNlcn1nBvzCvX3i}Q>gKOw7mJ5$9AdRc z3g%Vr-6M||*SJN7ApjrN5)&UNQzsd|B_b%rFl72dhsq@9kJGsbW9i0SW!FSn*>m>H zQ{Wsw6iw7V&mjLeCXY#!B8f`-WImAHaxnC%F+idy%=RZ^a>V04u9?0_)UYRR;?&Xo ziii#9Za=Ds%Adondu)$SVT=gI#7HT4C6ALF?s z%lm2a&Dz^d)u-4|P-HebUk-caldl1dYnzL5j?~eKs?|w&xB)xHHw|V4;LY0_i3wB{ z#?k4@FP9Z*^P6q+!BqrkJ0*FY0oO6$A|cBIoAr3B*s<`7k^#K!k~7h81t0X~aTB>y zOf|6#3_x$!?{@eSr+ltE3*mZ4RJB8iP>P3k+d!UHd;a2&v9nA;D+v;K9_*9{e=ce$ z+7VCKSP*%apyHIfxgO3QaV{`C2e!OJU64ll{5KGrR{HNU#-(Z)2qONU05Gt*>< zri5mtGSLH@qVI;2okwx^-F27$TQ){mIKcNK=0|=V0GvWM%fv0M;1a76R;g4|%(<#u zg24K{-=W;iG{hGNbIOr0QZkBt*t|_i5qTcPt3~5bUDajxvr&;)ned)s zrgd*LI|D>Bn;x9|u#}(_`i?uh4@Y>%MHqxmeBm+0n9c^Bi^@gm?MQF41o%o{aQB@JuYnKzk=u-flHQW!(LZ)=0J4?K z%EHNXtLJnK3;u(qvQ6r{WfK<=SS;;q9*pcdTvF9wE?q_~D3vjma zMK0J4V~dpv#vy&<6#?v8Xo;hHu%1rMir;toizn~gs#O+#Rl&>rgMPlUUW(gwSt4h{ z*_AV482^fuKXV6skJi1HKmXHkou5AZKZla?{^n7uT=AN|(9Xl7X za@30r@R|(moEBd8Jgvx(xBG>wm^JeVA+B(x(Ax~)2c&8}N)y#Oh^!>*2Z#gB^X=7yhNaF<}+cMuaq(OYoScdS>v-XYj#-(~J5 z>ap0KC$spo%@U7WnHKv{2bU3BCw~t%w#J6pUh1zBIHFxXX@hPAa9n(;Kx!dpqcrzXICl%N(QrDHx?Y>}}T5E?`&V0I+? zV1E1(RTwBs_gmKU^M(RePP>yJrMF2;yrf3*nsO5-%L#PIBEO zL18=#*i>7hZ4~L4Dv4Iqo+RGy({)4iW%tbT$w5IEoQ^WZdfrg6b#Glch;kk z*bfP&MV9@pFx;^UjilGnPLKh?qB^0KjTHCRWKdX5D0yQ_@tp2MNT!2eZ&qK?Dwl9x znh!0za6@3Q;MHVEajTCD`bEy>K*$}!<_MbQ3X^4WJK2r;2@$8MNC|K= zT!H1WCphu^vvtH&CxZ-6FaF5{dt=h*Xn{C4@Ie+DxKp@^B=T=ZC~`&ee(jz@W&7!J z)9Xq$qv!qJ`XCX-96<{g3i!RvGKcy83&u8M z6N>&mhwM##xp!WtV8G!l)G5k?l?_nXX5;2m>SN4KX1#Gj$XXbnDIP^oq|+{4PUryv zzvLgsIB9Xs8BA9+d`pAou6sDNl^5GQ9VXb;ogiCR(q)fzEfOL|v<~CK>H6xmBV5Ea zJt*=nFCu{K0%_8&flW?SQK0enkddcxJPlD*YkcPXaluROU!~15!6i*805OT%O3#QC zW!O=137M&IUZ4^oW6i`cvUrs}8WU4$Pa_5iLf%%p>Dh`#=N6vFdjbf3lHq~ZkHB@^ z5+e&FAI4r=_2W}txXa4#)d;&icl8Nf&EdT7D{6LWw6psm`#!Y*J)SZ59JC;}o&mcKKQPY>hHx zqhwli7f}xtn&1KB&ohn@$btMPxxrcv0nQPq7}tmKPB!kOp!Z$;n&FAQg5Hvr-@hlkscD6-&V zM)idG=p|?7o4CLwl`lw2lpt>s+!dDEqZLvl5OxXTf5$~8#0?&8CyB%bo5#%kG9(@8 z>6%fYWr-A+z+Zdj%K7In_@mh3^t$N3Qch}?$-^BaSkxM0OY}YYv~u1#Vz#8XB5>32Vk!ZGDu{8L^bv3UORGEn zQ3Td8^UqF%iT5hO0Unk$1Kg&xU*dki?^w5)R%HblK5p!C?qM`T#)9j}M3b@~PUT8v|UtoTSAvu<%%{vdR zaQzkTLW{x#DX-bK_ptKsBCbzqN=Tp}UlAR<9eD^kZlyZxL*knVo9=w^j1a=+4}-|a zM}=JtB&9bM#C`9ge^(Zy5Z9 z&3X}qjzAwv+6;M88Prw!h|aUWB@V)2^?hF%m2Te&6y*Zq$`t6;iVXtf?@qwoSiF!> z8lda}7G#C5eDSKytlnija(6CuwiyU452I2ZigVl6$t$r+jf;yV>O>c zZFwG%Nhrz^=b+Tm^7e)J@X5$f>@Kzx_o&foVWs4oV|DjnFjwliHtYF$;IM=e$yT43 z+|SHR#v(V?5cSG;cn^zWmHA9=MxY}mV-wZ=nn4#IT{L{+%8Np1UyKF~8mW$t0UQA~-hNdt)Q~CfKou+198H*wq`qyK@}Tbu)R~3~Ujv zOY*wqzl~0eXh^PLD-IHsH=IjQwh|4aYaCeJ?^EcPS&bPg%6-Vc=;8F;{ertl2EEm; zvcj{y7=p+ODUbJr*#CZz_un_*{TiJ9`1j zG+UhAy7I9oW_8lAeGLNU;ot;A43!kT8|*ZCp$fHr5xlvUCD%Y$6txno)xziz82$IZ zL4ImYIf(7GC=SbcvRB}f;j&hbQi`XZS+NGXHdl@xrb}G>o`iUN*9$LUl6sA^dA61( z6TT|S1*-aZrOaw)Xw|W*#}3<~*Cq|vf_MDMmHLVcuAnI$%YW2?+`DK%IrjIAKW%G} zntz0GP~7;R7Ug?i_OPV)sfe;yK|y{!u!eAYr&cuX?JF0j2&fBgH-(oLjzH=AGmXk? zRT+WV%N9kj+=csVt$cXQzJ_QmGx*M03exHDnLRM1Sj5BUdg*ccO^T+RBU5 z^0Zp){C7@wF?z(WK96rqnK>q}x9b*BNPhmQa^oS)KUP&2bDOK+)Ek)cmI(*A*SaZ7 zd7oOFkDlCK5F0W?{X_OJp8z@g$l2yieLGbRIZ30~RJ9CUB!)wMolQr85Q{(iOIwW%f77^SsYRO=w77Heq&5;Zq zS%z3w!Y+>n(G9vO{de@Xx49|zJCtS3Px@GfH2WBImxg?n`jy0l&X7LAE1b#9cP0?A zKQu0Ai`CF{SuMpQw?Hx6zaHRy42{l>Y-{X)$5%DOfDDun!vw;3sUrdjL{YLaq6#*v zLdm|^(-j7AjN$qXZFRfYX26JS-t|_Jbs1LtTeNczhzYsni4^pInp3W&R|ldm#(5uR zvX6&F@~XQJWZ0hZ9prb2H((qbQXtyR2<0i*FN8YtO)@5WuEA0`M4XB(twsMofjZF=J zkC~CZ|93^UKY5tieUPX>GK9==MhV(%c0`xeV1eUE=}dc3UH&ls@Nn!j`ipQeWBF)w zW?)oCwvVss;^>W@TF)ExAR(x$`#sv7u6!|V6f2vtzcw=@S;{V^70hmS z$BGgJtOWU(YiSjjD2niu1B>`ZG)xZBM5OtwPgGr#ifVP3M$`I-JxmLAXU>28ezn?Zp#PUdlBS{-b7At>4|!+{k3TCx$lUS> zpi)dnepNb_Vy}6|&{;k-1Oy5yo*6b(93GKWUW68#s`AwqTcAm{3^O#sU`5KZuQ$_| zOWrM$L<%BmcHS-OTy9*6$|!Wbq#GFfd4_gDLNuRVuqWfU>VGAlq7LYd5qeUtt8mm$ zRpU?=qlzs)6AW%`DrHzhp4ke=+v(2JcKrZA;0qez*ND*k>pVmrg`WBulLiTaRFG9D_89 z*11~`^?upx@&*zn2z4-@emtiJN{EY$+X;VZi4<3*Rk&_jR_xjt&aLV1h{GFT8GUSD zV_$Fg1!LC>i*0}&)x4kYO&?wBrMLVKd@e-lm7;Ir3cGVkY)sOVjUJNf#nxn`OF~hD zzG*iojfGw|!@09Dd|ccA9rEk;`Iy|QZoYip1=e`-zi+eK@Zag^e0}K-XfvLE(?Vd> zUb&ifAb0akE9&BHie`m7tsB8vMvFlpkTzX*C zgp;(Wb(yrklK|Nmg!^<$49*+n<86N65nRQsVRp>#eSYF@L)$EUX_M&;D+pLUGXGTb zUfNli+P~>-LntFdcab|yUR2d=yYa>5_?Cm_xGx+6tsw=0s0(e3tNJd9bloVogi9_& z6&&h4;CF#>EtzE>|4av%Kn_UQ)_UP$7Oj*Y(6$ad#InfGP#u&3(4VnJh(P3Q0Tvoe z-qCxS^M!{sXL(gr+#NozBX9mX9bJsi-=8VmpWO2Yn8PUoY{^gg&8Rk+&8VM8?{oD8 z-hwO68+xdU`lk9+L)m)VCx@0wP zL_H6#$IHVmv^L6VxAGUI5K#$bbpUTgg@oRPp@X>;UE6b&znw@4&GnYCi)gb@nr&?A zl5o8LaDFoExn>xtdU0@#zGK!cWIWt?Qr~)l73NrE?Inm4-YDC05HCDBFvwhlEDK5RiQSEUSdo=!?k$5{n?cX7qWxDHzpi&9paE@C78 z^|NdBM?YQ4qRGjzWJi$IWxOs)g+@av|HhsL?&L~Nj(EvC=Q=#tZyWs`UM!SfX*sZT zP@w=Oimzk=jt@Vzjx0^%-;n%J#|c%@RsRunqBS+%BnX>IcKhSlYiJ4lA-3lWm{Qh4 zDZXlpWNJ2bU>Z4B5ol_B%@{vXT@}zKq;LPZP5_<;kSVo~-TU@lRWpP6_h03@1bc5* zQUt3+r6eh(B#q{2x3-8jT0{=71kK6!UHpg^9nB27!vt0?yu{lL23M=Y0bXDR=4n+? zZ&chrJX>w*c#LsFO0OeY=2l}42;UKJ9Q~&G0dj`p>f!ErWYL3{F2ZWi=RpS#baU;T z$?VgG6o+NQ5 z)LpC{omWY>De&3;}jiJb4wXg7V|qs53X7WAYhp$r zX}#cc9pC*1gu2@|R8OhDbk}3udhSL?Yk58ji{x9)LPswPUuX^>DPceQ$;;=_|j7g-$yF$PbtPA3hIgsfVurrV(3{BhPIq$rRGyD zqK3Ah=x>4&6*~Jm(-R(PP`l_^JEh8BzvI&Cl!%E^&TrtcmsqJ9Bb{|4-=IC^us8)< zPg)#D$q~G9mL}EgfM}MHNkuGtH_*Vi0h!sMmY5D&D3H}A5KvgLuts$~hC%eJsnyqF z<*Vjl_FBf{@8qLJQsgZGlkxY*rT%s?;8z zdD;(iN;#oePg`(Z-o~EGupEIyfDQcyk1V=P6?t$fn=%Zxqo)05qL@H8aO_}~;rV9A z*snBuaj^X@>~UQgMAKHFc7c6V@kDfPUB2m`!xE!Msdz(g(T!-{$(k_PMGntY&ZkOu z!jnnku8O;m1S>>6MIu&R! z#z`z7RYo>vQtu{3G_bT09+>p4fJv4?Q2L;X@WUOF9Y_TxX@ZN&r`r0%CyTN2X<7l9 zTtza2=1BAw{w^9WYQ$sfK{id;5=@_CM?kDfFvqaUIQ7Zp-E+9w#?pQZKs^@7qp@j90E5;M=K^bDHix@NQtZPWK}JQoM$$O={{Y@d Bv)ljx diff --git a/lms/static/images/university/berkeley/berkeley_bw.png b/lms/static/images/university/berkeley/berkeley_bw.png index 9904e613150e9770c0734e2b322ca296d9c20082..77204c3913da66c63fad2dfee334ba426f834902 100644 GIT binary patch literal 4912 zcmaKwX*AS<*T>14C1sl!YZS?reM`tLWZwsseHptSvS-j3OSTXqOZGt#W~>d#zGWZ7 zh$b;6Yu10yo98^wdCqz6i+jJH^SS5T_c!^8kq#prCmk6X8Ka)ArYRX2Ipkl?p{4lO zOBAnb{Tm!XTGl~kP}iVP=KvQn_2FHTrU2SV?8yy|} z@#9BWSlH6i()|3qzrQ~kjV>=QKR-Wz_wF4AgNcfY^6>Duyu7?#k^cVQExsUABeQ?_ z{}q|NJ$Rdpj9E!fQ{5a`_+6NsQ&Y{X=HDAuw3c;t>8PqrrS+ZwoE|74zoVBc8Iu-E ztA**VK-!nAW3=CqJW=iLD9(od8hC%r|At?s|CqBu{J$jWzkyi%#cuOM8R1Q;RtD?f z8@pw)7<{`0+u9_|`A3|1-N@yK2uLBt@wWCze?Z4d_m--0&Np#XL4<2g0+mRR#1R}i zG{#J;G41YtaRqV{c3%XX@2}9N`?8$gBFcFkzH zlsVI_f$!Ck4LX=TM(gC5OuP2!I_$R9uJ*mWJk(0JGQ4q|C7+LO=ES0)B~^U|vw z{a#>B4jmHo&z3_E??VDJCJ!Y4#0uN=h*6Koj4;1olkl15T_nn#R6T1+8TYI!4>$Q( zxEN%u$dmzX@odyiT)`;s;Il0TBF}?%x)+h!tHd>*)ZI?+SZILNEKGSffaPcic>f61 zv(Gz+v+Hjh%Yt5X0AcdefFmZFuT#l_?(PL6p^%p++XR2E1aW4*9RIeu1yF6jBVl7g^W-WyiASUjX{^Q; zmWX#KWu8X^t6hjzDI&(}JQJ9*y8`1R+8(h-(-9r_xa^6psJ{=BK}+1k5oph;P8P*w zcO=~MZVn!e(+6(4y92MDHy1!&&^<%@Vgy0owNB={k=VYKZMwzHk=9QghmX4u!n zJ=<8~&+0=Q`T8R9==Ju#rxzq{k+6Bidsyi;j}+Yd*fIg1 zy@FG?MiwF;q>$+y@;i<)B+VUeby$NYM>mf)@o{;-Mcws(g=^~PjGsWo$TR=xc$!Z8 z))dS}ySEg_-ByWbuz&u}fXC%0j9kHN4weSReM*%cet~+j5~tE=Jw0hi+5PG5Hl``2 zG}Rq5mzqkd&%{uUcx$|1w8m3>5gi&|?CY3RAaBUutFCID{BEi@n^pAdcq#%lhKM>q z+H4(aCl-PT7wE(tx&yP4@v z*AROfG+wQ8`pQZ+qd%S2>ci!kM}Pemb@%DcLUI@f6#j)b{RUaxNB7c&YB*!vKw6VOcsX+Y_J;$--mdC6<1%>1aLc>xYkeV=&1$_ZzN@Jf(j*d*Z3FqDi(CN zpX$AcFya3qj*JBx7^m}UZN;i+(`CN}*P3XU_xh@%HyCW}Cw7A8aBjvZZZu?w{#%T;L>h(|;lMwM(R2ed=od z)MBjp&F6jX58Izv()~4F^;83PHWHq6xtV_Pbwl+@Nf!8pk$sjxo7|RQo>#V|^oDFv zH`GNXpqusiysxqQ@+zC2=dIY$hI3HbAryWO_+mD3OneBDGRf`Sl-8w?(QE=|L|+S5 zaur~ys@5ox^BP!~x9W5X{dF<%u@{swAxN96xSe+rCLN3S+5N`bP#B&ND$Qw#Uv0n+oyJSk$P6#I(dywkqa1rYOFNi*ql?P7!gN7!Aq!fttVaDRDN)ve%SY z(5;xpQ~%iR);D+i9T;V9b@R(Pkw;FEvSs+If55I%$ly#@oAqY(AcQP-*&$XLh%Do< z9ZU_Ht@S^YY}d35v5Khj z@7w(!R%k!&QA0(KP;V`ZL_sxLAHutgloN?fuPXURR6gyv7`K@poZU$nmFa*#ol#t9 z)JM_wo->>j*IOnXJ^MS_20C_e;6v_dmIWuGu7;O)euja@FkFr^nLX0yAH;dC54R#5 z7NyQ0vR=~W79D-n%}`6Kg1i3bKB%=rwu%n275+u>PXC_^l2!3hj@(^+_YfVoa?TVy z##?$V`@U02`kY0{^Lgl=2(ebfSgLLBRf{tpSjYQycHaE*{oYH-w?aSZQT|a#Z2_gP zxW8)Qf!Q~1jEJ_^F`5aOb&iQ)EoR6q#6oMtJCQQ+{bl_`JR2t0I=a4~T?szSGRTx$ z4jNcn(0s6}!~SWow0q0A{u4*3v|oKS4h?ErK^9XVQlK!NEB=B04S$tU%lhFId||Q8 zE*?jlqo)AaBx|gGV6>AXy_})bw;N2GDerYPrR_YF)H0m@JX5LW)?ykJmvEx(hlL`D zUHoF%6H1ZMo$h;{;t?E5EB!;-6!dA}2$q#*K+8sc9dz6|?B8s})rSkF#~P^y@^^hU z*rS7_t#}o*Oui6A=f%jYc1d|dsmt!cMMYHIt9ZZaQ>>->oeDeUs`dB;smsdW{)^m%)ZH#w7lIji;Q4uCi(9 z_lJ^HCu)^o*joy1lOGF*JDevCgftEgL|cN`AZzDU)G-8+j0x_z(H1>e@GA@cGm?q4S+CB$qj>*rTUN6Nuz^eJoe#{B>ozD4&0Lb z`yK5+uE?mtz^@n(IFhCT&2Bkm(atoQZAi-?v4>e2`WBi@RW>dEz-aXHD)yk>XLG@* zDifeGE=z6o;u*4|24zur1s&stDfw>7PcJlnyBw`WL{Y0sjKtOB9Tlz^#q(jaG)q7CxzTnj9V5Ic6)#FSWySyG>ymR z6ghE21BTh#=*xzWt*c*!FIAkvHHdBkI0q*)eYl&$?;O@?#uN7lH9$pTm?=&$-+nwa z#6xrz9XTT71>6_&3bcFTTl?tq8NqwT2gl!*BEPk(N~g$jPN*;lLCgkiJ74*v_}d5U z_lC(d=umQQ$cvVHA8Ia2-G%fAX{;*kOgVkfD4BX+rMLNAnO0LErn2F8+e=D@0rK<| zuFmsE7QsThejyIjzXxuV%@P(Qqg4WEH+qQc5$_kC6!{TZ&qV~$&4qexQ&%4%B$J-= z1yKo5e6y`;t3!s=hE%6wOXeS}eybzd`Y*y})^BetLKNi0V)-XzHlaX+61 zxuq{L(dz1QIJDu<3_hRcKCy*ml{3zW15-P{{blN^H`C$dP~PL%wxvNWf!cuv_H{(N_@t5zC~4kk&>I)I*8ogmOm2k>~w- zu9NEiEjEEX#Hj!zV3(c~;W0@)z|zlP$fqM`H)e{T5n?i*q)8<)I86Q}Do?qb9lnRHR^_HJu0R-`inDXev0J}x{(Jo8!!bGp*? zPkO&J*Dn|w^<8=nA*YmjRFvNR3BIHG;oJSFiQ|R(Ky*RYIX`0 zDHqdM^RJo=+1isokj)X7#s_;0O^X78eHdnA@z`mbF^#5+9A9Q@m|IHQ5j*$yv`V`P zUyA|p6hSDiO&a%oubWHJrc~LNt6h=r_cA&>wH>q(>Fe(#?Sl;mY&d{Le`wL0Q_=l3c zkG|-x%89E@zF^QQY9kP+yjmlu9-dLQ>KYzP(?iw|6a=zc&%*tjutnk ze*s7It)zVKf?D$D!b)SQDa#7Ke;Ldd*aei20YdiI=u2s<3j+hW&6S7gB`6rFU zK1>YW?ZcELfsSrf8m(y`h4fB1^9aIh6Xaj>3gTm#ONFxATW`!b!L7a*4{ObnxLhNR zICa|Pcda{B`{6~R-XIkF;UaQtZrcdf5T(2<4uQ8Hie*Ahb|_Kqe%G#Ped_VDe9F$? zu|)})W^l44gD+7_Na?ZK5;H#2Df-;MQ?x1$X)}Df(DdeRV(jg?z$sW!l5AUa#m-Da z*j%#WJ1eg1WjpZI+ijayAZPV?PZz_lDK_$`ism&cCx8UHnINs}&30Y06=8imL64_B zZ&Il{F=wdmMu;g1a!%ek;G&L8!`*JC6i0uZcCFxj1M>titOQ4pYS?<}5pVhOd2jsC zC9U=FuZ?W{*50qYuF<kUA?^nV~K-hZ}!=cZCC`(7~;yTpKX8MAKiS%3PuGM>?1O2gTOT^ zFnKr!mPhurU$SF4o9m%m<#8gp-osB$`i$l|GQWj>J%b8*n-<-snlbrlOeKD-p?2xc zuJC7imPwbwwL!@l5}Pq>$rR}>JAIes zu5>{p0c;>my54xsdC&K)yUx0Q+_mohV`lF?&+~iw%rmnl`HGbZE0X}zsZ*y|%}n7o zr%s*rpw06c>1apob?IZ;i63X=gtNt(3lXE#9952*uiRIvnE zhQLt1ex{LFlx?Jyop)q_w+0fRrz@%*p-BS>Lg5gi5kY~$VVV&-fPdg>(&m4*l>nmu zAi)Lb0RE|zljRjr7zT?HRZ#>fc!N~IqN-3uFchQ#)sPp3fWROnFhmKYrT_+Ms;Fs# zz@q;e0Gc!`60KZ<5nh;ZoDP7d=|4>f3bC~OH*j#+zZ^w#nNkEI zLfP+^#GtT!zlXz{<9X}rh9C_CFjP%N846c} z8UKy(pLn4VsEWF>ssT*hfQDfNHZ+2P)iug68(pVHU0jB2LIi1L$JC5OxX}*pkb(@4uiq~|KdgdZ@i{NQ%31edi;Os^7j@k z2meg}o&2=Lzsm;|Osg3ztvFN@ECNrR5_)b1H?WJC{a$+Wp56P>jls?p4-6XH9BDa8 z7P*0z2yb40aiiW$(zZvJ^$zZGpniH5XI>)RQ;ySA=f_!UIpUo9bWA{5_C{`KAD;~C z9X>Emf0}akeWooA4r%zMirjm~ERj5VdzqTihuYOv_Ks?$)L)aHQ1{LlpN)-!?~A`) zUI$h@{68C`p-Gb29L8!{07VEu$;;n}``Ltq?57?Me5}6cC*Gun^N6Gp>H9VzOP=EK z>*mfdN|2r3ZyXqJ)MvtAayztSV$H=&Z1JBhE*pU@7P;V(FtSq7EA2Z zx0tBLVHxY^1BKz3NWfBgNY_zdS_<(4lTM=Vh8b`Q66+j@ma0C`U`TcD2|CVtH2Roda-WSZ#Jorvl#e-+_bz|fZ$>szf=Yh-U zhn2aKMze=&1k=Ve)NxAx^U|>$1DKX<;ydu?P0&@@3t?kwj|Zv;0;W2mTlM@~AQ`Ok z-pn#M9L{FV!dqyWzh7Ol$Zm3Iq<6kw$rOAK-7-QUV?5k`w>V)<6FHVJtFM2mUjh`SdK~tX>F8KP+b#FPA(n10!l;qa~ z>H!!aXEL0EJut)cH5K+dp~tVEfvuT%1_NLvW8_s;Xs~`|VX|9XpT2P+uxw~#nha&A zC_lNmy*LH^UJfg+!`*-K2L)bSH&zyUoO81fRYc$W*)oP9gvYN+^fO5n3areMM?%ay zO3*GH3CWVNytzKIQ-HAtCw+^PpzjBx6idy4Vmr#oe6C~0*xC@?twENF{L8Jqa152c}4)c~Z?JH|1_pak$Yq>MKoNoB_nh80mB3Nd0{Hmy`n|3KisO~8lJ ziacQNyxi8UI?|E#@7jZwKFh#s5}KN}d}#?tHU_yh(xWuw8O^Wz;LINaBe|>2E16G6 z%6Ctby(HvF4JEt>^dpVMQqGuPf#=2jjK;ar=L$*s!4m1i(KRagH8o!HKwxN$)^WeGmn}Svgl7FhBX(8H1oWyJ)l|EcsrM9%{Hk?OZK7FzAPM)@Y zK9&4QrEMFU87fsh{aO|xzoOV=v3glDl$kRfu|^o1>E(7A)lEJLlFCslA2{lez+{@Y zZihNFT@)^T%Hky;AP|?VoXyQV>P(JYf#j;lBe|G6QZ4!B(}ypw`=}}x_TeP4GSPf} zfegw*!lnFy2s#0mDz2e~X9uhmBr=QDbqDRb<^j1+02!8S&dmmAq1Np7pPQYpl}>%^df$S6^VbN0?%pmoc|e?7Dgm@lHsdZhA?)TlT33z89YGJ zaRyzN>UjH!hxdu^BWr;S;!?%bG~*KnRUswASBG8RX0&@DcN^RtuHeiB5XZJ*@}D{H zBL-YUkME^tmpn2BdwtK1_7O_ZK*%v%k7@vcGP zo}9>Y2;M!fgBqVX317H}@sB3uc9cL-fuRz7Nx1Z8M`@R@Nc72II-owU$d<9ZIQPN#+H1wL&+9$jYLeuN&LoxA zkF11K6s68+PdjIhWwUc})*CM{Ezhk=_!ZaNNQ^r;3DcF%V!y=1Ts?ZuE{JCmF8%ts zw=xhdKUH4Y99!0uE?lw2n}o5o2@#`@AG@CJQ5w4d_gwi__-J@F8$gl9&eeDk*5|O0mp9d zH(3{ZL!YmHEt>Yptd^K+x~nx-XpB*wx#}&X=cTf3x%I0MV50s?W?}VgiqKN{!fIwo z)quxO>}%kL_DXXs$~Wp9i3do&ySvI42S*DUc%()eCoQ{`zvYYFVI6N#oG7=G!Z-io zZt@tk84dWXjzsxO)T0>bGRyWJVY;lgjJ5KJL&_4B({D=&{E3uv$G3sCHA{)A%}e4L z+qBmFK1`@;z|X2NQKw{yZevp{rh%Nmt*Pu=uD92aeR@8{s;EqvKY8BsES`&}H5gT0 zPa40s`D|9}T3A?i`}8d~UQ1QWlG;S##dvKai%B_@T|}E#-c%hYp836i!9nK6=*u^w zl<(h4xQ4Rk$-Qf9X~rh=~j~i57OC$&9y4_c`^yz!^EqXY)e@Z*1||etu(k$y=y)jR1D~OBAS* z)n2rm z^yk;xh-~a3J}C50{A`@PCq`nqo+UZN2yEXruR}p)#sfbpUh>h>Qt!Wz5q5v#xKO!- zV#&5GhOzm?jS0PdMhc&y=G>#H+Q3n2u!G<+a~U0T9=gognmy%ksOSZdX{v+S>NdcY zxs1CbaP@<_%_Poeua$-H3SSO|RDyPr-gCI`MB3gNC}s&rKFsTxec~BTx6PkhD&l_A z5R&?6jldU+n?maz$3r!5gepyqHDBE7i@vD)SyPdEA~{w>aeM#J7aVc6jpPMQAzln`<|*rP z8bg{g_EODQ%@+*&`))knYqG!5Wy5>7_@=g4;UF4kyWKW=*|V?V!9m;Ex&9I=cfb0Z zOKdh2_L7h4M>B(`t$qj`sSi3_-xdlO?T9R>TqZ7r7V()LIi2!tS=~ha zO*64yb)lt8B{P@VJiP16OkfQqxQwobE8> zPThOK-BLKKx?7Tz`W$putVuV$;VpJb2oaWE_QWSDu~;!8h7?XhD9}1FMQ6#vffF>+ z0;|Wof7-Da=!W!PXcqbiOw_PUzM($sTJjO*A$>vF$ z?zfVvn6ymA#(73rNKV(Yjir+NuCO?alc6|t!!S8$KS^1L3Ka>`G=c|oyp7EgJYjzC4M_A3^?36LN_`qCfF%r`ooO;5*L4fYfglY%foC+kXE!K z?l#pOEA=H;D~rg=-(8eB<5F1HaYg@2&V@ka1x=Yl(5O72L_bAp^gzQzV*u>o&5POA zrFkAc13644c)rXDP zH(>>0atCpy&(+!#B{7p~@l6Wnr5#16nsG#7fjNz@o3{%Zst@~5MpCIW3pJ}M$`DCE zDg@Z;6g!+5Q80Dj=&{7uxzBntq9814z>Jn)Mn6M}Jhz#;y+y3UZaZp>7)y?m0ZoDW z-e>$A8sff=jwYyP5HEcw4$p3X_lB~25bes>;bh56YdE({!Ck$oBlLW%?vD8qB6&dP zk)Smy3mvd}lrM4k!|~mqKyNJ*4KhAsQ|JhvytoDF*BEhV+u|N+D25T=#B5!fv#5_J z6r#63?y;nPPXGDJv1|0omZtmXc;*_sd$&@Nn@NM4)0QeZa6oM#@W~2@YbK zWt?wVGZ-c{BpQr9a|0B2k;P-_{DWn+<9yGMB5Ro~IggL11xon!t+En?oSwI!)(q)- z21KM(#_jSQmgu39kIpqHNX`TH_xiW5ycBs!O8yW}1Sm3{xrz;JD$E+;+(2$!#~m}V zDPMhdDJodH>(Ftw|K$E~H?6OQ4%K)wJ648r72}IMHvxE$PxiO^m8J);vW46}EA$EV zfXdVML+*N0uJ+M=T6a{3U|?^!gIHUWU){voc9B@6FkPH@tu|9(QOq#Z7g9l@zB5k% zV4mfYiobo_kL5>Kjd7&oBq-iFSLMaytWr)x#r@E10?W<`!`nh$0(+b1wPs$OM>pQ~ zBiw&uWwjMxv$E=e262;4U0RY4{bVp zbE(AwTYr}j`0d?6-?cA2zb|=ai|nDna${!7$+ub>@-%iT^_U~FcnHqsDH7j4?H#AJ zPxo3!h`!{1O zHHk!*a}5MQo51DX&|cMT-YU<>LXW}s@!GE~a8)7n0%nrUY{`8g#-n9RRk5I4t&W06 z^TtJB7BpieUlLMAivd zFR0qpk8t3$bl3-6HyAj2l-lxply4X|KZK)+5k85yy@3@E4SuN9%QvWARg`yOf}HTX zl0O5fE+7L`NJG-unw2-l+FMkp4Xq+aiq3QOjTHYF3=Y`=F<%f`6WEI6T#O!A21Q)n zoe(wcR`ri66CMWT8H-`dN0$R;L(@05p@U>*n>wS)JV9Rs@MXv3x9o+tpkRuz@%{6w zj>@+YJD(zfIRT?)*=T-tP=UIDOA}kqI{}0o!!dEacvisDBhxKecBih`BbK5fpxm|L zk;D0hIqIchyUZ1{f*Ck=EN%Is;^X*grylur_$uPoYd+P+l+CDOhhPM!_r7lKSVy`9 zu?Off=G(PerdXFtv|sS*|2*^IM0B}u@)_L@ou;6b2Yd8OorX1}b>kBG~w z9qHZuns7jfNBO`4G^4xn>F-*=uxr>u8VpXDkW;+Wgi&Rg1jlgoa#(|*?V@F2Rz z=|Qbp@FYLsx?>LA_*Bd>axKgRGVcB(UCBSJ<@e*Uw=`kW)|=eTL#Gl-do(q-J2Kx> z3Nwdqti{7`zUZGakl01HQVV-8fvrQKf@0OSFE87AzbYZrL9DAZq29A zi_Le^<-L?O^0xyndV})mhI|K3u-^(4pPA3C2$y-$>lCscJt%f?{b4F&61PU!onmc% zY1h#WJD5;(eK)IG3h}9Nq5jOg&=a&m@&nb*TSz{P4?+RelrsyF9jUw8ufEUs%1ds!GZcqonxy-Ge=tfv`U*fV0LOlsy$cJw zj!l21_9~T>XAO*s!6Eu|8Hv@`z<$|Z&H=0M0?CuwfYKs@ng{nA) zsW^rj&6w&}y!;SmS^w+zR(w*=DwF&4HG+83Nwcbv;ApYcQ|R^amG={hTVXUcUJyJ@xLM?%u;jIkK+}FiPo7#?1oK&tuCj zy-fV1?7C&{Q_*Ph@4Wy|&wjP$bViEH*xSRMI(%$c*Kv6BO!Q=AN!t;TYw~wKd=h{p zTR4GhJ`cD0NGCr5%}yM`$@8KRG_D(2>A1imbulV)EBJ>t^H@yF6Of)lC`U z9u&BNC{quI#?=<2?Bgnm(fkXF#zW*40{kix?uLYu3h7Z9G%gUlDmqX^majq@0+8YS z)h(5>aO%kc6n{gtrf>??eM=KHe8oBAPUS?bSV~U=!TR~HbJ;xnSGgs8iXH_-SLRJk ziG`gl0y}@U+MVm{2~nBAP4wea9VYW4R!!|yCsHaWd$o%9DM1DIj8A|Z30gOw>6p9~ z^PUfN#T3_Nx-P?oc{N<)C0-pG5&}n@erolV=FI)f@;w#K{Ika8!misD&OJ=klE>*e z##_#A#+_E-M31Ze0{fShm>!rvEB1Hx>`xNb_L|ws&SeGGn~VnDY--jLe?Re3#e1TS zf5Uz!PlCfTIHpYcP^B#-XKK?{{8|_UaNz=LQOFqO-M~7NgBJemD%i7@L7xKeX)k?z z!AZs~o5+^a`Do-QM{Q|hysm1u_2*r~e%VWb@41BW=bz=M1yl*I8fL4_KFC=mt+x-z zd9e+B1lD(UTzPuZGoeXxz13d{;PthCto=qSgGDNFWr$&#YCr!CAd9V9;s~O{2MJ6| zynJdIP2e>v>r98V*3^@d?wf{_LmH?LCA89s0RJ!Y%~a%WBG*BaK49r4FVcWw)b%pD zM8cr2?!fY z%pLm7o%3Tr&I-y^X)wa+jn&emn(WpJv&|QdRe|g%k9cCJtyF%iBjj~&p=(`k@b|l* z0RDN3LQ+~x0DCrQgdkBYOzR&i?Sqe*U<+-R)1vM>?|hIPv|b+@RmoACbEvX4I=sK^ zNsXFI8!37JqANkhpnSmtl_9A_QWUMvJIG~jti4BfD>YDAu;SLaHr3MaZa7;#$~S>( zhb@M=Io0zv!QJ|Q#VTBgI394^{BlpF>T%)KQZ$TsXGfxb2OxX-I{ioIMZ4#_tq&@% zc>b!bB(zqBs$?^ncvMpen)r0=<~64?&Xn{HSKp+F&N>eIVqm%?rsut!)EEB_=iR?n z9?uDPn!nm&;0tD*UOMaA6HZJ!oR{WRbDRxQefPWz;J1qnc=mYfl|8iRn>4=i=XaOa z+LR%>8Jk#2`eC=Nm7{a#$F{Ss<7S>0Fu4}c8KvRsOrkwTX>j)V%y5BpTD#aXApsQV z;8#=XMisBE{G{;L=@po8~oBL@SD5W!b9aqJl2!C{5p-q31mI;fU)5;vcItEKh#twwpMmjiZ>Jt! zoRe0nSm%iK^SMZ*-(mZldg*g&&`5A(Lu&p8v2T~BuxTjBwcTdGK^AN+c+V~F%X?Y+ zGdhOwQo+ik!xlo)fe_K{)ezHgfXS10#~!(`StlUC3vrTG@l>dfF}&7ye)Y>&r|B6# zc8Q~*l4u5_cS_ppEV`XOYSLM5&XFUjZW{1QVP8-ihiztxMHZZiF7(xJJES3bomZv* fuSb-}=I_%EX_!3L2qvXJe+QTuS;6ZJuig1CshN#! From f462cd6efa099b2bb7386b8ab2c8ad8d0086f2eb Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 20 Mar 2013 15:14:14 -0400 Subject: [PATCH 097/436] if we're loading a course module with depth = 0, then we don't need to fetch/compute inherited metadata --- common/lib/xmodule/xmodule/modulestore/mongo.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py index f6b703e806..1cbc053b62 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -376,7 +376,7 @@ class MongoModuleStore(ModuleStoreBase): return data - def _load_item(self, item, data_cache): + def _load_item(self, item, data_cache, depth=0): """ Load an XModuleDescriptor from item, using the children stored in data_cache """ @@ -388,7 +388,11 @@ class MongoModuleStore(ModuleStoreBase): resource_fs = OSFS(root) - metadata_inheritance_tree = self.get_cached_metadata_inheritance_tree(Location(item['location'])) + metadata_inheritance_tree = None + # if we are loading a course object, if we're not prefetching children (depth != 0) then don't + # bother with the metadata inheritence + if item['location']['category'] != 'course' or depth != 0: + 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 # the 'metadata_inheritance_tree' parameter @@ -410,7 +414,7 @@ class MongoModuleStore(ModuleStoreBase): """ data_cache = self._cache_children(items, depth) - return [self._load_item(item, data_cache) for item in items] + return [self._load_item(item, data_cache, depth) for item in items] def get_courses(self): ''' From 133bd767d5b42a9e0e4819a03a14e8c24b9ffd93 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 20 Mar 2013 15:26:29 -0400 Subject: [PATCH 098/436] small refactoring to use a better semantic with regards to the parameter --- common/lib/xmodule/xmodule/modulestore/mongo.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py index 1cbc053b62..ea3bdf44d0 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -376,7 +376,7 @@ class MongoModuleStore(ModuleStoreBase): return data - def _load_item(self, item, data_cache, depth=0): + 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 +389,8 @@ class MongoModuleStore(ModuleStoreBase): resource_fs = OSFS(root) metadata_inheritance_tree = None - # if we are loading a course object, if we're not prefetching children (depth != 0) then don't - # bother with the metadata inheritence - if item['location']['category'] != 'course' or depth != 0: + + 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 +413,10 @@ class MongoModuleStore(ModuleStoreBase): """ data_cache = self._cache_children(items, depth) - return [self._load_item(item, data_cache, depth) 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): ''' From 38df54dc53df10cd8a23705e888fdfc229cece16 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 20 Mar 2013 16:19:45 -0400 Subject: [PATCH 099/436] studio - moved handling scrollable and new window links into their own functions for use throughout the app and not just on doc.ready + removed unused js plugin reference --- cms/static/js/base.js | 33 ++++++++++++++++++++++----------- cms/templates/base.html | 1 - 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 18c31fc4e0..fa31f25906 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -45,9 +45,6 @@ $(document).ready(function () { (e).preventDefault(); }); - // smooth scrolling page links - $('a[rel*="view"]').smoothScroll({offset: -200, easing: 'swing', speed: 2000}); - // nav - dropdown related $body.click(function (e) { $('.nav-dropdown .nav-item .wrapper-nav-sub').removeClass('is-shown'); @@ -75,10 +72,7 @@ $(document).ready(function () { }); // general link management - new window/tab - $('a[rel="external"]').attr('title', 'This link will open in a new browser window/tab').click(function (e) { - window.open($(this).attr('href')); - e.preventDefault(); - }); + $('a[rel="external"]').attr('title', 'This link will open in a new browser window/tab').bind('click', linkNewWindow); // general link management - lean modal window $('a[rel="modal"]').attr('title', 'This link will open in a modal window').leanModal({overlay: 0.50, closeButton: '.action-modal-close' }); @@ -86,6 +80,10 @@ $(document).ready(function () { (e).preventDefault(); }); + // general link management - smooth scrolling page links + $('a[rel*="view"]').bind('click', linkSmoothScroll); + + // toggling overview section details $(function () { if ($('.courseware-section').length > 0) { @@ -155,10 +153,23 @@ $(document).ready(function () { }); }); -// function collapseAll(e) { -// $('.branch').addClass('collapsed'); -// $('.expand-collapse-icon').removeClass('collapse').addClass('expand'); -// } +function linkSmoothScroll(e) { + (e).preventDefault(); + + $.smoothScroll({ + offset: -200, + easing: 'swing', + speed: 1000, + scrollElement: null, + scrollTarget: $(this).attr('href') + }); + console.log('clicked!'); +} + +function linkNewWindow(e) { + window.open($(this).attr('href')); + e.preventDefault(); +} function toggleSections(e) { e.preventDefault(); diff --git a/cms/templates/base.html b/cms/templates/base.html index 6ce0de4b11..e852b5d7fe 100644 --- a/cms/templates/base.html +++ b/cms/templates/base.html @@ -46,7 +46,6 @@ - From ccd4db9ac3ed5f174fcb824a47920fd928ddf2cc Mon Sep 17 00:00:00 2001 From: cahrens Date: Mon, 25 Mar 2013 15:24:53 -0400 Subject: [PATCH 152/436] Writing test. --- cms/static/js/views/validating_view.js | 28 +++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/cms/static/js/views/validating_view.js b/cms/static/js/views/validating_view.js index c3ea57fd20..3376e5fe9b 100644 --- a/cms/static/js/views/validating_view.js +++ b/cms/static/js/views/validating_view.js @@ -25,14 +25,7 @@ CMS.Views.ValidatingView = Backbone.View.extend({ for (var field in error) { var ele = this.$el.find('#' + this.fieldToSelectorMap[field]); this._cacheValidationErrors.push(ele); - var inputElements = 'input, textarea'; - if ($(ele).is(inputElements)) { - $(ele).addClass('error'); - } - else { - // put error on the contained inputs - $(ele).find(inputElements).addClass('error'); - } + this.getInputElements(ele).addClass('error'); $(ele).parent().append(this.errorTemplate({message : error[field]})); } }, @@ -40,12 +33,8 @@ CMS.Views.ValidatingView = Backbone.View.extend({ clearValidationErrors : function() { // error is object w/ fields and error strings while (this._cacheValidationErrors.length > 0) { - var ele = this._cacheValidationErrors.pop(); - if ($(ele).is('div')) { - // put error on the contained inputs - $(ele).find('input, textarea').removeClass('error'); - } - else $(ele).removeClass('error'); + var ele = this._cacheValidationErrors.pop(); + this.getInputElements(ele).removeClass('error'); $(ele).nextAll('.message-error').remove(); } }, @@ -68,5 +57,16 @@ CMS.Views.ValidatingView = Backbone.View.extend({ }, inputUnfocus : function(event) { $("label[for='" + event.currentTarget.id + "']").removeClass("is-focused"); + }, + + getInputElements: function(ele) { + var inputElements = 'input, textarea'; + if ($(ele).is(inputElements)) { + return $(ele); + } + else { + // put error on the contained inputs + return $(ele).find(inputElements); + } } }); From 7dcb1bf7c64f7c9ea2de847c09be3c63e3a6da12 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Mon, 25 Mar 2013 16:09:13 -0400 Subject: [PATCH 153/436] it appears we are taking one too many round trips to do when pre-fetching children. This can be very expensive as the tree gets wider the deeper we go. For example, in courseware we want depth=2 (course, chapter, sequential). But looking at log output we were also getting verticals, which there can be a lot of. This should cut down on the total data we are grabbing from the DB. --- common/lib/xmodule/xmodule/modulestore/mongo.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py index f6fa98fc28..b76251bb99 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -366,6 +366,9 @@ class MongoModuleStore(ModuleStoreBase): children.extend(item.get('definition', {}).get('children', [])) data[Location(item['location'])] = item + if depth == 0: + break; + # Load all children by id. See # http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%24or # for or-query syntax From c660229b25f4ef1f9b922c8b325e60cbcb35bcb3 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Mon, 25 Mar 2013 16:37:59 -0400 Subject: [PATCH 154/436] Added checking for problem answer state after a problem is checked --- .../courseware/features/problems.feature | 4 + .../courseware/features/problems.py | 92 ++++++++++++++++++- 2 files changed, 91 insertions(+), 5 deletions(-) diff --git a/lms/djangoapps/courseware/features/problems.feature b/lms/djangoapps/courseware/features/problems.feature index efeb338c45..dc8495af60 100644 --- a/lms/djangoapps/courseware/features/problems.feature +++ b/lms/djangoapps/courseware/features/problems.feature @@ -8,6 +8,7 @@ Feature: Answer problems And I am viewing a "" problem When I answer a "" problem "correctly" Then My "" answer is marked "correct" + And The "" problem displays a "correct" answer Examples: | ProblemType | @@ -25,6 +26,7 @@ Feature: Answer problems And I am viewing a "" problem When I answer a "" problem "incorrectly" Then My "" answer is marked "incorrect" + And The "" problem displays a "incorrect" answer Examples: | ProblemType | @@ -41,6 +43,7 @@ Feature: Answer problems Given I am viewing a "" problem When I check a problem Then My "" answer is marked "incorrect" + And The "" problem displays a "blank" answer Examples: | ProblemType | @@ -58,6 +61,7 @@ Feature: Answer problems And I answer a "" problem "ly" When I reset the problem Then My "" answer is marked "unanswered" + And The "" problem displays a "blank" answer Examples: | ProblemType | Correctness | diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index 6b2239c38b..36a0477988 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -2,8 +2,8 @@ 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 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, \ @@ -26,7 +26,7 @@ PROBLEM_FACTORY_DICT = { 'kwargs': { 'question_text': 'The correct answer is Choice 3', 'choices': [False, False, True, False], - 'choice_names': ['choice_1', 'choice_2', 'choice_3', 'choice_4']}}, + 'choice_names': ['choice_0', 'choice_1', 'choice_2', 'choice_3']}}, 'checkbox': { 'factory': ChoiceResponseXMLFactory(), @@ -152,9 +152,9 @@ def answer_problem(step, problem_type, correctness): elif problem_type == "multiple choice": if correctness == 'correct': - inputfield('multiple choice', choice='choice_3').check() - else: inputfield('multiple choice', choice='choice_2').check() + else: + inputfield('multiple choice', choice='choice_1').check() elif problem_type == "checkbox": if correctness == 'correct': @@ -202,6 +202,65 @@ def answer_problem(step, problem_type, correctness): # Submit the problem check_problem(step) +@step(u'The "([^"]*)" problem displays a "([^"]*)" answer') +def assert_problem_has_answer(step, problem_type, answer_class): + ''' + Assert that the problem is displaying a particular answer. + These correspond to the same correct/incorrect + answers we set in answer_problem() + + We can also check that a problem has been left blank + by setting answer_class='blank' + ''' + assert answer_class in ['correct', 'incorrect', 'blank'] + + if problem_type == "drop down": + if answer_class == 'blank': + assert world.browser.is_element_not_present_by_css('option[selected="true"]') + else: + actual = world.browser.find_by_css('option[selected="true"]').value + expected = 'Option 2' if answer_class == 'correct' else 'Option 3' + assert actual == expected + + elif problem_type == "multiple choice": + if answer_class == 'correct': + assert_checked('multiple choice', ['choice_2']) + elif answer_class == 'incorrect': + assert_checked('multiple choice', ['choice_1']) + else: + assert_checked('multiple choice', []) + + elif problem_type == "checkbox": + if answer_class == 'correct': + assert_checked('checkbox', ['choice_0', 'choice_2']) + elif answer_class == 'incorrect': + assert_checked('checkbox', ['choice_3']) + else: + assert_checked('checkbox', []) + + elif problem_type == 'string': + if answer_class == 'blank': + expected = '' + else: + expected = 'correct string' if answer_class == 'correct' else 'incorrect' + + assert_textfield('string', expected) + + elif problem_type == 'formula': + if answer_class == 'blank': + expected = '' + else: + expected = "x^2+2*x+y" if answer_class == 'correct' else 'x^2' + + assert_textfield('formula', expected) + + else: + # The other response types use random data, + # which would be difficult to check + # We trade input value coverage in the other tests for + # input type coverage in this test. + pass + @step(u'I check a problem') def check_problem(step): @@ -274,6 +333,7 @@ def assert_answer_mark(step, problem_type, correctness): # Expect that we found the expected selector assert(has_expected) + def inputfield(problem_type, choice=None, input_num=1): """ Return the element for *problem_type*. For example, if problem_type is 'string', return @@ -289,8 +349,30 @@ def inputfield(problem_type, choice=None, input_num=1): base = "_choice_" if problem_type == "multiple choice" else "_" sel = sel + base + str(choice) + # If the input element doesn't exist, fail immediately assert(world.browser.is_element_present_by_css(sel, wait_time=4)) # Retrieve the input element return world.browser.find_by_css(sel) + +def assert_checked(problem_type, choices): + ''' + Assert that choice names given in *choices* are the only + ones checked. + + Works for both radio and checkbox problems + ''' + + all_choices = ['choice_0', 'choice_1', 'choice_2', 'choice_3'] + for ch in all_choices: + el = inputfield(problem_type, choice=ch) + + if ch in choices: + assert el.checked + else: + assert not el.checked + +def assert_textfield(problem_type, expected_text, input_num=1): + el = inputfield(problem_type, input_num=input_num) + assert el.value == expected_text From 37e7d68cef72174e33bdb4d2dab06b881ab69c9f Mon Sep 17 00:00:00 2001 From: Will Daly Date: Mon, 25 Mar 2013 16:46:31 -0400 Subject: [PATCH 155/436] pep8 and pylint fixes --- .../courseware/features/problems.py | 55 ++++++++++++------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index 36a0477988..d2d379a212 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -1,3 +1,8 @@ +''' +Steps for problem.feature lettuce tests +''' + + from lettuce import world, step from lettuce.django import django_url import random @@ -88,6 +93,9 @@ PROBLEM_FACTORY_DICT = { def add_problem_to_course(course, problem_type): + ''' + Add a problem to the course we have created using factories. + ''' assert(problem_type in PROBLEM_FACTORY_DICT) @@ -98,11 +106,12 @@ def add_problem_to_course(course, problem_type): # 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'}) + template_name = "i4x://edx/templates/problem/Blank_Common_Problem" + world.ItemFactory.create(parent_location=section_location(course), + template=template_name, + display_name=str(problem_type), + data=problem_xml, + metadata={'rerandomize': 'always'}) @step(u'I am viewing a "([^"]*)" problem') @@ -164,11 +173,13 @@ def answer_problem(step, problem_type, correctness): inputfield('checkbox', choice='choice_3').check() elif problem_type == 'string': - textvalue = 'correct string' if correctness == 'correct' else 'incorrect' + 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)) + textvalue = "pi + 1" if correctness == 'correct' \ + else str(random.randint(-2, 2)) inputfield('numerical').fill(textvalue) elif problem_type == 'formula': @@ -202,6 +213,7 @@ def answer_problem(step, problem_type, correctness): # Submit the problem check_problem(step) + @step(u'The "([^"]*)" problem displays a "([^"]*)" answer') def assert_problem_has_answer(step, problem_type, answer_class): ''' @@ -242,7 +254,8 @@ def assert_problem_has_answer(step, problem_type, answer_class): if answer_class == 'blank': expected = '' else: - expected = 'correct string' if answer_class == 'correct' else 'incorrect' + expected = 'correct string' if answer_class == 'correct' \ + else 'incorrect' assert_textfield('string', expected) @@ -286,7 +299,7 @@ CORRECTNESS_SELECTORS = { 'string': ['div.correct'], 'numerical': ['div.correct'], 'formula': ['div.correct'], - 'script': ['div.correct'], + 'script': ['div.correct'], 'code': ['span.correct']}, 'incorrect': {'drop down': ['span.incorrect'], @@ -306,12 +319,14 @@ CORRECTNESS_SELECTORS = { 'numerical': ['div.unanswered'], 'formula': ['div.unanswered'], 'script': ['div.unanswered'], - 'code': ['span.unanswered'] }} + 'code': ['span.unanswered']}} @step(u'My "([^"]*)" answer is marked "([^"]*)"') def assert_answer_mark(step, problem_type, correctness): - """ Assert that the expected answer mark is visible for a given problem type. + """ + Assert that the expected answer mark is visible + for a given problem type. *problem_type* is a string identifying the type of problem (e.g. 'drop down') *correctness* is in ['correct', 'incorrect', 'unanswered'] @@ -349,13 +364,14 @@ def inputfield(problem_type, choice=None, input_num=1): base = "_choice_" if problem_type == "multiple choice" else "_" sel = sel + base + str(choice) - + # If the input element doesn't exist, fail immediately assert(world.browser.is_element_present_by_css(sel, wait_time=4)) # Retrieve the input element return world.browser.find_by_css(sel) + def assert_checked(problem_type, choices): ''' Assert that choice names given in *choices* are the only @@ -365,14 +381,15 @@ def assert_checked(problem_type, choices): ''' all_choices = ['choice_0', 'choice_1', 'choice_2', 'choice_3'] - for ch in all_choices: - el = inputfield(problem_type, choice=ch) + for this_choice in all_choices: + element = inputfield(problem_type, choice=this_choice) - if ch in choices: - assert el.checked + if this_choice in choices: + assert element.checked else: - assert not el.checked + assert not element.checked + def assert_textfield(problem_type, expected_text, input_num=1): - el = inputfield(problem_type, input_num=input_num) - assert el.value == expected_text + element = inputfield(problem_type, input_num=input_num) + assert element.value == expected_text From fa63bcce949e13839bc457ccaa89433185a0a514 Mon Sep 17 00:00:00 2001 From: cahrens Date: Mon, 25 Mar 2013 17:25:49 -0400 Subject: [PATCH 156/436] Allow clearing of dates (except for course start date). https://edx.lighthouseapp.com/projects/102637/tickets/231 --- .../features/course-settings.feature | 28 +++ .../contentstore/features/course-settings.py | 163 ++++++++++++++++++ .../js/models/settings/course_details.js | 3 + .../js/views/settings/main_settings_view.js | 6 + 4 files changed, 200 insertions(+) create mode 100644 cms/djangoapps/contentstore/features/course-settings.feature create mode 100644 cms/djangoapps/contentstore/features/course-settings.py diff --git a/cms/djangoapps/contentstore/features/course-settings.feature b/cms/djangoapps/contentstore/features/course-settings.feature new file mode 100644 index 0000000000..1589e579d8 --- /dev/null +++ b/cms/djangoapps/contentstore/features/course-settings.feature @@ -0,0 +1,28 @@ +Feature: Course Settings + As a course author, I want to be able to configure my course settings. + + Scenario: User can set course dates + Given I have opened a new course in Studio + When I select Schedule and Details + And I set course dates + Then I see the set dates on refresh + + Scenario: User can clear previously set course dates (except start date) + Given I have set course dates + And I clear all the dates except start + Then I see cleared dates on refresh + + Scenario: User cannot clear the course start date + Given I have set course dates + And I clear the course start date + Then I receive a warning about course start date + And The previously set start date is shown on refresh + + Scenario: User can correct the course start date warning + Given I have tried to clear the course start + And I have entered a new course start date + Then The warning about course start date goes away + And My new course start date is shown on refresh + + + diff --git a/cms/djangoapps/contentstore/features/course-settings.py b/cms/djangoapps/contentstore/features/course-settings.py new file mode 100644 index 0000000000..a0c25045f2 --- /dev/null +++ b/cms/djangoapps/contentstore/features/course-settings.py @@ -0,0 +1,163 @@ +from lettuce import world, step +from common import * +from terrain.steps import reload_the_page +from selenium.webdriver.common.keys import Keys +import time + +from nose.tools import assert_true, assert_false, assert_equal + +COURSE_START_DATE_CSS = "#course-start-date" +COURSE_END_DATE_CSS = "#course-end-date" +ENROLLMENT_START_DATE_CSS = "#course-enrollment-start-date" +ENROLLMENT_END_DATE_CSS = "#course-enrollment-end-date" + +COURSE_START_TIME_CSS = "#course-start-time" +COURSE_END_TIME_CSS = "#course-end-time" +ENROLLMENT_START_TIME_CSS = "#course-enrollment-start-time" +ENROLLMENT_END_TIME_CSS = "#course-enrollment-end-time" + +DUMMY_TIME = "3:30pm" +DEFAULT_TIME = "12:00am" + + +############### ACTIONS #################### +@step('I select Schedule and Details$') +def test_i_select_schedule_and_details(step): + expand_icon_css = 'li.nav-course-settings i.icon-expand' + if world.browser.is_element_present_by_css(expand_icon_css): + css_click(expand_icon_css) + link_css = 'li.nav-course-settings-schedule a' + css_click(link_css) + + +@step('I have set course dates$') +def test_i_have_set_course_dates(step): + step.given('I have opened a new course in Studio') + step.given('I select Schedule and Details') + step.given('And I set course dates') + + +@step('And I set course dates$') +def test_and_i_set_course_dates(step): + set_date_or_time(COURSE_START_DATE_CSS, '12/20/2013') + set_date_or_time(COURSE_END_DATE_CSS, '12/26/2013') + set_date_or_time(ENROLLMENT_START_DATE_CSS, '12/1/2013') + set_date_or_time(ENROLLMENT_END_DATE_CSS, '12/10/2013') + + set_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME) + set_date_or_time(ENROLLMENT_END_TIME_CSS, DUMMY_TIME) + + pause() + + +@step('Then I see the set dates on refresh$') +def test_then_i_see_the_set_dates_on_refresh(step): + reload_the_page(step) + verify_date_or_time(COURSE_START_DATE_CSS, '12/20/2013') + verify_date_or_time(COURSE_END_DATE_CSS, '12/26/2013') + verify_date_or_time(ENROLLMENT_START_DATE_CSS, '12/01/2013') + verify_date_or_time(ENROLLMENT_END_DATE_CSS, '12/10/2013') + + verify_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME) + # Unset times get set to 12 AM once the corresponding date has been set. + verify_date_or_time(COURSE_END_TIME_CSS, DEFAULT_TIME) + verify_date_or_time(ENROLLMENT_START_TIME_CSS, DEFAULT_TIME) + verify_date_or_time(ENROLLMENT_END_TIME_CSS, DUMMY_TIME) + + +@step('And I clear all the dates except start$') +def test_and_i_clear_all_the_dates_except_start(step): + set_date_or_time(COURSE_END_DATE_CSS, '') + set_date_or_time(ENROLLMENT_START_DATE_CSS, '') + set_date_or_time(ENROLLMENT_END_DATE_CSS, '') + + pause() + + +@step('Then I see cleared dates on refresh$') +def test_then_i_see_cleared_dates_on_refresh(step): + reload_the_page(step) + verify_date_or_time(COURSE_END_DATE_CSS, '') + verify_date_or_time(ENROLLMENT_START_DATE_CSS, '') + verify_date_or_time(ENROLLMENT_END_DATE_CSS, '') + + verify_date_or_time(COURSE_END_TIME_CSS, '') + verify_date_or_time(ENROLLMENT_START_TIME_CSS, '') + verify_date_or_time(ENROLLMENT_END_TIME_CSS, '') + + # Verify course start date (required) and time still there + verify_date_or_time(COURSE_START_DATE_CSS, '12/20/2013') + verify_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME) + + +@step('I clear the course start date$') +def test_i_clear_the_course_start_date(step): + set_date_or_time(COURSE_START_DATE_CSS, '') + + +@step('I receive a warning about course start date$') +def test_i_receive_a_warning_about_course_start_date(step): + assert_css_with_text('.message-error', 'The course must have an assigned start date.') + assert_true('error' in css_find(COURSE_START_DATE_CSS).first._element.get_attribute('class')) + assert_true('error' in css_find(COURSE_START_TIME_CSS).first._element.get_attribute('class')) + + +@step('The previously set start date is shown on refresh$') +def test_the_previously_set_start_date_is_shown_on_refresh(step): + reload_the_page(step) + verify_date_or_time(COURSE_START_DATE_CSS, '12/20/2013') + verify_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME) + + +@step('Given I have tried to clear the course start$') +def test_i_have_tried_to_clear_the_course_start(step): + step.given("I have set course dates") + step.given("I clear the course start date") + step.given("I receive a warning about course start date") + + +@step('I have entered a new course start date$') +def test_i_have_entered_a_new_course_start_date(step): + set_date_or_time(COURSE_START_DATE_CSS, '12/22/2013') + pause() + + +@step('The warning about course start date goes away$') +def test_the_warning_about_course_start_date_goes_away(step): + assert_equal(0, len(css_find('.message-error'))) + assert_false('error' in css_find(COURSE_START_DATE_CSS).first._element.get_attribute('class')) + assert_false('error' in css_find(COURSE_START_TIME_CSS).first._element.get_attribute('class')) + + +@step('My new course start date is shown on refresh$') +def test_my_new_course_start_date_is_shown_on_refresh(step): + reload_the_page(step) + verify_date_or_time(COURSE_START_DATE_CSS, '12/22/2013') + # Time should have stayed from before attempt to clear date. + verify_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME) + + +############### HELPER METHODS #################### +def set_date_or_time(css, date_or_time): + """ + Sets date or time field. + """ + css_fill(css, date_or_time) + e = css_find(css).first + # hit Enter to apply the changes + e._element.send_keys(Keys.ENTER) + + +def verify_date_or_time(css, date_or_time): + """ + Verifies date or time field. + """ + assert_equal(date_or_time, css_find(css).first.value) + + +def pause(): + """ + Must sleep briefly to allow last time save to finish, + else refresh of browser will fail. + """ + time.sleep(float(1)) diff --git a/cms/static/js/models/settings/course_details.js b/cms/static/js/models/settings/course_details.js index 148df7a325..d41545cca9 100644 --- a/cms/static/js/models/settings/course_details.js +++ b/cms/static/js/models/settings/course_details.js @@ -37,6 +37,9 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({ // Returns either nothing (no return call) so that validate works or an object of {field: errorstring} pairs // A bit funny in that the video key validation is asynchronous; so, it won't stop the validation. var errors = {}; + if (newattrs.start_date === null) { + errors.start_date = "The course must have an assigned start date."; + } if (newattrs.start_date && newattrs.end_date && newattrs.start_date >= newattrs.end_date) { errors.end_date = "The course end date cannot be before the course start date."; } diff --git a/cms/static/js/views/settings/main_settings_view.js b/cms/static/js/views/settings/main_settings_view.js index 9bd8feab8c..3e1690f0b6 100644 --- a/cms/static/js/views/settings/main_settings_view.js +++ b/cms/static/js/views/settings/main_settings_view.js @@ -101,6 +101,12 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({ cacheModel.save(fieldName, newVal); } } + else { + // Clear date (note that this clears the time as well, as date and time are linked). + // Note also that the validation logic prevents us from clearing the start date + // (start date is required by the back end). + cacheModel.save(fieldName, null); + } }; // instrument as date and time pickers From ce884c44ee94fd6229fa2397496c698da0dd835e Mon Sep 17 00:00:00 2001 From: cahrens Date: Mon, 25 Mar 2013 17:26:59 -0400 Subject: [PATCH 157/436] Newline cleanup. --- cms/djangoapps/contentstore/features/course-settings.feature | 3 --- 1 file changed, 3 deletions(-) diff --git a/cms/djangoapps/contentstore/features/course-settings.feature b/cms/djangoapps/contentstore/features/course-settings.feature index 1589e579d8..e869bfe47a 100644 --- a/cms/djangoapps/contentstore/features/course-settings.feature +++ b/cms/djangoapps/contentstore/features/course-settings.feature @@ -23,6 +23,3 @@ Feature: Course Settings And I have entered a new course start date Then The warning about course start date goes away And My new course start date is shown on refresh - - - From d8f1c2b41a7b1f0af023f8dd75048f31d3df8569 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Mon, 25 Mar 2013 22:49:39 -0400 Subject: [PATCH 158/436] add unit test for proper depth build out --- .../contentstore/tests/test_contentstore.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 615ffb6ed0..5b080c7a83 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -205,7 +205,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): new_loc = descriptor.location._replace(org='MITx', course='999') print "Checking {0} should now also be at {1}".format(descriptor.location.url(), new_loc.url()) resp = self.client.get(reverse('edit_unit', kwargs={'location': new_loc.url()})) - self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.status_code, 200) def test_delete_course(self): import_from_xml(modulestore(), 'common/test/data/', ['full']) @@ -307,6 +307,21 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): # note, we know the link it should be because that's what in the 'full' course in the test data self.assertContains(resp, '/c4x/edX/full/asset/handouts_schematic_tutorial.pdf') + def test_prefetch_children(self): + import_from_xml(modulestore(), 'common/test/data/', ['full']) + module_store = modulestore('direct') + location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012') + + course = module_store.get_item(location, depth=2) + + # make sure we pre-fetched a known sequential which should be at depth=2 + self.assertTrue(Location(['i4x', 'edX', 'full', 'sequential', + 'Administrivia_and_Circuit_Elements', None]) in course.system.module_data) + + # make sure we don't have a specific vertical which should be at depth=3 + self.assertFalse(Location(['i4x', 'edX', 'full', 'vertical', 'vertical_58', + None]) in course.system.module_data) + def test_export_course_with_unknown_metadata(self): module_store = modulestore('direct') content_store = contentstore() From 269152c4f2072decad55615c3fc7f612cce28075 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Mon, 25 Mar 2013 23:15:35 -0400 Subject: [PATCH 159/436] add a test scenario to count RT to database when prefetching children. This uses a shim function on pymongo's collection.find to do the counting --- .../contentstore/tests/test_contentstore.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 5b080c7a83..edb20561bc 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -37,6 +37,14 @@ TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE) TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data') TEST_DATA_MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data') +class MongoCollectionFindWrapper(object): + def __init__(self, original): + self.original = original + self.counter = 0 + + def find(self, query, *args, **kwargs): + self.counter = self.counter+1 + return self.original(query, *args, **kwargs) @override_settings(MODULESTORE=TEST_DATA_MODULESTORE) class ContentStoreToyCourseTest(ModuleStoreTestCase): @@ -145,8 +153,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): # make sure the parent no longer points to the child object which was deleted self.assertFalse(sequential.location.url() in chapter.children) - - def test_about_overrides(self): ''' This test case verifies that a course can use specialized override for about data, e.g. /about/Fall_2012/effort.html @@ -312,8 +318,15 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): module_store = modulestore('direct') location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012') + wrapper = MongoCollectionFindWrapper(module_store.collection.find) + module_store.collection.find = wrapper.find course = module_store.get_item(location, depth=2) + # make sure we haven't done too many round trips to DB + # note we say 4 round trips here for 1) the course, 2 & 3) for the chapters and sequentials, and + # 4) because of the RT due to calculating the inherited metadata + self.assertEqual(wrapper.counter, 4) + # make sure we pre-fetched a known sequential which should be at depth=2 self.assertTrue(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None]) in course.system.module_data) From 175e1b46e99e73b7e1dd75008b88fd7a1bb87947 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Tue, 26 Mar 2013 08:32:49 -0400 Subject: [PATCH 160/436] studio - in progress styling of tender help widget --- cms/static/sass/_tender-widget.scss | 91 ----------------------------- cms/static/sass/base-style.scss | 1 + cms/templates/base.html | 1 - cms/templates/widgets/footer.html | 3 - cms/templates/widgets/tender.html | 4 +- 5 files changed, 3 insertions(+), 97 deletions(-) delete mode 100644 cms/static/sass/_tender-widget.scss diff --git a/cms/static/sass/_tender-widget.scss b/cms/static/sass/_tender-widget.scss deleted file mode 100644 index 422be36908..0000000000 --- a/cms/static/sass/_tender-widget.scss +++ /dev/null @@ -1,91 +0,0 @@ -// tender help/support widget -// ==================== - -#tender_frame, #tender_window { - background-image: none !important; -} - -#tender_frame { - @include border-radius(3px); - @include box-shadow(0 2px 3px $shadow); - border: 1px solid $gray; - background: $white; -} - -#tender_closer { - color: $blue-l2 !important; - margin-top: 15px; - margin-right: 5px; -} - -// ==================== - -// tender style overrides - not rendered through here, but an archive is needed -#tender_frame iframe html { - font-size: 62.5%; -} - -.widget-layout { - font-family: 'Open Sans', sans-serif; -} - -.widget-layout .search, -.widget-layout .tabs, -.widget-layout .header h1 a { - display: none; -} - -.widget-layout .header { - background: rgb(85, 151, 221); - padding: 20px; -} - -.widget-layout h1, .widget-layout h2, .widget-layout h3, .widget-layout h4, .widget-layout h5, .widget-layout h6, .widget-layout label { - font-weight: 600; -} - -.widget-layout .header h1 { - font-weight: 700; - font-size: 24px; - font-size: 2.4rem; -} - -.widget-layout .content { - padding: 20px; -} - -.widget-layout label { - font-size: 14px; - font-size: 1.4rem; - margin-bottom: 5px; - color: rgb(127,127,127) !important; -} - -.widget-layout input[type="text"], .widget-layout textarea { - padding: 10px; - font-size: 16px; - font-size: 1.6rem; - color: rgb(0,0,0) !important; -} - -.widget-layout textarea { - width: 97%; -} - -.widget-layout .form-actions { - border-top: 1px solid #ccc; - margin-top: 10px; - padding-top: 10px; -} - -.widget-layout dl.form { - margin-bottom: 15px; -} - -.widget-layout #brain_buster_captcha { - display: block; - width: 100%; - border-bottom: 1px solid #ccc; - margin-bottom: 10px; - padding-bottom: 10px; -} \ No newline at end of file diff --git a/cms/static/sass/base-style.scss b/cms/static/sass/base-style.scss index 6a1f1bb252..5c67789f72 100644 --- a/cms/static/sass/base-style.scss +++ b/cms/static/sass/base-style.scss @@ -28,6 +28,7 @@ @import 'elements/modal'; @import 'elements/alerts'; @import 'elements/jquery-ui-calendar'; +@import 'elements/tender-widget'; // specific views @import 'views/account'; diff --git a/cms/templates/base.html b/cms/templates/base.html index 45147e5783..fd2b96f03a 100644 --- a/cms/templates/base.html +++ b/cms/templates/base.html @@ -56,7 +56,6 @@ <%block name="content"> <%include file="widgets/footer.html" /> <%include file="widgets/tender.html" /> - <%block name="jsextra"> diff --git a/cms/templates/widgets/footer.html b/cms/templates/widgets/footer.html index e3063dafa7..8f0265cd86 100644 --- a/cms/templates/widgets/footer.html +++ b/cms/templates/widgets/footer.html @@ -17,9 +17,6 @@

    - diff --git a/cms/templates/widgets/tender.html b/cms/templates/widgets/tender.html index e2bba3d2ef..300b71701c 100644 --- a/cms/templates/widgets/tender.html +++ b/cms/templates/widgets/tender.html @@ -1,12 +1,12 @@ % if user.is_authenticated(): +Provide Feedback From 69c95ca785b60da37ea5e3aadadf177bef1f4c01 Mon Sep 17 00:00:00 2001 From: cahrens Date: Tue, 26 Mar 2013 09:51:24 -0400 Subject: [PATCH 161/436] Newline cleanup. --- cms/static/js/base.js | 1 + 1 file changed, 1 insertion(+) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 0521371b6a..f623607d14 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -303,6 +303,7 @@ function saveSubsection() { data: JSON.stringify({ 'id': id, 'metadata': metadata}), success: function () { $spinner.delay(500).fadeOut(150); + $changedInput = null; }, error: function () { showToastMessage('There has been an error while saving your changes.'); From df935d422d31fcf34489f8b0fa501a4ac627212a Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 26 Mar 2013 09:52:26 -0400 Subject: [PATCH 162/436] Fix issues with open ended image grading and peer grading centralized module finder. --- .../open_ended_grading_classes/openendedchild.py | 4 ---- lms/djangoapps/courseware/module_render.py | 10 +++------- lms/djangoapps/open_ended_grading/views.py | 2 +- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py index 2e49565bec..b9341f0cbe 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py @@ -357,10 +357,6 @@ class OpenEndedChild(object): if get_data['can_upload_files'] in ['true', '1']: has_file_to_upload = True file = get_data['student_file'][0] - if self.system.track_fuction: - self.system.track_function('open_ended_image_upload', {'filename': file.name}) - else: - log.info("No tracking function found when uploading image.") uploaded_to_s3, image_ok, s3_public_url = self.upload_image_to_s3(file) if uploaded_to_s3: image_tag = self.generate_image_tag_from_url(s3_public_url, file.name) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 973940d784..a1c09d3d83 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -208,9 +208,6 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours 'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS } - def get_or_default(key, default): - getattr(settings, key, default) - #This is a hacky way to pass settings to the combined open ended xmodule #It needs an S3 interface to upload images to S3 #It needs the open ended grading interface in order to get peer grading to be done @@ -226,12 +223,11 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours open_ended_grading_interface['mock_staff_grading'] = settings.MOCK_STAFF_GRADING if is_descriptor_combined_open_ended: s3_interface = { - 'access_key' : get_or_default('AWS_ACCESS_KEY_ID',''), - 'secret_access_key' : get_or_default('AWS_SECRET_ACCESS_KEY',''), - 'storage_bucket_name' : get_or_default('AWS_STORAGE_BUCKET_NAME','') + 'access_key' : getattr(settings,'AWS_ACCESS_KEY_ID',''), + 'secret_access_key' : getattr(settings,'AWS_SECRET_ACCESS_KEY',''), + 'storage_bucket_name' : getattr(settings,'AWS_STORAGE_BUCKET_NAME','') } - def inner_get_module(descriptor): """ Delegate to get_module. It does an access check, so may return None diff --git a/lms/djangoapps/open_ended_grading/views.py b/lms/djangoapps/open_ended_grading/views.py index 65cfe22ed0..78da00bf2b 100644 --- a/lms/djangoapps/open_ended_grading/views.py +++ b/lms/djangoapps/open_ended_grading/views.py @@ -111,7 +111,7 @@ def peer_grading(request, course_id): #Get the peer grading modules currently in the course items = modulestore().get_items(['i4x', None, course_id_parts[1], 'peergrading', None]) #See if any of the modules are centralized modules (ie display info from multiple problems) - items = [i for i in items if i.metadata.get("use_for_single_location", True) in false_dict] + items = [i for i in items if getattr(i,"use_for_single_location", True) in false_dict] #Get the first one item_location = items[0].location #Generate a url for the first module and redirect the user to it From d4615da555f77a15ba7c4f70d380f813f195a6f7 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 26 Mar 2013 09:57:52 -0400 Subject: [PATCH 163/436] Adjust max image dim, add in safety for rewriting links --- .../combined_open_ended_modulev1.py | 6 +++++- .../open_ended_image_submission.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py index 98a54601de..c7df87fd45 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py @@ -363,7 +363,11 @@ class CombinedOpenEndedV1Module(): """ self.update_task_states() html = self.current_task.get_html(self.system) - return_html = rewrite_links(html, self.rewrite_content_links) + return_html = html + try: + return_html = rewrite_links(html, self.rewrite_content_links) + except: + pass return return_html def get_current_attributes(self, task_number): diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_image_submission.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_image_submission.py index 6956f336a5..759645840f 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_image_submission.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_image_submission.py @@ -36,7 +36,7 @@ ALLOWABLE_IMAGE_SUFFIXES = [ ] #Maximum allowed dimensions (x and y) for an uploaded image -MAX_ALLOWED_IMAGE_DIM = 1500 +MAX_ALLOWED_IMAGE_DIM = 2000 #Dimensions to which image is resized before it is evaluated for color count, etc MAX_IMAGE_DIM = 150 From 8afe2eb001a925bd49e9e5fb9678c3572e47ad0e Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 26 Mar 2013 10:35:47 -0400 Subject: [PATCH 164/436] Increase max score allowed --- .../open_ended_grading_classes/combined_open_ended_modulev1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py index c7df87fd45..f88fd9ab82 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py @@ -24,7 +24,7 @@ MAX_ATTEMPTS = 1 MAX_SCORE = 1 #The highest score allowed for the overall xmodule and for each rubric point -MAX_SCORE_ALLOWED = 3 +MAX_SCORE_ALLOWED = 50 #If true, default behavior is to score module as a practice problem. Otherwise, no grade at all is shown in progress #Metadata overrides this. From f681d4300d7c9226eed57ee117169126598f9d42 Mon Sep 17 00:00:00 2001 From: cahrens Date: Tue, 26 Mar 2013 10:42:44 -0400 Subject: [PATCH 165/436] More cleanup in base.js. --- cms/static/js/base.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index f623607d14..bd8dc0bae8 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -236,7 +236,7 @@ function getEdxTimeFromDateTimeVals(date_val, time_val, format) { time_val = '00:00'; // Note, we are using date.js utility which has better parsing abilities than the built in JS date parsing - date = Date.parse(date_val + " " + time_val); + var date = Date.parse(date_val + " " + time_val); if (format == null) format = 'yyyy-MM-ddTHH:mm'; @@ -254,6 +254,7 @@ function getEdxTimeFromDateTimeInputs(date_id, time_id, format) { } function autosaveInput(e) { + var self = this; if (this.saveTimer) { clearTimeout(this.saveTimer); } @@ -261,7 +262,7 @@ function autosaveInput(e) { this.saveTimer = setTimeout(function () { $changedInput = $(e.target); saveSubsection(); - this.saveTimer = null; + self.saveTimer = null; }, 500); } From 97cb4910a7b8d36123941538776a1d53ec4be034 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 26 Mar 2013 11:04:14 -0400 Subject: [PATCH 166/436] Add in default bucket, edit image url checks --- .../open_ended_grading_classes/open_ended_image_submission.py | 2 +- lms/djangoapps/courseware/module_render.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_image_submission.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_image_submission.py index 759645840f..2eb9502269 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_image_submission.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_image_submission.py @@ -178,7 +178,7 @@ class URLProperties(object): Runs all available url tests @return: True if URL passes tests, false if not. """ - url_is_okay = self.check_suffix() and self.check_if_parses() and self.check_domain() + url_is_okay = self.check_suffix() and self.check_if_parses() return url_is_okay def check_domain(self): diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index a1c09d3d83..15f95f1beb 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -225,7 +225,7 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours s3_interface = { 'access_key' : getattr(settings,'AWS_ACCESS_KEY_ID',''), 'secret_access_key' : getattr(settings,'AWS_SECRET_ACCESS_KEY',''), - 'storage_bucket_name' : getattr(settings,'AWS_STORAGE_BUCKET_NAME','') + 'storage_bucket_name' : getattr(settings,'AWS_STORAGE_BUCKET_NAME','openended') } def inner_get_module(descriptor): From 87f545329a6f9f75fed6cdc16502a23e124a9ebe Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Tue, 26 Mar 2013 11:05:33 -0400 Subject: [PATCH 167/436] studio - adding in tender-widget sass file --- cms/static/sass/elements/_tender-widget.scss | 142 +++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 cms/static/sass/elements/_tender-widget.scss diff --git a/cms/static/sass/elements/_tender-widget.scss b/cms/static/sass/elements/_tender-widget.scss new file mode 100644 index 0000000000..fce62b8675 --- /dev/null +++ b/cms/static/sass/elements/_tender-widget.scss @@ -0,0 +1,142 @@ +// tender help/support widget +// ==================== + +#tender_frame, #tender_window { + background-image: none !important; + background: none; +} + +#tender_window { + @include border-radius(3px); + @include box-shadow(0 2px 3px $shadow); + background: $white !important; + border: 1px solid $gray; +} + +#tender_window { + padding: 0 !important; +} + +#tender_frame { + background: $white; +} + +#tender_closer { + color: $blue-l2 !important; + margin-top: 15px; + margin-right: 5px; + text-transform: uppercase; + + &:hover { + color: $blue-l4 !important; + } +} + +// ==================== + +// tender style overrides - not rendered through here, but an archive is needed +#tender_frame iframe html { + font-size: 62.5%; +} + +.widget-layout { + font-family: 'Open Sans', sans-serif; +} + +.widget-layout .search, +.widget-layout .tabs, +.widget-layout .footer, +.widget-layout .header h1 a { + display: none; +} + +.widget-layout .header { + background: rgb(85, 151, 221); + padding: 20px; +} + +.widget-layout h1, .widget-layout h2, .widget-layout h3, .widget-layout h4, .widget-layout h5, .widget-layout h6, .widget-layout label { + font-weight: 600; +} + +.widget-layout .header h1 { + font-weight: 500; + font-size: 24px; +} + +.widget-layout .content { + overflow: auto; + padding: 20px; +} + +.widget-layout label { + font-size: 14px; + margin-bottom: 5px; + color: #4c4c4c; + font-weight: 500; +} + +.widget-layout input[type="text"], .widget-layout textarea { + padding: 10px; + font-size: 16px; + color: rgb(0,0,0) !important; + border: 1px solid #b0b6c2; + border-radius: 2px; + background-color: #edf1f5; + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #edf1f5),color-stop(100%, #fdfdfe)); + background-image: -webkit-linear-gradient(top, #edf1f5,#fdfdfe); + background-image: -moz-linear-gradient(top, #edf1f5,#fdfdfe); + background-image: -ms-linear-gradient(top, #edf1f5,#fdfdfe); + background-image: -o-linear-gradient(top, #edf1f5,#fdfdfe); + background-image: linear-gradient(top, #edf1f5,#fdfdfe); + background-color: #edf1f5; + -webkit-box-shadow: 0 1px 2px rgba(0,0,0,0.1) inset; + -moz-box-shadow: 0 1px 2px rgba(0,0,0,0.1) inset; + box-shadow: 0 1px 2px rgba(0,0,0,0.1) inset; +} + +.widget-layout input[type="text"]:focus, .widget-layout textarea:focus { + background-color: #fffcf1; + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fffcf1),color-stop(100%, #fffefd)); + background-image: -webkit-linear-gradient(top, #fffcf1,#fffefd); + background-image: -moz-linear-gradient(top, #fffcf1,#fffefd); + background-image: -ms-linear-gradient(top, #fffcf1,#fffefd); + background-image: -o-linear-gradient(top, #fffcf1,#fffefd); + background-image: linear-gradient(top, #fffcf1,#fffefd); + outline: 0; +} + +.widget-layout textarea { + width: 97%; +} + +.widget-layout .form-actions { + border-top: 1px solid #ccc; + margin-top: 10px; + padding-top: 10px; +} + +.widget-layout dl.form { + float: none; + width: 100%; + border-bottom: 1px solid #f2f2f2; + margin-bottom: 10px; + padding-bottom: 10px; +} + +.widget-layout #brain_buster_captcha { + +} + +// specific elements +.widget-layout #discussion_body { + margin: 0 0 15px 0; +} + +.widget-layout .category dt, .widget-layout .category dd { + display: inline-block !important; +} + +.widget-layout .category dt { + margin-right: 15px !important; +} \ No newline at end of file From 24301d2a0761510143f7bc62bc9d7d0d01abd5ca Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 22 Mar 2013 11:30:31 -0400 Subject: [PATCH 168/436] Moved helper functions from terrain/steps.py to terrain/helpers.py --- common/djangoapps/terrain/helpers.py | 152 +++++++++++++++++++++++++ common/djangoapps/terrain/steps.py | 164 ++------------------------- 2 files changed, 159 insertions(+), 157 deletions(-) create mode 100644 common/djangoapps/terrain/helpers.py diff --git a/common/djangoapps/terrain/helpers.py b/common/djangoapps/terrain/helpers.py new file mode 100644 index 0000000000..55c8f3db5a --- /dev/null +++ b/common/djangoapps/terrain/helpers.py @@ -0,0 +1,152 @@ +from lettuce import world, step +from .factories import * +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 bs4 import BeautifulSoup +import os.path +from selenium.common.exceptions import WebDriverException +from urllib import quote_plus +from lettuce.django import django_url + +@world.absorb +def wait(seconds): + time.sleep(float(seconds)) + +@world.absorb +def scroll_to_bottom(): + # Maximize the browser + world.browser.execute_script("window.scrollTo(0, screen.height);") + + +@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 = world.RegistrationFactory(user=portal_user) + registration.register(portal_user) + registration.activate() + + user_profile = world.UserProfileFactory(user=portal_user) + + +@world.absorb +def log_in(username, password): + ''' + Log the user in programatically + ''' + + # 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 +def register_by_course_id(course_id, is_staff=False): + create_user('robot') + u = User.objects.get(username='robot') + if is_staff: + u.is_staff = True + u.save() + CourseEnrollment.objects.get_or_create(user=u, course_id=course_id) + + +@world.absorb +def save_the_html(path='/tmp'): + u = world.browser.url + html = world.browser.html.encode('ascii', 'ignore') + filename = '%s.html' % quote_plus(u) + f = open('%s/%s' % (path, filename), 'w') + f.write(html) + f.close + + +@world.absorb +def save_the_course_content(path='/tmp'): + html = world.browser.html.encode('ascii', 'ignore') + soup = BeautifulSoup(html) + + # get rid of the header, we only want to compare the body + soup.head.decompose() + + # for now, remove the data-id attributes, because they are + # causing mismatches between cms-master and master + for item in soup.find_all(attrs={'data-id': re.compile('.*')}): + del item['data-id'] + + # we also need to remove them from unrendered problems, + # where they are contained in the text of divs instead of + # in attributes of tags + # Be careful of whether or not it was the last attribute + # and needs a trailing space + for item in soup.find_all(text=re.compile(' data-id=".*?" ')): + s = unicode(item.string) + item.string.replace_with(re.sub(' data-id=".*?" ', ' ', s)) + + for item in soup.find_all(text=re.compile(' data-id=".*?"')): + s = unicode(item.string) + item.string.replace_with(re.sub(' data-id=".*?"', ' ', s)) + + # prettify the html so it will compare better, with + # each HTML tag on its own line + output = soup.prettify() + + # use string slicing to grab everything after 'courseware/' in the URL + u = world.browser.url + section_url = u[u.find('courseware/') + 11:] + + + if not os.path.exists(path): + os.makedirs(path) + + filename = '%s.html' % (quote_plus(section_url)) + 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/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index 3bc838a6af..ae36227fee 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -1,20 +1,8 @@ from lettuce import world, step -from .factories import * +from .helpers 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 -from bs4 import BeautifulSoup import time -import re -import os.path -from selenium.common.exceptions import WebDriverException from logging import getLogger logger = getLogger(__name__) @@ -22,8 +10,7 @@ logger = getLogger(__name__) @step(u'I wait (?:for )?"(\d+)" seconds?$') def wait(step, seconds): - time.sleep(float(seconds)) - + world.wait(seconds) @step('I reload the page$') def reload_the_page(step): @@ -87,8 +74,8 @@ def the_page_title_should_contain(step, title): @step('I am a logged in user$') def i_am_logged_in_user(step): - create_user('robot') - log_in('robot', 'test') + world.create_user('robot') + world.log_in('robot', 'test') @step('I am not logged in$') @@ -98,151 +85,14 @@ def i_am_not_logged_in(step): @step('I am staff for course "([^"]*)"$') def i_am_staff_for_course_by_id(step, course_id): - register_by_course_id(course_id, True) + world.register_by_course_id(course_id, True) @step('I log in$') def i_log_in(step): - log_in('robot', 'test') + world.log_in('robot', 'test') @step(u'I am an edX user$') def i_am_an_edx_user(step): - create_user('robot') - -#### helper functions - - -@world.absorb -def scroll_to_bottom(): - # Maximize the browser - world.browser.execute_script("window.scrollTo(0, screen.height);") - - -@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 = world.RegistrationFactory(user=portal_user) - registration.register(portal_user) - registration.activate() - - user_profile = world.UserProfileFactory(user=portal_user) - - -@world.absorb -def log_in(username, password): - ''' - Log the user in programatically - ''' - - # 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 -def register_by_course_id(course_id, is_staff=False): - create_user('robot') - u = User.objects.get(username='robot') - if is_staff: - u.is_staff = True - u.save() - CourseEnrollment.objects.get_or_create(user=u, course_id=course_id) - - -@world.absorb -def save_the_html(path='/tmp'): - u = world.browser.url - html = world.browser.html.encode('ascii', 'ignore') - filename = '%s.html' % quote_plus(u) - f = open('%s/%s' % (path, filename), 'w') - f.write(html) - f.close - - -@world.absorb -def save_the_course_content(path='/tmp'): - html = world.browser.html.encode('ascii', 'ignore') - soup = BeautifulSoup(html) - - # get rid of the header, we only want to compare the body - soup.head.decompose() - - # for now, remove the data-id attributes, because they are - # causing mismatches between cms-master and master - for item in soup.find_all(attrs={'data-id': re.compile('.*')}): - del item['data-id'] - - # we also need to remove them from unrendered problems, - # where they are contained in the text of divs instead of - # in attributes of tags - # Be careful of whether or not it was the last attribute - # and needs a trailing space - for item in soup.find_all(text=re.compile(' data-id=".*?" ')): - s = unicode(item.string) - item.string.replace_with(re.sub(' data-id=".*?" ', ' ', s)) - - for item in soup.find_all(text=re.compile(' data-id=".*?"')): - s = unicode(item.string) - item.string.replace_with(re.sub(' data-id=".*?"', ' ', s)) - - # prettify the html so it will compare better, with - # each HTML tag on its own line - output = soup.prettify() - - # use string slicing to grab everything after 'courseware/' in the URL - u = world.browser.url - section_url = u[u.find('courseware/') + 11:] - - - if not os.path.exists(path): - os.makedirs(path) - - filename = '%s.html' % (quote_plus(section_url)) - 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() + world.create_user('robot') From 315b360e4cafeab3fec798272ed2e5ee22cb88d0 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 22 Mar 2013 11:31:41 -0400 Subject: [PATCH 169/436] Fixed an import error in terrain/helpers.py --- common/djangoapps/terrain/helpers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/common/djangoapps/terrain/helpers.py b/common/djangoapps/terrain/helpers.py index 55c8f3db5a..12d6818659 100644 --- a/common/djangoapps/terrain/helpers.py +++ b/common/djangoapps/terrain/helpers.py @@ -12,6 +12,7 @@ import os.path from selenium.common.exceptions import WebDriverException from urllib import quote_plus from lettuce.django import django_url +import time @world.absorb def wait(seconds): From e494d529fc48f21c1bb01bdee7dc8515035b6219 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 22 Mar 2013 11:38:30 -0400 Subject: [PATCH 170/436] Split terrain/helpers.py into ui_helpers.py and course_helpers.py --- .../terrain/{helpers.py => course_helpers.py} | 32 ------------------- common/djangoapps/terrain/steps.py | 3 +- common/djangoapps/terrain/ui_helpers.py | 30 +++++++++++++++++ 3 files changed, 32 insertions(+), 33 deletions(-) rename common/djangoapps/terrain/{helpers.py => course_helpers.py} (82%) create mode 100644 common/djangoapps/terrain/ui_helpers.py diff --git a/common/djangoapps/terrain/helpers.py b/common/djangoapps/terrain/course_helpers.py similarity index 82% rename from common/djangoapps/terrain/helpers.py rename to common/djangoapps/terrain/course_helpers.py index 12d6818659..dbdaa2a21c 100644 --- a/common/djangoapps/terrain/helpers.py +++ b/common/djangoapps/terrain/course_helpers.py @@ -12,17 +12,6 @@ import os.path from selenium.common.exceptions import WebDriverException from urllib import quote_plus from lettuce.django import django_url -import time - -@world.absorb -def wait(seconds): - time.sleep(float(seconds)) - -@world.absorb -def scroll_to_bottom(): - # Maximize the browser - world.browser.execute_script("window.scrollTo(0, screen.height);") - @world.absorb def create_user(uname): @@ -87,15 +76,6 @@ def register_by_course_id(course_id, is_staff=False): CourseEnrollment.objects.get_or_create(user=u, course_id=course_id) -@world.absorb -def save_the_html(path='/tmp'): - u = world.browser.url - html = world.browser.html.encode('ascii', 'ignore') - filename = '%s.html' % quote_plus(u) - f = open('%s/%s' % (path, filename), 'w') - f.write(html) - f.close - @world.absorb def save_the_course_content(path='/tmp'): @@ -139,15 +119,3 @@ 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/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index ae36227fee..6e54b71aa6 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -1,5 +1,6 @@ from lettuce import world, step -from .helpers import * +from .course_helpers import * +from .ui_helpers import * from lettuce.django import django_url from nose.tools import assert_equals import time diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py new file mode 100644 index 0000000000..4667957e87 --- /dev/null +++ b/common/djangoapps/terrain/ui_helpers.py @@ -0,0 +1,30 @@ +from lettuce import world, step +import time +from urllib import quote_plus + +@world.absorb +def wait(seconds): + time.sleep(float(seconds)) + + +@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() + +@world.absorb +def save_the_html(path='/tmp'): + u = world.browser.url + html = world.browser.html.encode('ascii', 'ignore') + filename = '%s.html' % quote_plus(u) + f = open('%s/%s' % (path, filename), 'w') + f.write(html) + f.close + From 0562f11c5622c94214162ac5c43fd69b8851601f Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 22 Mar 2013 11:41:30 -0400 Subject: [PATCH 171/436] Fixed import issue with WebDriverException --- common/djangoapps/terrain/course_helpers.py | 1 - common/djangoapps/terrain/ui_helpers.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/common/djangoapps/terrain/course_helpers.py b/common/djangoapps/terrain/course_helpers.py index dbdaa2a21c..8c949de1ad 100644 --- a/common/djangoapps/terrain/course_helpers.py +++ b/common/djangoapps/terrain/course_helpers.py @@ -9,7 +9,6 @@ from django.contrib.sessions.middleware import SessionMiddleware from student.models import CourseEnrollment from bs4 import BeautifulSoup import os.path -from selenium.common.exceptions import WebDriverException from urllib import quote_plus from lettuce.django import django_url diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py index 4667957e87..2ad7150740 100644 --- a/common/djangoapps/terrain/ui_helpers.py +++ b/common/djangoapps/terrain/ui_helpers.py @@ -1,6 +1,7 @@ from lettuce import world, step import time from urllib import quote_plus +from selenium.common.exceptions import WebDriverException @world.absorb def wait(seconds): From b0eb73302b9753acbc53f3ddc4fe86226f51292b Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 22 Mar 2013 11:50:50 -0400 Subject: [PATCH 172/436] Moved some courseware/features/common.py steps into terrain/steps.py --- common/djangoapps/terrain/steps.py | 38 ++++++++- lms/djangoapps/courseware/features/common.py | 83 -------------------- 2 files changed, 36 insertions(+), 85 deletions(-) diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index 6e54b71aa6..8356b5446d 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -72,6 +72,9 @@ def the_page_title_should_be(step, title): def the_page_title_should_contain(step, title): assert(title in world.browser.title) +@step('I log in$') +def i_log_in(step): + world.log_in('robot', 'test') @step('I am a logged in user$') def i_am_logged_in_user(step): @@ -89,11 +92,42 @@ def i_am_staff_for_course_by_id(step, course_id): world.register_by_course_id(course_id, True) -@step('I log in$') -def i_log_in(step): +@step(r'click (?:the|a) link (?:called|with the text) "([^"]*)"$') +def click_the_link_called(step, text): + world.browser.find_link_by_text(text).click() + + +@step(r'should see that the url is "([^"]*)"$') +def should_have_the_url(step, url): + assert_equals(world.browser.url, url) + +@step(r'should see (?:the|a) link (?:called|with the text) "([^"]*)"$') +def should_see_a_link_called(step, text): + assert len(world.browser.find_link_by_text(text)) > 0 + +@step(r'should see "(.*)" (?:somewhere|anywhere) in (?:the|this) page') +def should_see_in_the_page(step, text): + assert_in(text, world.browser.html) + + +@step('I am logged in$') +def i_am_logged_in(step): + world.create_user('robot') world.log_in('robot', 'test') + world.browser.visit(django_url('/')) + + +@step('I am not logged in$') +def i_am_not_logged_in(step): + world.browser.cookies.delete() @step(u'I am an edX user$') def i_am_an_edx_user(step): world.create_user('robot') + + +@step(u'User "([^"]*)" is an edX user$') +def registered_edx_user(step, uname): + world.create_user(uname) + diff --git a/lms/djangoapps/courseware/features/common.py b/lms/djangoapps/courseware/features/common.py index 7d41637c8e..8477347580 100644 --- a/lms/djangoapps/courseware/features/common.py +++ b/lms/djangoapps/courseware/features/common.py @@ -6,83 +6,10 @@ 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 logger = getLogger(__name__) - -@step(u'I wait (?:for )?"(\d+)" seconds?$') -def wait(step, seconds): - time.sleep(float(seconds)) - - -@step('I (?:visit|access|open) the homepage$') -def i_visit_the_homepage(step): - world.browser.visit(django_url('/')) - assert world.browser.is_element_present_by_css('header.global', 10) - - -@step(u'I (?:visit|access|open) the dashboard$') -def i_visit_the_dashboard(step): - world.browser.visit(django_url('/dashboard')) - assert world.browser.is_element_present_by_css('section.container.dashboard', 5) - - -@step(r'click (?:the|a) link (?:called|with the text) "([^"]*)"$') -def click_the_link_called(step, text): - world.browser.find_link_by_text(text).click() - - -@step('I should be on the dashboard page$') -def i_should_be_on_the_dashboard(step): - assert world.browser.is_element_present_by_css('section.container.dashboard', 5) - assert world.browser.title == 'Dashboard' - - -@step(u'I (?:visit|access|open) the courses page$') -def i_am_on_the_courses_page(step): - world.browser.visit(django_url('/courses')) - assert world.browser.is_element_present_by_css('section.courses') - - -@step('I should see that the path is "([^"]*)"$') -def i_should_see_that_the_path_is(step, path): - assert world.browser.url == django_url(path) - - -@step(u'the page title should be "([^"]*)"$') -def the_page_title_should_be(step, title): - assert world.browser.title == title - - -@step(r'should see that the url is "([^"]*)"$') -def should_have_the_url(step, url): - assert_equals(world.browser.url, url) - - -@step(r'should see (?:the|a) link (?:called|with the text) "([^"]*)"$') -def should_see_a_link_called(step, text): - assert len(world.browser.find_link_by_text(text)) > 0 - - -@step(r'should see "(.*)" (?:somewhere|anywhere) in (?:the|this) page') -def should_see_in_the_page(step, text): - assert_in(text, world.browser.html) - - -@step('I am logged in$') -def i_am_logged_in(step): - world.create_user('robot') - world.log_in('robot', 'test') - world.browser.visit(django_url('/')) - - -@step('I am not logged in$') -def i_am_not_logged_in(step): - world.browser.cookies.delete() - - TEST_COURSE_ORG = 'edx' TEST_COURSE_NAME = 'Test Course' TEST_SECTION_NAME = "Problem" @@ -135,16 +62,6 @@ def add_tab_to_course(step, course, extra_tab_name): display_name=str(extra_tab_name)) -@step(u'I am an edX user$') -def i_am_an_edx_user(step): - world.create_user('robot') - - -@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 From c12e1fb1cec0fabd3d825dc7f270381146b1a2e7 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 22 Mar 2013 11:54:17 -0400 Subject: [PATCH 173/436] Added missing import statement --- common/djangoapps/terrain/steps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index 8356b5446d..8dac372a64 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -2,7 +2,7 @@ from lettuce import world, step from .course_helpers import * from .ui_helpers import * from lettuce.django import django_url -from nose.tools import assert_equals +from nose.tools import assert_equals, assert_in import time from logging import getLogger From 5e69050a163fc19e6ce042b206e8a25f105ac509 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 22 Mar 2013 12:01:55 -0400 Subject: [PATCH 174/436] Elminated unused functions from courseware/features/courses.py and moved the rest to common.py --- lms/djangoapps/courseware/features/common.py | 87 +++++++ lms/djangoapps/courseware/features/courses.py | 234 ------------------ .../courseware/features/smart-accordion.py | 2 +- 3 files changed, 88 insertions(+), 235 deletions(-) delete mode 100644 lms/djangoapps/courseware/features/courses.py diff --git a/lms/djangoapps/courseware/features/common.py b/lms/djangoapps/courseware/features/common.py index 8477347580..2d366d462d 100644 --- a/lms/djangoapps/courseware/features/common.py +++ b/lms/djangoapps/courseware/features/common.py @@ -6,6 +6,9 @@ from student.models import CourseEnrollment from xmodule.modulestore import Location from xmodule.modulestore.django import _MODULESTORES, modulestore from xmodule.templates import update_templates +from xmodule.course_module import CourseDescriptor +from courseware.courses import get_course_by_id +from xmodule import seq_module, vertical_module from logging import getLogger logger = getLogger(__name__) @@ -94,3 +97,87 @@ def section_location(course_num): course=course_num, category='sequential', name=TEST_SECTION_NAME.replace(" ", "_")) + + +def get_courses(): + ''' + Returns dict of lists of courses available, keyed by course.org (ie university). + Courses are sorted by course.number. + ''' + courses = [c for c in modulestore().get_courses() + if isinstance(c, CourseDescriptor)] + courses = sorted(courses, key=lambda course: course.number) + return courses + + +def get_courseware_with_tabs(course_id): + """ + Given a course_id (string), return a courseware array of dictionaries for the + top three levels of navigation. Same as get_courseware() except include + the tabs on the right hand main navigation page. + + This hides the appropriate courseware as defined by the hide_from_toc field: + chapter.lms.hide_from_toc + + Example: + + [{ + 'chapter_name': 'Overview', + 'sections': [{ + 'clickable_tab_count': 0, + 'section_name': 'Welcome', + 'tab_classes': [] + }, { + 'clickable_tab_count': 1, + 'section_name': 'System Usage Sequence', + 'tab_classes': ['VerticalDescriptor'] + }, { + 'clickable_tab_count': 0, + 'section_name': 'Lab0: Using the tools', + 'tab_classes': ['HtmlDescriptor', 'HtmlDescriptor', 'CapaDescriptor'] + }, { + 'clickable_tab_count': 0, + 'section_name': 'Circuit Sandbox', + 'tab_classes': [] + }] + }, { + 'chapter_name': 'Week 1', + 'sections': [{ + 'clickable_tab_count': 4, + 'section_name': 'Administrivia and Circuit Elements', + 'tab_classes': ['VerticalDescriptor', 'VerticalDescriptor', 'VerticalDescriptor', 'VerticalDescriptor'] + }, { + 'clickable_tab_count': 0, + 'section_name': 'Basic Circuit Analysis', + 'tab_classes': ['CapaDescriptor', 'CapaDescriptor', 'CapaDescriptor'] + }, { + 'clickable_tab_count': 0, + 'section_name': 'Resistor Divider', + 'tab_classes': [] + }, { + 'clickable_tab_count': 0, + 'section_name': 'Week 1 Tutorials', + 'tab_classes': [] + }] + }, { + 'chapter_name': 'Midterm Exam', + 'sections': [{ + 'clickable_tab_count': 2, + 'section_name': 'Midterm Exam', + 'tab_classes': ['VerticalDescriptor', 'VerticalDescriptor'] + }] + }] + """ + + 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, + '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()]} + for s in c.get_children() if not s.lms.hide_from_toc]} + for c in chapters] + + return courseware diff --git a/lms/djangoapps/courseware/features/courses.py b/lms/djangoapps/courseware/features/courses.py deleted file mode 100644 index c99fb58b85..0000000000 --- a/lms/djangoapps/courseware/features/courses.py +++ /dev/null @@ -1,234 +0,0 @@ -from lettuce import world -from xmodule.course_module import CourseDescriptor -from xmodule.modulestore.django import modulestore -from courseware.courses import get_course_by_id -from xmodule import seq_module, vertical_module - -from logging import getLogger -logger = getLogger(__name__) - -## support functions - - -def get_courses(): - ''' - Returns dict of lists of courses available, keyed by course.org (ie university). - Courses are sorted by course.number. - ''' - courses = [c for c in modulestore().get_courses() - if isinstance(c, CourseDescriptor)] - courses = sorted(courses, key=lambda course: course.number) - return courses - - -def get_courseware_with_tabs(course_id): - """ - Given a course_id (string), return a courseware array of dictionaries for the - top three levels of navigation. Same as get_courseware() except include - the tabs on the right hand main navigation page. - - This hides the appropriate courseware as defined by the hide_from_toc field: - chapter.lms.hide_from_toc - - Example: - - [{ - 'chapter_name': 'Overview', - 'sections': [{ - 'clickable_tab_count': 0, - 'section_name': 'Welcome', - 'tab_classes': [] - }, { - 'clickable_tab_count': 1, - 'section_name': 'System Usage Sequence', - 'tab_classes': ['VerticalDescriptor'] - }, { - 'clickable_tab_count': 0, - 'section_name': 'Lab0: Using the tools', - 'tab_classes': ['HtmlDescriptor', 'HtmlDescriptor', 'CapaDescriptor'] - }, { - 'clickable_tab_count': 0, - 'section_name': 'Circuit Sandbox', - 'tab_classes': [] - }] - }, { - 'chapter_name': 'Week 1', - 'sections': [{ - 'clickable_tab_count': 4, - 'section_name': 'Administrivia and Circuit Elements', - 'tab_classes': ['VerticalDescriptor', 'VerticalDescriptor', 'VerticalDescriptor', 'VerticalDescriptor'] - }, { - 'clickable_tab_count': 0, - 'section_name': 'Basic Circuit Analysis', - 'tab_classes': ['CapaDescriptor', 'CapaDescriptor', 'CapaDescriptor'] - }, { - 'clickable_tab_count': 0, - 'section_name': 'Resistor Divider', - 'tab_classes': [] - }, { - 'clickable_tab_count': 0, - 'section_name': 'Week 1 Tutorials', - 'tab_classes': [] - }] - }, { - 'chapter_name': 'Midterm Exam', - 'sections': [{ - 'clickable_tab_count': 2, - 'section_name': 'Midterm Exam', - 'tab_classes': ['VerticalDescriptor', 'VerticalDescriptor'] - }] - }] - """ - - 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, - '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()]} - for s in c.get_children() if not s.lms.hide_from_toc]} - for c in chapters] - - return courseware - - -def process_section(element, num_tabs=0): - ''' - Process section reads through whatever is in 'course-content' and classifies it according to sequence module type. - - This function is recursive - - There are 6 types, with 6 actions. - - Sequence Module - -contains one child module - - Vertical Module - -contains other modules - -process it and get its children, then process them - - Capa Module - -problem type, contains only one problem - -for this, the most complex type, we created a separate method, process_problem - - Video Module - -video type, contains only one video - -we only check to ensure that a section with class of video exists - - HTML Module - -html text - -we do not check anything about it - - Custom Tag Module - -a custom 'hack' module type - -there is a large variety of content that could go in a custom tag module, so we just pass if it is of this unusual type - - can be used like this: - e = world.browser.find_by_css('section.course-content section') - process_section(e) - - ''' - if element.has_class('xmodule_display xmodule_SequenceModule'): - logger.debug('####### Processing xmodule_SequenceModule') - child_modules = element.find_by_css("div>div>section[class^='xmodule']") - for mod in child_modules: - process_section(mod) - - elif element.has_class('xmodule_display xmodule_VerticalModule'): - logger.debug('####### Processing xmodule_VerticalModule') - vert_list = element.find_by_css("li section[class^='xmodule']") - for item in vert_list: - process_section(item) - - elif element.has_class('xmodule_display xmodule_CapaModule'): - logger.debug('####### Processing xmodule_CapaModule') - assert element.find_by_css("section[id^='problem']"), "No problems found in Capa Module" - p = element.find_by_css("section[id^='problem']").first - p_id = p['id'] - logger.debug('####################') - logger.debug('id is "%s"' % p_id) - logger.debug('####################') - process_problem(p, p_id) - - elif element.has_class('xmodule_display xmodule_VideoModule'): - logger.debug('####### Processing xmodule_VideoModule') - assert element.find_by_css("section[class^='video']"), "No video found in Video Module" - - elif element.has_class('xmodule_display xmodule_HtmlModule'): - logger.debug('####### Processing xmodule_HtmlModule') - pass - - elif element.has_class('xmodule_display xmodule_CustomTagModule'): - logger.debug('####### Processing xmodule_CustomTagModule') - pass - - else: - assert False, "Class for element not recognized!!" - - -def process_problem(element, problem_id): - ''' - Process problem attempts to - 1) scan all the input fields and reset them - 2) click the 'check' button and look for an incorrect response (p.status text should be 'incorrect') - 3) click the 'show answer' button IF it exists and IF the answer is not already displayed - 4) enter the correct answer in each input box - 5) click the 'check' button and verify that answers are correct - - Because of all the ajax calls happening, sometimes the test fails because objects disconnect from the DOM. - The basic functionality does exist, though, and I'm hoping that someone can take it over and make it super effective. - ''' - - prob_xmod = element.find_by_css("section.problem").first - input_fields = prob_xmod.find_by_css("section[id^='input']") - - ## clear out all input to ensure an incorrect result - for field in input_fields: - field.find_by_css("input").first.fill('') - - ## because of cookies or the application, only click the 'check' button if the status is not already 'incorrect' - # This would need to be reworked because multiple choice problems don't have this status - # if prob_xmod.find_by_css("p.status").first.text.strip().lower() != 'incorrect': - prob_xmod.find_by_css("section.action input.check").first.click() - - ## all elements become disconnected after the click - ## grab element and prob_xmod because the dom has changed (some classes/elements became hidden and changed the hierarchy) - # Wait for the ajax reload - assert world.browser.is_element_present_by_css("section[id='%s']" % problem_id, wait_time=5) - element = world.browser.find_by_css("section[id='%s']" % problem_id).first - prob_xmod = element.find_by_css("section.problem").first - input_fields = prob_xmod.find_by_css("section[id^='input']") - for field in input_fields: - assert field.find_by_css("div.incorrect"), "The 'check' button did not work for %s" % (problem_id) - - show_button = element.find_by_css("section.action input.show").first - ## this logic is to ensure we do not accidentally hide the answers - if show_button.value.lower() == 'show answer': - show_button.click() - else: - pass - - ## grab element and prob_xmod because the dom has changed (some classes/elements became hidden and changed the hierarchy) - assert world.browser.is_element_present_by_css("section[id='%s']" % problem_id, wait_time=5) - element = world.browser.find_by_css("section[id='%s']" % problem_id).first - prob_xmod = element.find_by_css("section.problem").first - input_fields = prob_xmod.find_by_css("section[id^='input']") - - ## in each field, find the answer, and send it to the field. - ## Note that this does not work if the answer type is a strange format, e.g. "either a or b" - for field in input_fields: - field.find_by_css("input").first.fill(field.find_by_css("p[id^='answer']").first.text) - - prob_xmod.find_by_css("section.action input.check").first.click() - - ## assert that we entered the correct answers - ## grab element and prob_xmod because the dom has changed (some classes/elements became hidden and changed the hierarchy) - assert world.browser.is_element_present_by_css("section[id='%s']" % problem_id, wait_time=5) - element = world.browser.find_by_css("section[id='%s']" % problem_id).first - prob_xmod = element.find_by_css("section.problem").first - input_fields = prob_xmod.find_by_css("section[id^='input']") - for field in input_fields: - ## if you don't use 'starts with ^=' the test will fail because the actual class is 'correct ' (with a space) - assert field.find_by_css("div[class^='correct']"), "The check answer values were not correct for %s" % problem_id diff --git a/lms/djangoapps/courseware/features/smart-accordion.py b/lms/djangoapps/courseware/features/smart-accordion.py index a7eb782722..539bce96ce 100644 --- a/lms/djangoapps/courseware/features/smart-accordion.py +++ b/lms/djangoapps/courseware/features/smart-accordion.py @@ -2,7 +2,7 @@ from lettuce import world, step from re import sub from nose.tools import assert_equals from xmodule.modulestore.django import modulestore -from courses import * +from common import * from logging import getLogger logger = getLogger(__name__) From 6dd86f7a97826ec7af6fcb608928d6f0a7c07660 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 22 Mar 2013 12:19:46 -0400 Subject: [PATCH 175/436] Refactored courseware_common and open_ended to use ui helpers --- common/djangoapps/terrain/ui_helpers.py | 16 +++++++++ .../courseware/features/courseware_common.py | 15 +++----- .../courseware/features/openended.py | 36 +++++++------------ 3 files changed, 34 insertions(+), 33 deletions(-) diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py index 2ad7150740..d56ce3649b 100644 --- a/common/djangoapps/terrain/ui_helpers.py +++ b/common/djangoapps/terrain/ui_helpers.py @@ -20,6 +20,22 @@ def css_click(css_selector): time.sleep(1) world.browser.find_by_css(css_selector).click() +@world.absorb +def css_fill(css_selector, text): + world.browser.find_by_css(css_selector).first.fill(text) + +@world.absorb +def click_link(partial_text): + world.browser.find_link_by_partial_text(partial_text).first.click() + +@world.absorb +def css_text(css_selector): + return world.browser.find_by_css(css_selector).first.text + +@world.absorb +def css_visible(css_selector): + return world.browser.find_by_css(css_selector).visible + @world.absorb def save_the_html(path='/tmp'): u = world.browser.url diff --git a/lms/djangoapps/courseware/features/courseware_common.py b/lms/djangoapps/courseware/features/courseware_common.py index 96304e016f..567254c334 100644 --- a/lms/djangoapps/courseware/features/courseware_common.py +++ b/lms/djangoapps/courseware/features/courseware_common.py @@ -9,11 +9,10 @@ def i_click_on_view_courseware(step): @step('I click on the "([^"]*)" tab$') -def i_click_on_the_tab(step, tab): - world.browser.find_link_by_partial_text(tab).first.click() +def i_click_on_the_tab(step, tab_text): + world.click_link(tab_text) world.save_the_html() - @step('I visit the courseware URL$') def i_visit_the_course_info_url(step): url = django_url('/courses/MITx/6.002x/2012_Fall/courseware') @@ -32,13 +31,9 @@ def i_am_on_the_dashboard_page(step): @step('the "([^"]*)" tab is active$') -def the_tab_is_active(step, tab): - css = '.course-tabs a.active' - active_tab = world.browser.find_by_css(css) - assert (active_tab.text == tab) - +def the_tab_is_active(step, tab_text): + assert world.css_text('.course-tabs a.active') == tab_text @step('the login dialog is visible$') def login_dialog_visible(step): - css = 'form#login_form.login_form' - assert world.browser.find_by_css(css).visible + assert world.css_visible('form#login_form.login_form') diff --git a/lms/djangoapps/courseware/features/openended.py b/lms/djangoapps/courseware/features/openended.py index 0725a051ff..7601bfcc53 100644 --- a/lms/djangoapps/courseware/features/openended.py +++ b/lms/djangoapps/courseware/features/openended.py @@ -12,7 +12,7 @@ def navigate_to_an_openended_question(step): problem = '/courses/MITx/3.091x/2012_Fall/courseware/Week_10/Polymer_Synthesis/' world.browser.visit(django_url(problem)) tab_css = 'ol#sequence-list > li > a[data-element="5"]' - world.browser.find_by_css(tab_css).click() + world.css_click(tab_css) @step('I navigate to an openended question as staff$') @@ -22,50 +22,41 @@ def navigate_to_an_openended_question_as_staff(step): problem = '/courses/MITx/3.091x/2012_Fall/courseware/Week_10/Polymer_Synthesis/' world.browser.visit(django_url(problem)) tab_css = 'ol#sequence-list > li > a[data-element="5"]' - world.browser.find_by_css(tab_css).click() + world.css_click(tab_css) @step(u'I enter the answer "([^"]*)"$') def enter_the_answer_text(step, text): - textarea_css = 'textarea' - world.browser.find_by_css(textarea_css).first.fill(text) + world.css_fill('textarea', text) @step(u'I submit the answer "([^"]*)"$') def i_submit_the_answer_text(step, text): - textarea_css = 'textarea' - world.browser.find_by_css(textarea_css).first.fill(text) - check_css = 'input.check' - world.browser.find_by_css(check_css).click() + world.css_fill('textarea', text) + world.css_click('input.check') @step('I click the link for full output$') def click_full_output_link(step): - link_css = 'a.full' - world.browser.find_by_css(link_css).first.click() + world.css_click('a.full') @step(u'I visit the staff grading page$') def i_visit_the_staff_grading_page(step): - # course_u = '/courses/MITx/3.091x/2012_Fall' - # sg_url = '%s/staff_grading' % course_u - world.browser.click_link_by_text('Instructor') - world.browser.click_link_by_text('Staff grading') - # world.browser.visit(django_url(sg_url)) + world.click_link('Instructor') + world.click_link('Staff grading') @step(u'I see the grader message "([^"]*)"$') def see_grader_message(step, msg): message_css = 'div.external-grader-message' - grader_msg = world.browser.find_by_css(message_css).text - assert_in(msg, grader_msg) + assert_in(msg, world.css_text(message_css)) @step(u'I see the grader status "([^"]*)"$') def see_the_grader_status(step, status): status_css = 'div.grader-status' - grader_status = world.browser.find_by_css(status_css).text - assert_equals(status, grader_status) + assert_equals(status, world.css_text(status_css)) @step('I see the red X$') @@ -77,7 +68,7 @@ def see_the_red_x(step): @step(u'I see the grader score "([^"]*)"$') def see_the_grader_score(step, score): score_css = 'div.result-output > p' - score_text = world.browser.find_by_css(score_css).text + score_text = world.css_text(score_css) assert_equals(score_text, 'Score: %s' % score) @@ -89,14 +80,13 @@ def see_full_output_link(step): @step('I see the spelling grading message "([^"]*)"$') def see_spelling_msg(step, msg): - spelling_css = 'div.spelling' - spelling_msg = world.browser.find_by_css(spelling_css).text + spelling_msg = world.css_text('div.spelling') assert_equals('Spelling: %s' % msg, spelling_msg) @step(u'my answer is queued for instructor grading$') def answer_is_queued_for_instructor_grading(step): list_css = 'ul.problem-list > li > a' - actual_msg = world.browser.find_by_css(list_css).text + actual_msg = world.css_text(list_css) expected_msg = "(0 graded, 1 pending)" assert_in(expected_msg, actual_msg) From 4528490fac9050881eba0ff98df07782e71bbabc Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 22 Mar 2013 12:40:33 -0400 Subject: [PATCH 176/436] Refactored lms/coureware lettuce tests to use terrain helpers for common ui manipulations --- common/djangoapps/terrain/course_helpers.py | 1 + common/djangoapps/terrain/steps.py | 6 ++++- common/djangoapps/terrain/ui_helpers.py | 23 ++++++++++++++++++- .../courseware/features/courseware_common.py | 13 +++++------ lms/djangoapps/courseware/features/login.py | 4 +--- .../courseware/features/openended.py | 6 ++--- .../courseware/features/problems.py | 4 ++-- .../courseware/features/registration.py | 8 +++---- lms/djangoapps/courseware/features/signup.py | 2 +- .../courseware/features/smart-accordion.py | 10 ++++---- .../courseware/features/xqueue_setup.py | 1 + 11 files changed, 50 insertions(+), 28 deletions(-) diff --git a/common/djangoapps/terrain/course_helpers.py b/common/djangoapps/terrain/course_helpers.py index 8c949de1ad..ebf5745f11 100644 --- a/common/djangoapps/terrain/course_helpers.py +++ b/common/djangoapps/terrain/course_helpers.py @@ -12,6 +12,7 @@ import os.path from urllib import quote_plus from lettuce.django import django_url + @world.absorb def create_user(uname): diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index 8dac372a64..e99dec44b3 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -13,6 +13,7 @@ logger = getLogger(__name__) def wait(step, seconds): world.wait(seconds) + @step('I reload the page$') def reload_the_page(step): world.browser.reload() @@ -72,10 +73,12 @@ def the_page_title_should_be(step, title): def the_page_title_should_contain(step, title): assert(title in world.browser.title) + @step('I log in$') def i_log_in(step): world.log_in('robot', 'test') + @step('I am a logged in user$') def i_am_logged_in_user(step): world.create_user('robot') @@ -101,10 +104,12 @@ def click_the_link_called(step, text): def should_have_the_url(step, url): assert_equals(world.browser.url, url) + @step(r'should see (?:the|a) link (?:called|with the text) "([^"]*)"$') def should_see_a_link_called(step, text): assert len(world.browser.find_link_by_text(text)) > 0 + @step(r'should see "(.*)" (?:somewhere|anywhere) in (?:the|this) page') def should_see_in_the_page(step, text): assert_in(text, world.browser.html) @@ -130,4 +135,3 @@ def i_am_an_edx_user(step): @step(u'User "([^"]*)" is an edX user$') def registered_edx_user(step, uname): world.create_user(uname) - diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py index d56ce3649b..1aac9cc72e 100644 --- a/common/djangoapps/terrain/ui_helpers.py +++ b/common/djangoapps/terrain/ui_helpers.py @@ -2,12 +2,29 @@ from lettuce import world, step import time from urllib import quote_plus from selenium.common.exceptions import WebDriverException +from lettuce.django import django_url + @world.absorb def wait(seconds): time.sleep(float(seconds)) +@world.absorb +def visit(url): + world.browser.visit(django_url(url)) + + +@world.absorb +def url_equals(url): + return world.browser.url == django_url(url) + + +@world.absorb +def is_css_present(css_selector): + return world.browser.is_element_present_by_css(css_selector, wait_time=4) + + @world.absorb def css_click(css_selector): try: @@ -20,22 +37,27 @@ def css_click(css_selector): time.sleep(1) world.browser.find_by_css(css_selector).click() + @world.absorb def css_fill(css_selector, text): world.browser.find_by_css(css_selector).first.fill(text) + @world.absorb def click_link(partial_text): world.browser.find_link_by_partial_text(partial_text).first.click() + @world.absorb def css_text(css_selector): return world.browser.find_by_css(css_selector).first.text + @world.absorb def css_visible(css_selector): return world.browser.find_by_css(css_selector).visible + @world.absorb def save_the_html(path='/tmp'): u = world.browser.url @@ -44,4 +66,3 @@ def save_the_html(path='/tmp'): f = open('%s/%s' % (path, filename), 'w') f.write(html) f.close - diff --git a/lms/djangoapps/courseware/features/courseware_common.py b/lms/djangoapps/courseware/features/courseware_common.py index 567254c334..6aa9559e65 100644 --- a/lms/djangoapps/courseware/features/courseware_common.py +++ b/lms/djangoapps/courseware/features/courseware_common.py @@ -1,11 +1,9 @@ from lettuce import world, step -from lettuce.django import django_url @step('I click on View Courseware') def i_click_on_view_courseware(step): - css = 'a.enter-course' - world.browser.find_by_css(css).first.click() + world.css_click('a.enter-course') @step('I click on the "([^"]*)" tab$') @@ -13,10 +11,10 @@ def i_click_on_the_tab(step, tab_text): world.click_link(tab_text) world.save_the_html() + @step('I visit the courseware URL$') def i_visit_the_course_info_url(step): - url = django_url('/courses/MITx/6.002x/2012_Fall/courseware') - world.browser.visit(url) + world.visit('/courses/MITx/6.002x/2012_Fall/courseware') @step(u'I do not see "([^"]*)" anywhere on the page') @@ -26,14 +24,15 @@ def i_do_not_see_text_anywhere_on_the_page(step, text): @step(u'I am on the dashboard page$') def i_am_on_the_dashboard_page(step): - assert world.browser.is_element_present_by_css('section.courses') - assert world.browser.url == django_url('/dashboard') + assert world.is_css_present('section.courses') + assert world.url_equals('/dashboard') @step('the "([^"]*)" tab is active$') def the_tab_is_active(step, tab_text): assert world.css_text('.course-tabs a.active') == tab_text + @step('the login dialog is visible$') def login_dialog_visible(step): assert world.css_visible('form#login_form.login_form') diff --git a/lms/djangoapps/courseware/features/login.py b/lms/djangoapps/courseware/features/login.py index 094db078ca..3e3c0efbc4 100644 --- a/lms/djangoapps/courseware/features/login.py +++ b/lms/djangoapps/courseware/features/login.py @@ -28,9 +28,7 @@ def i_should_see_the_login_error_message(step, msg): @step(u'click the dropdown arrow$') def click_the_dropdown(step): - css = ".dropdown" - e = world.browser.find_by_css(css) - e.click() + world.css_click('.dropdown') #### helper functions diff --git a/lms/djangoapps/courseware/features/openended.py b/lms/djangoapps/courseware/features/openended.py index 7601bfcc53..2f14b808a3 100644 --- a/lms/djangoapps/courseware/features/openended.py +++ b/lms/djangoapps/courseware/features/openended.py @@ -61,8 +61,7 @@ def see_the_grader_status(step, status): @step('I see the red X$') def see_the_red_x(step): - x_css = 'div.grader-status > span.incorrect' - assert world.browser.find_by_css(x_css) + assert world.is_css_present('div.grader-status > span.incorrect') @step(u'I see the grader score "([^"]*)"$') @@ -74,8 +73,7 @@ def see_the_grader_score(step, score): @step('I see the link for full output$') def see_full_output_link(step): - link_css = 'a.full' - assert world.browser.find_by_css(link_css) + assert world.is_css_present('a.full') @step('I see the spelling grading message "([^"]*)"$') diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index d2d379a212..bdd9062ef3 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -339,7 +339,7 @@ def assert_answer_mark(step, problem_type, correctness): # At least one of the correct selectors should be present for sel in selector_dict[problem_type]: - has_expected = world.browser.is_element_present_by_css(sel, wait_time=4) + has_expected = world.is_css_present(sel) # As soon as we find the selector, break out of the loop if has_expected: @@ -366,7 +366,7 @@ def inputfield(problem_type, choice=None, input_num=1): # If the input element doesn't exist, fail immediately - assert(world.browser.is_element_present_by_css(sel, wait_time=4)) + assert world.is_css_present(sel) # Retrieve the input element return world.browser.find_by_css(sel) diff --git a/lms/djangoapps/courseware/features/registration.py b/lms/djangoapps/courseware/features/registration.py index 94b9b50f6c..63f044b16f 100644 --- a/lms/djangoapps/courseware/features/registration.py +++ b/lms/djangoapps/courseware/features/registration.py @@ -13,17 +13,17 @@ def i_register_for_the_course(step, course): register_link = intro_section.find_by_css('a.register') register_link.click() - assert world.browser.is_element_present_by_css('section.container.dashboard') + assert world.is_css_present('section.container.dashboard') @step(u'I should see the course numbered "([^"]*)" in my dashboard$') def i_should_see_that_course_in_my_dashboard(step, course): course_link_css = 'section.my-courses a[href*="%s"]' % course - assert world.browser.is_element_present_by_css(course_link_css) + assert world.is_css_present(course_link_css) @step(u'I press the "([^"]*)" button in the Unenroll dialog') def i_press_the_button_in_the_unenroll_dialog(step, value): button_css = 'section#unenroll-modal input[value="%s"]' % value - world.browser.find_by_css(button_css).click() - assert world.browser.is_element_present_by_css('section.container.dashboard') + world.css_click(button_css) + assert world.is_css_present('section.container.dashboard') diff --git a/lms/djangoapps/courseware/features/signup.py b/lms/djangoapps/courseware/features/signup.py index 3a697a6102..d9edcb215b 100644 --- a/lms/djangoapps/courseware/features/signup.py +++ b/lms/djangoapps/courseware/features/signup.py @@ -22,4 +22,4 @@ def i_check_checkbox(step, checkbox): @step('I should see "([^"]*)" in the dashboard banner$') def i_should_see_text_in_the_dashboard_banner_section(step, text): css_selector = "section.dashboard-banner h2" - assert (text in world.browser.find_by_css(css_selector).text) + assert (text in world.css_text(css_selector)) diff --git a/lms/djangoapps/courseware/features/smart-accordion.py b/lms/djangoapps/courseware/features/smart-accordion.py index 539bce96ce..8240a13905 100644 --- a/lms/djangoapps/courseware/features/smart-accordion.py +++ b/lms/djangoapps/courseware/features/smart-accordion.py @@ -32,20 +32,20 @@ def i_verify_all_the_content_of_each_course(step): pass for test_course in registered_courses: - test_course.find_by_css('a').click() + test_course.css_click('a') check_for_errors() # Get the course. E.g. 'MITx/6.002x/2012_Fall' current_course = sub('/info', '', sub('.*/courses/', '', world.browser.url)) validate_course(current_course, ids) - world.browser.find_link_by_text('Courseware').click() - assert world.browser.is_element_present_by_id('accordion', wait_time=2) + world.click_link('Courseware') + assert world.is_css_present('accordion') check_for_errors() browse_course(current_course) # clicking the user link gets you back to the user's home page - world.browser.find_by_css('.user-link').click() + world.css_click('.user-link') check_for_errors() @@ -94,7 +94,7 @@ def browse_course(course_id): world.browser.find_by_css('#accordion > nav > div')[chapter_it].find_by_tag('li')[section_it].find_by_tag('a').click() ## sometimes the course-content takes a long time to load - assert world.browser.is_element_present_by_css('.course-content', wait_time=5) + assert world.is_css_present('.course-content') ## look for server error div check_for_errors() diff --git a/lms/djangoapps/courseware/features/xqueue_setup.py b/lms/djangoapps/courseware/features/xqueue_setup.py index 23706941a9..d6d7a13a5c 100644 --- a/lms/djangoapps/courseware/features/xqueue_setup.py +++ b/lms/djangoapps/courseware/features/xqueue_setup.py @@ -3,6 +3,7 @@ from lettuce import before, after, world from django.conf import settings import threading + @before.all def setup_mock_xqueue_server(): From dde0d1676b8176119d5f33bf234221836c781aac Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 22 Mar 2013 13:02:40 -0400 Subject: [PATCH 177/436] Refactored terrain/steps.py to use ui_helpers Added a wait time before checking the page HTML, and changed it to check just in the HTML body --- common/djangoapps/terrain/steps.py | 27 +++++++++++-------------- common/djangoapps/terrain/ui_helpers.py | 7 ++++++- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index e99dec44b3..dc8d2f8b87 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -26,42 +26,40 @@ def browser_back(step): @step('I (?:visit|access|open) the homepage$') def i_visit_the_homepage(step): - world.browser.visit(django_url('/')) - assert world.browser.is_element_present_by_css('header.global', 10) - + world.visit('/') + assert world.is_css_present('header.global') @step(u'I (?:visit|access|open) the dashboard$') def i_visit_the_dashboard(step): - world.browser.visit(django_url('/dashboard')) - assert world.browser.is_element_present_by_css('section.container.dashboard', 5) - + world.visit('/dashboard') + assert world.is_css_present('section.container.dashboard') @step('I should be on the dashboard page$') def i_should_be_on_the_dashboard(step): - assert world.browser.is_element_present_by_css('section.container.dashboard', 5) + assert world.is_css_present('section.container.dashboard') assert world.browser.title == 'Dashboard' @step(u'I (?:visit|access|open) the courses page$') def i_am_on_the_courses_page(step): - world.browser.visit(django_url('/courses')) - assert world.browser.is_element_present_by_css('section.courses') + world.visit('/courses') + assert world.is_css_present('section.courses') @step(u'I press the "([^"]*)" button$') def and_i_press_the_button(step, value): button_css = 'input[value="%s"]' % value - world.browser.find_by_css(button_css).first.click() + world.css_click(button_css) @step(u'I click the link with the text "([^"]*)"$') def click_the_link_with_the_text_group1(step, linktext): - world.browser.find_link_by_text(linktext).first.click() + world.click_link(linktext) @step('I should see that the path is "([^"]*)"$') def i_should_see_that_the_path_is(step, path): - assert world.browser.url == django_url(path) + assert world.url_equals(path) @step(u'the page title should be "([^"]*)"$') @@ -97,8 +95,7 @@ def i_am_staff_for_course_by_id(step, course_id): @step(r'click (?:the|a) link (?:called|with the text) "([^"]*)"$') def click_the_link_called(step, text): - world.browser.find_link_by_text(text).click() - + world.click_link(text) @step(r'should see that the url is "([^"]*)"$') def should_have_the_url(step, url): @@ -112,7 +109,7 @@ def should_see_a_link_called(step, text): @step(r'should see "(.*)" (?:somewhere|anywhere) in (?:the|this) page') def should_see_in_the_page(step, text): - assert_in(text, world.browser.html) + assert_in(text, world.css_text('body')) @step('I am logged in$') diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py index 1aac9cc72e..3009d1fa8d 100644 --- a/common/djangoapps/terrain/ui_helpers.py +++ b/common/djangoapps/terrain/ui_helpers.py @@ -50,7 +50,12 @@ def click_link(partial_text): @world.absorb def css_text(css_selector): - return world.browser.find_by_css(css_selector).first.text + + # Wait for the css selector to appear + if world.is_css_present(css_selector): + return world.browser.find_by_css(css_selector).first.text + else: + return "" @world.absorb From e69931ec5a06ecec9bc57b2875181e94b9b2f059 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 22 Mar 2013 13:45:25 -0400 Subject: [PATCH 178/436] Refactored studio lettuce tests to use terrain/ui_helpers for ui manipulation --- .../features/advanced-settings.py | 40 ++----- .../contentstore/features/common.py | 102 +++++------------- .../contentstore/features/courses.py | 15 ++- .../contentstore/features/section.py | 26 ++--- .../contentstore/features/signup.py | 2 +- .../features/studio-overview-togglesection.py | 24 ++--- .../contentstore/features/subsection.py | 15 ++- common/djangoapps/terrain/ui_helpers.py | 34 ++++++ 8 files changed, 109 insertions(+), 149 deletions(-) diff --git a/cms/djangoapps/contentstore/features/advanced-settings.py b/cms/djangoapps/contentstore/features/advanced-settings.py index 7e86e94a31..0232c3b908 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.py +++ b/cms/djangoapps/contentstore/features/advanced-settings.py @@ -2,8 +2,6 @@ from lettuce import world, step from common import * import time from terrain.steps import reload_the_page -from selenium.common.exceptions import WebDriverException -from selenium.webdriver.support import expected_conditions as EC from nose.tools import assert_true, assert_false, assert_equal @@ -22,9 +20,9 @@ DISPLAY_NAME_VALUE = '"Robot Super Course"' def i_select_advanced_settings(step): expand_icon_css = 'li.nav-course-settings i.icon-expand' if world.browser.is_element_present_by_css(expand_icon_css): - css_click(expand_icon_css) + world.css_click(expand_icon_css) link_css = 'li.nav-course-settings-advanced a' - css_click(link_css) + world.css_click(link_css) @step('I am on the Advanced Course Settings page in Studio$') @@ -35,24 +33,8 @@ def i_am_on_advanced_course_settings(step): @step(u'I press the "([^"]*)" notification button$') def press_the_notification_button(step, name): - def is_visible(driver): - return EC.visibility_of_element_located((By.CSS_SELECTOR, css,)) - - # def is_invisible(driver): - # return EC.invisibility_of_element_located((By.CSS_SELECTOR,css,)) - css = 'a.%s-button' % name.lower() - wait_for(is_visible) - time.sleep(float(1)) - css_click_at(css) - -# is_invisible is not returning a boolean, not working -# try: -# css_click_at(css) -# wait_for(is_invisible) -# except WebDriverException, e: -# css_click_at(css) -# wait_for(is_invisible) + world.css_click_at(css) @step(u'I edit the value of a policy key$') @@ -61,7 +43,7 @@ def edit_the_value_of_a_policy_key(step): It is hard to figure out how to get into the CodeMirror area, so cheat and do it from the policy key field :) """ - e = css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)] + e = world.css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)] e._element.send_keys(Keys.TAB, Keys.END, Keys.ARROW_LEFT, ' ', 'X') @@ -85,7 +67,7 @@ def i_see_default_advanced_settings(step): @step('the settings are alphabetized$') def they_are_alphabetized(step): - key_elements = css_find(KEY_CSS) + key_elements = world.css_find(KEY_CSS) all_keys = [] for key in key_elements: all_keys.append(key.value) @@ -118,13 +100,13 @@ def assert_policy_entries(expected_keys, expected_values): for counter in range(len(expected_keys)): index = get_index_of(expected_keys[counter]) assert_false(index == -1, "Could not find key: " + expected_keys[counter]) - assert_equal(expected_values[counter], css_find(VALUE_CSS)[index].value, "value is incorrect") + assert_equal(expected_values[counter], world.css_find(VALUE_CSS)[index].value, "value is incorrect") def get_index_of(expected_key): - for counter in range(len(css_find(KEY_CSS))): + for counter in range(len(world.css_find(KEY_CSS))): # Sometimes get stale reference if I hold on to the array of elements - key = css_find(KEY_CSS)[counter].value + key = world.css_find(KEY_CSS)[counter].value if key == expected_key: return counter @@ -133,14 +115,14 @@ def get_index_of(expected_key): def get_display_name_value(): index = get_index_of(DISPLAY_NAME_KEY) - return css_find(VALUE_CSS)[index].value + return world.css_find(VALUE_CSS)[index].value def change_display_name_value(step, new_value): - e = css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)] + e = world.css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)] display_name = get_display_name_value() for count in range(len(display_name)): e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE) # Must delete "" before typing the JSON value e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE, Keys.BACK_SPACE, new_value) - press_the_notification_button(step, "Save") \ No newline at end of file + press_the_notification_button(step, "Save") diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 820b60123b..4cc5759949 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -1,11 +1,6 @@ from lettuce import world, step -from lettuce.django import django_url from nose.tools import assert_true from nose.tools import assert_equal -from selenium.webdriver.support.ui import WebDriverWait -from selenium.common.exceptions import WebDriverException, StaleElementReferenceException -from selenium.webdriver.support import expected_conditions as EC -from selenium.webdriver.common.by import By from xmodule.modulestore.django import _MODULESTORES, modulestore from xmodule.templates import update_templates @@ -20,9 +15,9 @@ def i_visit_the_studio_homepage(step): # To make this go to port 8001, put # LETTUCE_SERVER_PORT = 8001 # in your settings.py file. - world.browser.visit(django_url('/')) + world.visit('/') signin_css = 'a.action-signin' - assert world.browser.is_element_present_by_css(signin_css, 10) + assert world.is_css_present(signin_css) @step('I am logged into Studio$') @@ -43,7 +38,7 @@ def i_press_the_category_delete_icon(step, category): css = 'a.delete-button.delete-subsection-button span.delete-icon' else: assert False, 'Invalid category: %s' % category - css_click(css) + world.css_click(css) @step('I have opened a new course in Studio$') @@ -87,56 +82,6 @@ def flush_xmodule_store(): update_templates() -def assert_css_with_text(css, text): - assert_true(world.browser.is_element_present_by_css(css, 5)) - assert_equal(world.browser.find_by_css(css).text, text) - - -def css_click(css): - ''' - First try to use the regular click method, - but if clicking in the middle of an element - doesn't work it might be that it thinks some other - element is on top of it there so click in the upper left - ''' - try: - css_find(css).first.click() - except WebDriverException, e: - css_click_at(css) - - -def css_click_at(css, x=10, y=10): - ''' - A method to click at x,y coordinates of the element - rather than in the center of the element - ''' - e = css_find(css).first - e.action_chains.move_to_element_with_offset(e._element, x, y) - e.action_chains.click() - e.action_chains.perform() - - -def css_fill(css, value): - world.browser.find_by_css(css).first.fill(value) - - -def css_find(css): - def is_visible(driver): - return EC.visibility_of_element_located((By.CSS_SELECTOR,css,)) - - world.browser.is_element_present_by_css(css, 5) - wait_for(is_visible) - return world.browser.find_by_css(css) - - -def wait_for(func): - WebDriverWait(world.browser.driver, 5).until(func) - - -def id_find(id): - return world.browser.find_by_id(id) - - def clear_courses(): flush_xmodule_store() @@ -145,9 +90,9 @@ def fill_in_course_info( name='Robot Super Course', org='MITx', num='101'): - css_fill('.new-course-name', name) - css_fill('.new-course-org', org) - css_fill('.new-course-number', num) + world.css_fill('.new-course-name', name) + world.css_fill('.new-course-org', org) + world.css_fill('.new-course-number', num) def log_into_studio( @@ -155,21 +100,22 @@ def log_into_studio( email='robot+studio@edx.org', password='test', is_staff=False): - create_studio_user(uname=uname, email=email, is_staff=is_staff) - world.browser.cookies.delete() - world.browser.visit(django_url('/')) - signin_css = 'a.action-signin' - world.browser.is_element_present_by_css(signin_css, 10) - # click the signin button - css_click(signin_css) + create_studio_user(uname=uname, email=email, is_staff=is_staff) + + world.browser.cookies.delete() + world.visit('/') + + signin_css = 'a.action-signin' + world.is_css_present(signin_css) + world.css_click(signin_css) 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() - assert_true(world.browser.is_element_present_by_css('.new-course-button', 5)) + assert_true(world.is_css_present('.new-course-button')) def create_a_course(): @@ -184,26 +130,26 @@ def create_a_course(): world.browser.reload() course_link_css = 'span.class-name' - css_click(course_link_css) + world.css_click(course_link_css) course_title_css = 'span.course-title' - assert_true(world.browser.is_element_present_by_css(course_title_css, 5)) + assert_true(world.is_css_present(course_title_css)) def add_section(name='My Section'): link_css = 'a.new-courseware-section-button' - css_click(link_css) + world.css_click(link_css) name_css = 'input.new-section-name' save_css = 'input.new-section-name-save' - css_fill(name_css, name) - css_click(save_css) + world.css_fill(name_css, name) + world.css_click(save_css) span_css = 'span.section-name-span' - assert_true(world.browser.is_element_present_by_css(span_css, 5)) + assert_true(world.is_css_present(span_css)) def add_subsection(name='Subsection One'): css = 'a.new-subsection-item' - css_click(css) + world.css_click(css) name_css = 'input.new-subsection-name-input' save_css = 'input.new-subsection-name-save' - css_fill(name_css, name) - css_click(save_css) + world.css_fill(name_css, name) + world.css_click(save_css) diff --git a/cms/djangoapps/contentstore/features/courses.py b/cms/djangoapps/contentstore/features/courses.py index e394165f08..8301e6708f 100644 --- a/cms/djangoapps/contentstore/features/courses.py +++ b/cms/djangoapps/contentstore/features/courses.py @@ -11,7 +11,7 @@ def no_courses(step): @step('I click the New Course button$') def i_click_new_course(step): - css_click('.new-course-button') + world.css_click('.new-course-button') @step('I fill in the new course information$') @@ -27,7 +27,7 @@ def i_create_a_course(step): @step('I click the course link in My Courses$') def i_click_the_course_link_in_my_courses(step): course_css = 'span.class-name' - css_click(course_css) + world.css_click(course_css) ############ ASSERTIONS ################### @@ -35,28 +35,27 @@ def i_click_the_course_link_in_my_courses(step): @step('the Courseware page has loaded in Studio$') def courseware_page_has_loaded_in_studio(step): course_title_css = 'span.course-title' - assert world.browser.is_element_present_by_css(course_title_css) + assert world.is_css_present(course_title_css) @step('I see the course listed in My Courses$') def i_see_the_course_in_my_courses(step): course_css = 'span.class-name' - assert_css_with_text(course_css, 'Robot Super Course') - + assert world.css_has_text(course_css, 'Robot Super Course') @step('the course is loaded$') def course_is_loaded(step): class_css = 'a.class-name' - assert_css_with_text(class_css, 'Robot Super Course') + assert world.css_has_text(course_css, 'Robot Super Cousre') @step('I am on the "([^"]*)" tab$') def i_am_on_tab(step, tab_name): header_css = 'div.inner-wrapper h1' - assert_css_with_text(header_css, tab_name) + assert world.css_has_text(header_css, tab_name) @step('I see a link for adding a new section$') def i_see_new_section_link(step): link_css = 'a.new-courseware-section-button' - assert_css_with_text(link_css, '+ New Section') + assert world.css_has_text(link_css, '+ New Section') diff --git a/cms/djangoapps/contentstore/features/section.py b/cms/djangoapps/contentstore/features/section.py index b5ddb48a09..e57d50bbfe 100644 --- a/cms/djangoapps/contentstore/features/section.py +++ b/cms/djangoapps/contentstore/features/section.py @@ -10,7 +10,7 @@ import time @step('I click the new section link$') def i_click_new_section_link(step): link_css = 'a.new-courseware-section-button' - css_click(link_css) + world.css_click(link_css) @step('I enter the section name and click save$') @@ -31,19 +31,19 @@ def i_have_added_new_section(step): @step('I click the Edit link for the release date$') def i_click_the_edit_link_for_the_release_date(step): button_css = 'div.section-published-date a.edit-button' - css_click(button_css) + world.css_click(button_css) @step('I save a new section release date$') def i_save_a_new_section_release_date(step): date_css = 'input.start-date.date.hasDatepicker' time_css = 'input.start-time.time.ui-timepicker-input' - css_fill(date_css, '12/25/2013') + world.css_fill(date_css, '12/25/2013') # hit TAB to get to the time field - e = css_find(date_css).first + e = world.css_find(date_css).first e._element.send_keys(Keys.TAB) - css_fill(time_css, '12:00am') - e = css_find(time_css).first + world.css_fill(time_css, '12:00am') + e = world.css_find(time_css).first e._element.send_keys(Keys.TAB) time.sleep(float(1)) world.browser.click_link_by_text('Save') @@ -64,13 +64,13 @@ def i_see_my_section_name_with_quote_on_the_courseware_page(step): @step('I click to edit the section name$') def i_click_to_edit_section_name(step): - css_click('span.section-name-span') + world.css_click('span.section-name-span') @step('I see the complete section name with a quote in the editor$') def i_see_complete_section_name_with_quote_in_editor(step): css = '.edit-section-name' - assert world.browser.is_element_present_by_css(css, 5) + assert world.is_css_present(css) assert_equal(world.browser.find_by_css(css).value, 'Section with "Quote"') @@ -85,7 +85,7 @@ def i_see_a_release_date_for_my_section(step): import re css = 'span.published-status' - assert world.browser.is_element_present_by_css(css) + assert world.is_css_present(css) status_text = world.browser.find_by_css(css).text # e.g. 11/06/2012 at 16:25 @@ -99,7 +99,7 @@ def i_see_a_release_date_for_my_section(step): @step('I see a link to create a new subsection$') def i_see_a_link_to_create_a_new_subsection(step): css = 'a.new-subsection-item' - assert world.browser.is_element_present_by_css(css) + assert world.is_css_present(css) @step('the section release date picker is not visible$') @@ -120,10 +120,10 @@ def the_section_release_date_is_updated(step): def save_section_name(name): name_css = '.new-section-name' save_css = '.new-section-name-save' - css_fill(name_css, name) - css_click(save_css) + world.css_fill(name_css, name) + world.css_click(save_css) def see_my_section_on_the_courseware_page(name): section_css = 'span.section-name-span' - assert_css_with_text(section_css, name) + assert world.css_has_text(section_css, name) diff --git a/cms/djangoapps/contentstore/features/signup.py b/cms/djangoapps/contentstore/features/signup.py index e8d0dd8229..cd4adb79fb 100644 --- a/cms/djangoapps/contentstore/features/signup.py +++ b/cms/djangoapps/contentstore/features/signup.py @@ -17,7 +17,7 @@ def i_press_the_button_on_the_registration_form(step): submit_css = 'form#register_form button#submit' # Workaround for click not working on ubuntu # for some unknown reason. - e = css_find(submit_css) + e = world.css_find(submit_css) e.type(' ') @step('I should see be on the studio home page$') diff --git a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py b/cms/djangoapps/contentstore/features/studio-overview-togglesection.py index 060d592cfd..85a25a55ac 100644 --- a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py +++ b/cms/djangoapps/contentstore/features/studio-overview-togglesection.py @@ -49,7 +49,7 @@ def have_a_course_with_two_sections(step): def navigate_to_the_course_overview_page(step): log_into_studio(is_staff=True) course_locator = '.class-name' - css_click(course_locator) + world.css_click(course_locator) @step(u'I navigate to the courseware page of a course with multiple sections') @@ -66,44 +66,44 @@ def i_add_a_section(step): @step(u'I click the "([^"]*)" link$') def i_click_the_text_span(step, text): span_locator = '.toggle-button-sections span' - assert_true(world.browser.is_element_present_by_css(span_locator, 5)) + assert_true(world.browser.is_element_present_by_css(span_locator)) # first make sure that the expand/collapse text is the one you expected assert_equal(world.browser.find_by_css(span_locator).value, text) - css_click(span_locator) + world.css_click(span_locator) @step(u'I collapse the first section$') def i_collapse_a_section(step): collapse_locator = 'section.courseware-section a.collapse' - css_click(collapse_locator) + world.css_click(collapse_locator) @step(u'I expand the first section$') def i_expand_a_section(step): expand_locator = 'section.courseware-section a.expand' - css_click(expand_locator) + world.css_click(expand_locator) @step(u'I see the "([^"]*)" link$') def i_see_the_span_with_text(step, text): span_locator = '.toggle-button-sections span' - assert_true(world.browser.is_element_present_by_css(span_locator, 5)) - assert_equal(world.browser.find_by_css(span_locator).value, text) - assert_true(world.browser.find_by_css(span_locator).visible) + assert_true(world.is_css_present(span_locator)) + assert_equal(world.css_find(span_locator).value, text) + assert_true(world.css_visible(span_locator)) @step(u'I do not see the "([^"]*)" link$') def i_do_not_see_the_span_with_text(step, text): # Note that the span will exist on the page but not be visible span_locator = '.toggle-button-sections span' - assert_true(world.browser.is_element_present_by_css(span_locator)) - assert_false(world.browser.find_by_css(span_locator).visible) + assert_true(world.is_css_present(span_locator)) + assert_false(world.css_visible(span_locator)) @step(u'all sections are expanded$') def all_sections_are_expanded(step): subsection_locator = 'div.subsection-list' - subsections = world.browser.find_by_css(subsection_locator) + subsections = world.css_find(subsection_locator) for s in subsections: assert_true(s.visible) @@ -111,6 +111,6 @@ def all_sections_are_expanded(step): @step(u'all sections are collapsed$') def all_sections_are_expanded(step): subsection_locator = 'div.subsection-list' - subsections = world.browser.find_by_css(subsection_locator) + subsections = world.css_find(subsection_locator) for s in subsections: assert_false(s.visible) diff --git a/cms/djangoapps/contentstore/features/subsection.py b/cms/djangoapps/contentstore/features/subsection.py index 88e1424898..f5863be27b 100644 --- a/cms/djangoapps/contentstore/features/subsection.py +++ b/cms/djangoapps/contentstore/features/subsection.py @@ -15,8 +15,7 @@ def i_have_opened_a_new_course_section(step): @step('I click the New Subsection link') def i_click_the_new_subsection_link(step): - css = 'a.new-subsection-item' - css_click(css) + world.css_click('a.new-subsection-item') @step('I enter the subsection name and click save$') @@ -31,13 +30,13 @@ def i_save_subsection_name_with_quote(step): @step('I click to edit the subsection name$') def i_click_to_edit_subsection_name(step): - css_click('span.subsection-name-value') + world.css_click('span.subsection-name-value') @step('I see the complete subsection name with a quote in the editor$') def i_see_complete_subsection_name_with_quote_in_editor(step): css = '.subsection-display-name-input' - assert world.browser.is_element_present_by_css(css, 5) + assert world.is_css_present(css) assert_equal(world.browser.find_by_css(css).value, 'Subsection With "Quote"') @@ -70,11 +69,11 @@ def the_subsection_does_not_exist(step): def save_subsection_name(name): name_css = 'input.new-subsection-name-input' save_css = 'input.new-subsection-name-save' - css_fill(name_css, name) - css_click(save_css) + world.css_fill(name_css, name) + world.css_click(save_css) def see_subsection_name(name): css = 'span.subsection-name' - assert world.browser.is_element_present_by_css(css) + assert world.is_css_present(css) css = 'span.subsection-name-value' - assert_css_with_text(css, name) + assert world.css_has_text(css, name) diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py index 3009d1fa8d..e2f701d089 100644 --- a/common/djangoapps/terrain/ui_helpers.py +++ b/common/djangoapps/terrain/ui_helpers.py @@ -2,6 +2,9 @@ from lettuce import world, step import time from urllib import quote_plus from selenium.common.exceptions import WebDriverException +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait from lettuce.django import django_url @@ -9,6 +12,9 @@ from lettuce.django import django_url def wait(seconds): time.sleep(float(seconds)) +@world.absorb +def wait_for(func): + WebDriverWait(world.browser.driver, 5).until(func) @world.absorb def visit(url): @@ -24,9 +30,27 @@ def url_equals(url): def is_css_present(css_selector): return world.browser.is_element_present_by_css(css_selector, wait_time=4) +@world.absorb +def css_has_text(css_selector, text): + return world.css_text(css_selector) == text + +@world.absorb +def css_find(css): + def is_visible(driver): + return EC.visibility_of_element_located((By.CSS_SELECTOR,css,)) + + world.browser.is_element_present_by_css(css, 5) + wait_for(is_visible) + return world.browser.find_by_css(css) @world.absorb def css_click(css_selector): + ''' + First try to use the regular click method, + but if clicking in the middle of an element + doesn't work it might be that it thinks some other + element is on top of it there so click in the upper left + ''' try: world.browser.find_by_css(css_selector).click() @@ -37,6 +61,16 @@ def css_click(css_selector): time.sleep(1) world.browser.find_by_css(css_selector).click() +@world.absorb +def css_click_at(css, x=10, y=10): + ''' + A method to click at x,y coordinates of the element + rather than in the center of the element + ''' + e = css_find(css).first + e.action_chains.move_to_element_with_offset(e._element, x, y) + e.action_chains.click() + e.action_chains.perform() @world.absorb def css_fill(css_selector, text): From a58ae9b62d60450b7bf18a49531487e2150cf094 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 22 Mar 2013 13:49:50 -0400 Subject: [PATCH 179/436] Refactored studio lettuce test section.py to use more of ui helpers --- cms/djangoapps/contentstore/features/section.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cms/djangoapps/contentstore/features/section.py b/cms/djangoapps/contentstore/features/section.py index e57d50bbfe..41236f6dfd 100644 --- a/cms/djangoapps/contentstore/features/section.py +++ b/cms/djangoapps/contentstore/features/section.py @@ -105,13 +105,13 @@ def i_see_a_link_to_create_a_new_subsection(step): @step('the section release date picker is not visible$') def the_section_release_date_picker_not_visible(step): css = 'div.edit-subsection-publish-settings' - assert False, world.browser.find_by_css(css).visible + assert not world.css_visible(css) @step('the section release date is updated$') def the_section_release_date_is_updated(step): css = 'span.published-status' - status_text = world.browser.find_by_css(css).text + status_text = world.css_text(css) assert_equal(status_text,'Will Release: 12/25/2013 at 12:00am') From 00d25b684cf10bd2c8dd39a5077e365b3259bfde Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 22 Mar 2013 14:04:04 -0400 Subject: [PATCH 180/436] Moved modulestore flush code into terrain/course_helpers --- .../contentstore/features/common.py | 19 +------------------ .../contentstore/features/courses.py | 2 +- .../features/studio-overview-togglesection.py | 6 +++--- .../contentstore/features/subsection.py | 2 +- common/djangoapps/terrain/course_helpers.py | 15 +++++++++++++++ lms/djangoapps/courseware/features/common.py | 15 +-------------- 6 files changed, 22 insertions(+), 37 deletions(-) diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 4cc5759949..0b5c9acbed 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -43,7 +43,7 @@ def i_press_the_category_delete_icon(step, category): @step('I have opened a new course in Studio$') def i_have_opened_a_new_course(step): - clear_courses() + world.clear_courses() log_into_studio() create_a_course() @@ -69,23 +69,6 @@ def create_studio_user( user_profile = world.UserProfileFactory(user=studio_user) -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 clear_courses(): - flush_xmodule_store() - - def fill_in_course_info( name='Robot Super Course', org='MITx', diff --git a/cms/djangoapps/contentstore/features/courses.py b/cms/djangoapps/contentstore/features/courses.py index 8301e6708f..348cc25e97 100644 --- a/cms/djangoapps/contentstore/features/courses.py +++ b/cms/djangoapps/contentstore/features/courses.py @@ -6,7 +6,7 @@ from common import * @step('There are no courses$') def no_courses(step): - clear_courses() + world.clear_courses() @step('I click the New Course button$') diff --git a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py b/cms/djangoapps/contentstore/features/studio-overview-togglesection.py index 85a25a55ac..dc22d3ad1a 100644 --- a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py +++ b/cms/djangoapps/contentstore/features/studio-overview-togglesection.py @@ -8,13 +8,13 @@ logger = getLogger(__name__) @step(u'I have a course with no sections$') def have_a_course(step): - clear_courses() + world.clear_courses() course = world.CourseFactory.create() @step(u'I have a course with 1 section$') def have_a_course_with_1_section(step): - clear_courses() + world.clear_courses() course = world.CourseFactory.create() section = world.ItemFactory.create(parent_location=course.location) subsection1 = world.ItemFactory.create( @@ -25,7 +25,7 @@ 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() + world.clear_courses() course = world.CourseFactory.create() section = world.ItemFactory.create(parent_location=course.location) subsection1 = world.ItemFactory.create( diff --git a/cms/djangoapps/contentstore/features/subsection.py b/cms/djangoapps/contentstore/features/subsection.py index f5863be27b..2094e65ccb 100644 --- a/cms/djangoapps/contentstore/features/subsection.py +++ b/cms/djangoapps/contentstore/features/subsection.py @@ -7,7 +7,7 @@ from nose.tools import assert_equal @step('I have opened a new course section in Studio$') def i_have_opened_a_new_course_section(step): - clear_courses() + world.clear_courses() log_into_studio() create_a_course() add_section() diff --git a/common/djangoapps/terrain/course_helpers.py b/common/djangoapps/terrain/course_helpers.py index ebf5745f11..2ac3befd82 100644 --- a/common/djangoapps/terrain/course_helpers.py +++ b/common/djangoapps/terrain/course_helpers.py @@ -7,6 +7,8 @@ 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 xmodule.modulestore.django import _MODULESTORES, modulestore +from xmodule.templates import update_templates from bs4 import BeautifulSoup import os.path from urllib import quote_plus @@ -119,3 +121,16 @@ def save_the_course_content(path='/tmp'): f = open('%s/%s' % (path, filename), 'w') f.write(output) f.close + +@world.absorb +def clear_courses(): + # 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() diff --git a/lms/djangoapps/courseware/features/common.py b/lms/djangoapps/courseware/features/common.py index 2d366d462d..f015725ae9 100644 --- a/lms/djangoapps/courseware/features/common.py +++ b/lms/djangoapps/courseware/features/common.py @@ -24,7 +24,7 @@ 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() + world.clear_courses() # Create the course # We always use the same org and display name, @@ -65,19 +65,6 @@ def add_tab_to_course(step, course, extra_tab_name): display_name=str(extra_tab_name)) -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(" ", "_")) From 27d5ebf027224239c5109820794d6e5c0098930d Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 22 Mar 2013 14:27:10 -0400 Subject: [PATCH 181/436] pep8 fixes --- .../features/advanced-settings.feature | 8 +++--- .../features/advanced-settings.py | 1 + .../contentstore/features/common.py | 1 + .../contentstore/features/courses.feature | 2 +- .../contentstore/features/courses.py | 1 + .../contentstore/features/section.py | 2 +- .../contentstore/features/signup.py | 1 + .../studio-overview-togglesection.feature | 28 +++++++++---------- .../contentstore/features/subsection.py | 1 + common/djangoapps/terrain/course_helpers.py | 1 + common/djangoapps/terrain/steps.py | 3 ++ common/djangoapps/terrain/ui_helpers.py | 11 ++++++-- .../features/high-level-tabs.feature | 2 +- 13 files changed, 39 insertions(+), 23 deletions(-) diff --git a/cms/djangoapps/contentstore/features/advanced-settings.feature b/cms/djangoapps/contentstore/features/advanced-settings.feature index af97709ad0..66039e19b1 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.feature +++ b/cms/djangoapps/contentstore/features/advanced-settings.feature @@ -1,6 +1,6 @@ Feature: Advanced (manual) course policy In order to specify course policy settings for which no custom user interface exists - I want to be able to manually enter JSON key/value pairs + I want to be able to manually enter JSON key /value pairs Scenario: A course author sees default advanced settings Given I have opened a new course in Studio @@ -27,16 +27,16 @@ Feature: Advanced (manual) course policy And I reload the page Then the policy key value is changed - Scenario: Test how multi-line input appears + Scenario: Test how multi -line input appears Given I am on the Advanced Course Settings page in Studio When I create a JSON object as a value Then it is displayed as formatted And I reload the page Then it is displayed as formatted - Scenario: Test automatic quoting of non-JSON values + Scenario: Test automatic quoting of non -JSON values Given I am on the Advanced Course Settings page in Studio - When I create a non-JSON value not in quotes + When I create a non -JSON value not in quotes Then it is displayed as a string And I reload the page Then it is displayed as a string diff --git a/cms/djangoapps/contentstore/features/advanced-settings.py b/cms/djangoapps/contentstore/features/advanced-settings.py index 0232c3b908..a2708d8c96 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.py +++ b/cms/djangoapps/contentstore/features/advanced-settings.py @@ -16,6 +16,7 @@ DISPLAY_NAME_KEY = "display_name" DISPLAY_NAME_VALUE = '"Robot Super Course"' ############### ACTIONS #################### + @step('I select the Advanced Settings$') def i_select_advanced_settings(step): expand_icon_css = 'li.nav-course-settings i.icon-expand' diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 0b5c9acbed..870ab89694 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -10,6 +10,7 @@ from logging import getLogger logger = getLogger(__name__) ########### STEP HELPERS ############## + @step('I (?:visit|access|open) the Studio homepage$') def i_visit_the_studio_homepage(step): # To make this go to port 8001, put diff --git a/cms/djangoapps/contentstore/features/courses.feature b/cms/djangoapps/contentstore/features/courses.feature index 39d39b50aa..455313b0e2 100644 --- a/cms/djangoapps/contentstore/features/courses.feature +++ b/cms/djangoapps/contentstore/features/courses.feature @@ -10,4 +10,4 @@ Feature: Create Course And I fill in the new course information And I press the "Save" button Then the Courseware page has loaded in Studio - And I see a link for adding a new section \ No newline at end of file + And I see a link for adding a new section diff --git a/cms/djangoapps/contentstore/features/courses.py b/cms/djangoapps/contentstore/features/courses.py index 348cc25e97..b3b6f91bdb 100644 --- a/cms/djangoapps/contentstore/features/courses.py +++ b/cms/djangoapps/contentstore/features/courses.py @@ -43,6 +43,7 @@ def i_see_the_course_in_my_courses(step): course_css = 'span.class-name' assert world.css_has_text(course_css, 'Robot Super Course') + @step('the course is loaded$') def course_is_loaded(step): class_css = 'a.class-name' diff --git a/cms/djangoapps/contentstore/features/section.py b/cms/djangoapps/contentstore/features/section.py index 41236f6dfd..65f3bd4897 100644 --- a/cms/djangoapps/contentstore/features/section.py +++ b/cms/djangoapps/contentstore/features/section.py @@ -112,7 +112,7 @@ def the_section_release_date_picker_not_visible(step): def the_section_release_date_is_updated(step): css = 'span.published-status' status_text = world.css_text(css) - assert_equal(status_text,'Will Release: 12/25/2013 at 12:00am') + assert_equal(status_text, 'Will Release: 12/25/2013 at 12:00am') ############ HELPER METHODS ################### diff --git a/cms/djangoapps/contentstore/features/signup.py b/cms/djangoapps/contentstore/features/signup.py index cd4adb79fb..2dcf0d63fe 100644 --- a/cms/djangoapps/contentstore/features/signup.py +++ b/cms/djangoapps/contentstore/features/signup.py @@ -20,6 +20,7 @@ def i_press_the_button_on_the_registration_form(step): e = world.css_find(submit_css) e.type(' ') + @step('I should see be on the studio home page$') def i_should_see_be_on_the_studio_home_page(step): assert world.browser.find_by_css('div.inner-wrapper') diff --git a/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature b/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature index 52c10e41a8..88492d55e3 100644 --- a/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature +++ b/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature @@ -1,30 +1,30 @@ Feature: Overview Toggle Section In order to quickly view the details of a course's section or to scan the inventory of sections - As a course author - I want to toggle the visibility of each section's subsection details in the overview listing + As a course author + I want to toggle the visibility of each section's subsection details in the overview listing Scenario: The default layout for the overview page is to show sections in expanded view Given I have a course with multiple sections - When I navigate to the course overview page - Then I see the "Collapse All Sections" link - And all sections are expanded + When I navigate to the course overview page + Then I see the "Collapse All Sections" link + And all sections are expanded - Scenario: Expand/collapse for a course with no sections + Scenario: Expand /collapse for a course with no sections Given I have a course with no sections - When I navigate to the course overview page - Then I do not see the "Collapse All Sections" link + When I navigate to the course overview page + Then I do not see the "Collapse All Sections" link Scenario: Collapse link appears after creating first section of a course Given I have a course with no sections - When I navigate to the course overview page - And I add a section - Then I see the "Collapse All Sections" link - And all sections are expanded + When I navigate to the course overview page + And I add a section + Then I see the "Collapse All Sections" link + And all sections are expanded - @skip-phantom + @skip -phantom Scenario: Collapse link is not removed after last section of a course is deleted Given I have a course with 1 section - And I navigate to the course overview page + And I navigate to the course overview page When I press the "section" delete icon And I confirm the alert Then I see the "Collapse All Sections" link diff --git a/cms/djangoapps/contentstore/features/subsection.py b/cms/djangoapps/contentstore/features/subsection.py index 2094e65ccb..8695ea1c4f 100644 --- a/cms/djangoapps/contentstore/features/subsection.py +++ b/cms/djangoapps/contentstore/features/subsection.py @@ -72,6 +72,7 @@ def save_subsection_name(name): world.css_fill(name_css, name) world.css_click(save_css) + def see_subsection_name(name): css = 'span.subsection-name' assert world.is_css_present(css) diff --git a/common/djangoapps/terrain/course_helpers.py b/common/djangoapps/terrain/course_helpers.py index 2ac3befd82..85dfa85b37 100644 --- a/common/djangoapps/terrain/course_helpers.py +++ b/common/djangoapps/terrain/course_helpers.py @@ -122,6 +122,7 @@ def save_the_course_content(path='/tmp'): f.write(output) f.close + @world.absorb def clear_courses(): # Flush and initialize the module store diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index dc8d2f8b87..bf78a1d2b7 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -29,11 +29,13 @@ def i_visit_the_homepage(step): world.visit('/') assert world.is_css_present('header.global') + @step(u'I (?:visit|access|open) the dashboard$') def i_visit_the_dashboard(step): world.visit('/dashboard') assert world.is_css_present('section.container.dashboard') + @step('I should be on the dashboard page$') def i_should_be_on_the_dashboard(step): assert world.is_css_present('section.container.dashboard') @@ -97,6 +99,7 @@ def i_am_staff_for_course_by_id(step, course_id): def click_the_link_called(step, text): world.click_link(text) + @step(r'should see that the url is "([^"]*)"$') def should_have_the_url(step, url): assert_equals(world.browser.url, url) diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py index e2f701d089..6dadb976a7 100644 --- a/common/djangoapps/terrain/ui_helpers.py +++ b/common/djangoapps/terrain/ui_helpers.py @@ -12,10 +12,12 @@ from lettuce.django import django_url def wait(seconds): time.sleep(float(seconds)) + @world.absorb def wait_for(func): WebDriverWait(world.browser.driver, 5).until(func) + @world.absorb def visit(url): world.browser.visit(django_url(url)) @@ -30,23 +32,26 @@ def url_equals(url): def is_css_present(css_selector): return world.browser.is_element_present_by_css(css_selector, wait_time=4) + @world.absorb def css_has_text(css_selector, text): return world.css_text(css_selector) == text + @world.absorb def css_find(css): def is_visible(driver): - return EC.visibility_of_element_located((By.CSS_SELECTOR,css,)) + return EC.visibility_of_element_located((By.CSS_SELECTOR, css,)) world.browser.is_element_present_by_css(css, 5) wait_for(is_visible) return world.browser.find_by_css(css) + @world.absorb def css_click(css_selector): ''' - First try to use the regular click method, + First try to use the regular click method, but if clicking in the middle of an element doesn't work it might be that it thinks some other element is on top of it there so click in the upper left @@ -61,6 +66,7 @@ def css_click(css_selector): time.sleep(1) world.browser.find_by_css(css_selector).click() + @world.absorb def css_click_at(css, x=10, y=10): ''' @@ -72,6 +78,7 @@ def css_click_at(css, x=10, y=10): e.action_chains.click() e.action_chains.perform() + @world.absorb def css_fill(css_selector, text): world.browser.find_by_css(css_selector).first.fill(text) diff --git a/lms/djangoapps/courseware/features/high-level-tabs.feature b/lms/djangoapps/courseware/features/high-level-tabs.feature index 473f3f1572..c60ec7b374 100644 --- a/lms/djangoapps/courseware/features/high-level-tabs.feature +++ b/lms/djangoapps/courseware/features/high-level-tabs.feature @@ -3,7 +3,7 @@ Feature: All the high level tabs should work As a student I want to navigate through the high level tabs -Scenario: I can navigate to all high -level tabs in a course +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 From 0500ba4dd5e4a8563a31c6557f8ca331cdba8cfa Mon Sep 17 00:00:00 2001 From: Will Daly Date: Tue, 26 Mar 2013 11:17:56 -0400 Subject: [PATCH 182/436] Disabled pylint warnings for lettuce steps: * Missing docstring * Redefining name from outer scope --- cms/djangoapps/contentstore/features/advanced-settings.py | 3 +++ cms/djangoapps/contentstore/features/common.py | 3 +++ cms/djangoapps/contentstore/features/courses.py | 3 +++ cms/djangoapps/contentstore/features/section.py | 3 +++ cms/djangoapps/contentstore/features/signup.py | 3 +++ .../contentstore/features/studio-overview-togglesection.py | 3 +++ cms/djangoapps/contentstore/features/subsection.py | 3 +++ common/djangoapps/terrain/course_helpers.py | 3 +++ common/djangoapps/terrain/steps.py | 3 +++ common/djangoapps/terrain/ui_helpers.py | 3 +++ lms/djangoapps/courseware/features/common.py | 3 +++ lms/djangoapps/courseware/features/courseware.py | 3 +++ lms/djangoapps/courseware/features/courseware_common.py | 3 +++ lms/djangoapps/courseware/features/homepage.py | 3 +++ lms/djangoapps/courseware/features/login.py | 3 +++ lms/djangoapps/courseware/features/openended.py | 3 +++ lms/djangoapps/courseware/features/problems.py | 2 ++ lms/djangoapps/courseware/features/registration.py | 3 +++ lms/djangoapps/courseware/features/signup.py | 4 +++- lms/djangoapps/courseware/features/smart-accordion.py | 3 +++ lms/djangoapps/courseware/features/xqueue_setup.py | 4 +++- 21 files changed, 62 insertions(+), 2 deletions(-) diff --git a/cms/djangoapps/contentstore/features/advanced-settings.py b/cms/djangoapps/contentstore/features/advanced-settings.py index a2708d8c96..16562b6b15 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.py +++ b/cms/djangoapps/contentstore/features/advanced-settings.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from common import * import time diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 870ab89694..3878340af3 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from nose.tools import assert_true from nose.tools import assert_equal diff --git a/cms/djangoapps/contentstore/features/courses.py b/cms/djangoapps/contentstore/features/courses.py index b3b6f91bdb..5da7720945 100644 --- a/cms/djangoapps/contentstore/features/courses.py +++ b/cms/djangoapps/contentstore/features/courses.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from common import * diff --git a/cms/djangoapps/contentstore/features/section.py b/cms/djangoapps/contentstore/features/section.py index 65f3bd4897..0c0f5536a0 100644 --- a/cms/djangoapps/contentstore/features/section.py +++ b/cms/djangoapps/contentstore/features/section.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from common import * from nose.tools import assert_equal diff --git a/cms/djangoapps/contentstore/features/signup.py b/cms/djangoapps/contentstore/features/signup.py index 2dcf0d63fe..6ca358183b 100644 --- a/cms/djangoapps/contentstore/features/signup.py +++ b/cms/djangoapps/contentstore/features/signup.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from common import * diff --git a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py b/cms/djangoapps/contentstore/features/studio-overview-togglesection.py index dc22d3ad1a..7f717b731c 100644 --- a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py +++ b/cms/djangoapps/contentstore/features/studio-overview-togglesection.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from common import * from nose.tools import assert_true, assert_false, assert_equal diff --git a/cms/djangoapps/contentstore/features/subsection.py b/cms/djangoapps/contentstore/features/subsection.py index 8695ea1c4f..54f49f2fa6 100644 --- a/cms/djangoapps/contentstore/features/subsection.py +++ b/cms/djangoapps/contentstore/features/subsection.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from common import * from nose.tools import assert_equal diff --git a/common/djangoapps/terrain/course_helpers.py b/common/djangoapps/terrain/course_helpers.py index 85dfa85b37..f0df456c80 100644 --- a/common/djangoapps/terrain/course_helpers.py +++ b/common/djangoapps/terrain/course_helpers.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from .factories import * from django.conf import settings diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index bf78a1d2b7..a8a32db173 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from .course_helpers import * from .ui_helpers import * diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py index 6dadb976a7..d4d99e17b5 100644 --- a/common/djangoapps/terrain/ui_helpers.py +++ b/common/djangoapps/terrain/ui_helpers.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step import time from urllib import quote_plus diff --git a/lms/djangoapps/courseware/features/common.py b/lms/djangoapps/courseware/features/common.py index f015725ae9..f6256adfa1 100644 --- a/lms/djangoapps/courseware/features/common.py +++ b/lms/djangoapps/courseware/features/common.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from nose.tools import assert_equals, assert_in from lettuce.django import django_url diff --git a/lms/djangoapps/courseware/features/courseware.py b/lms/djangoapps/courseware/features/courseware.py index 7e99cc9f55..234f3a84d2 100644 --- a/lms/djangoapps/courseware/features/courseware.py +++ b/lms/djangoapps/courseware/features/courseware.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from lettuce.django import django_url diff --git a/lms/djangoapps/courseware/features/courseware_common.py b/lms/djangoapps/courseware/features/courseware_common.py index 6aa9559e65..4e9aa3fb7b 100644 --- a/lms/djangoapps/courseware/features/courseware_common.py +++ b/lms/djangoapps/courseware/features/courseware_common.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step diff --git a/lms/djangoapps/courseware/features/homepage.py b/lms/djangoapps/courseware/features/homepage.py index 442098c161..62e9096e70 100644 --- a/lms/djangoapps/courseware/features/homepage.py +++ b/lms/djangoapps/courseware/features/homepage.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from nose.tools import assert_in diff --git a/lms/djangoapps/courseware/features/login.py b/lms/djangoapps/courseware/features/login.py index 3e3c0efbc4..bc90ea301c 100644 --- a/lms/djangoapps/courseware/features/login.py +++ b/lms/djangoapps/courseware/features/login.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import step, world from django.contrib.auth.models import User diff --git a/lms/djangoapps/courseware/features/openended.py b/lms/djangoapps/courseware/features/openended.py index 2f14b808a3..d848eb55d7 100644 --- a/lms/djangoapps/courseware/features/openended.py +++ b/lms/djangoapps/courseware/features/openended.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from lettuce.django import django_url from nose.tools import assert_equals, assert_in diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index bdd9062ef3..b25d606c4e 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -2,6 +2,8 @@ Steps for problem.feature lettuce tests ''' +#pylint: disable=C0111 +#pylint: disable=W0621 from lettuce import world, step from lettuce.django import django_url diff --git a/lms/djangoapps/courseware/features/registration.py b/lms/djangoapps/courseware/features/registration.py index 63f044b16f..72bde65f99 100644 --- a/lms/djangoapps/courseware/features/registration.py +++ b/lms/djangoapps/courseware/features/registration.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from lettuce.django import django_url from common import TEST_COURSE_ORG, TEST_COURSE_NAME diff --git a/lms/djangoapps/courseware/features/signup.py b/lms/djangoapps/courseware/features/signup.py index d9edcb215b..5ba385ef54 100644 --- a/lms/djangoapps/courseware/features/signup.py +++ b/lms/djangoapps/courseware/features/signup.py @@ -1,5 +1,7 @@ -from lettuce import world, step +#pylint: disable=C0111 +#pylint: disable=W0621 +from lettuce import world, step @step('I fill in "([^"]*)" on the registration form with "([^"]*)"$') def when_i_fill_in_field_on_the_registration_form_with_value(step, field, value): diff --git a/lms/djangoapps/courseware/features/smart-accordion.py b/lms/djangoapps/courseware/features/smart-accordion.py index 8240a13905..63408d7683 100644 --- a/lms/djangoapps/courseware/features/smart-accordion.py +++ b/lms/djangoapps/courseware/features/smart-accordion.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from re import sub from nose.tools import assert_equals diff --git a/lms/djangoapps/courseware/features/xqueue_setup.py b/lms/djangoapps/courseware/features/xqueue_setup.py index d6d7a13a5c..90a68961ee 100644 --- a/lms/djangoapps/courseware/features/xqueue_setup.py +++ b/lms/djangoapps/courseware/features/xqueue_setup.py @@ -1,9 +1,11 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from courseware.mock_xqueue_server.mock_xqueue_server import MockXQueueServer from lettuce import before, after, world from django.conf import settings import threading - @before.all def setup_mock_xqueue_server(): From 586f566b4276b74756a0ce3bfe258ba979a45401 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Tue, 26 Mar 2013 11:54:06 -0400 Subject: [PATCH 183/436] Use advertised_start as a simple string LMS Lighthouse [#297] --- common/lib/xmodule/xmodule/course_module.py | 12 ++++++++---- common/lib/xmodule/xmodule/fields.py | 2 ++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index b1e5fa02c8..7999f8d6da 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -7,6 +7,8 @@ import requests import time from datetime import datetime +import dateutil.parser + from xmodule.modulestore import Location from xmodule.seq_module import SequenceDescriptor, SequenceModule from xmodule.timeparse import parse_time @@ -150,7 +152,7 @@ class CourseFields(object): enrollment_end = Date(help="Date that enrollment for this class is closed", scope=Scope.settings) start = Date(help="Start time when this module is visible", scope=Scope.settings) end = Date(help="Date that this class ends", scope=Scope.settings) - advertised_start = StringOrDate(help="Date that this course is advertised to start", scope=Scope.settings) + advertised_start = String(help="Date that this course is advertised to start", scope=Scope.settings) grading_policy = Object(help="Grading policy definition for this class", scope=Scope.content) show_calculator = Boolean(help="Whether to show the calculator in this course", default=False, scope=Scope.settings) display_name = String(help="Display name for this module", scope=Scope.settings) @@ -537,10 +539,12 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): announcement = self.announcement if announcement is not None: announcement = to_datetime(announcement) - if self.advertised_start is None or isinstance(self.advertised_start, basestring): + + try: + start = dateutil.parser.parse(self.advertised_start) + except (ValueError, AttributeError): start = to_datetime(self.start) - else: - start = to_datetime(self.advertised_start) + now = to_datetime(time.gmtime()) return announcement, start, now diff --git a/common/lib/xmodule/xmodule/fields.py b/common/lib/xmodule/xmodule/fields.py index 99ead854ad..0abe850d68 100644 --- a/common/lib/xmodule/xmodule/fields.py +++ b/common/lib/xmodule/xmodule/fields.py @@ -23,6 +23,8 @@ class Date(ModelType): """ if field is None: return field + elif field is "": + return None elif isinstance(field, basestring): d = dateutil.parser.parse(field) return d.utctimetuple() From 7c68508b85c0a31b0c4745172558d3075cde0a23 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Tue, 26 Mar 2013 12:42:30 -0400 Subject: [PATCH 184/436] studio - finalized tender widget styling --- cms/static/sass/elements/_tender-widget.scss | 133 +++++++++++++++++-- cms/templates/widgets/tender.html | 1 + 2 files changed, 123 insertions(+), 11 deletions(-) diff --git a/cms/static/sass/elements/_tender-widget.scss b/cms/static/sass/elements/_tender-widget.scss index fce62b8675..4d2cdea373 100644 --- a/cms/static/sass/elements/_tender-widget.scss +++ b/cms/static/sass/elements/_tender-widget.scss @@ -23,7 +23,7 @@ #tender_closer { color: $blue-l2 !important; - margin-top: 15px; + margin-top: 10px; margin-right: 5px; text-transform: uppercase; @@ -66,6 +66,7 @@ .widget-layout .content { overflow: auto; + height: auto !important; padding: 20px; } @@ -110,10 +111,20 @@ width: 97%; } +.widget-layout p.note { + text-align: right !important; + display: inline-block !important; + position: absolute !important; + right: -130px !important; + top: -5px !important; + font-size: 13px !important; + opacity: 0.80; +} + .widget-layout .form-actions { - border-top: 1px solid #ccc; - margin-top: 10px; - padding-top: 10px; + margin: 15px 0; + border: none; + padding: 0; } .widget-layout dl.form { @@ -124,19 +135,119 @@ padding-bottom: 10px; } -.widget-layout #brain_buster_captcha { +.widget-layout dl.form:last-child { + border: none; + padding-bottom: 0; + margin-bottom: 20px; +} +.widget-layout dl.form dt, .widget-layout dl.form dd { + display: inline-block; + vertical-align: middle; +} + +.widget-layout dl.form dt { + margin-right: 15px; + width: 70px; +} + +.widget-layout dl.form dd { + width: 65%; + position: relative; } // specific elements .widget-layout #discussion_body { + +} + +.widget-layout #discussion_body:before { + content: "What Question or Feedback Would You Like to Share?"; + display: block; + font-size: 14px; + margin-bottom: 5px; + color: #4c4c4c; + font-weight: 500; +} + + +.widget-layout dl#brain_buster_captcha { + float: none; + width: 100%; + border-top: 1px solid #f2f2f2; + margin-top: 10px; + padding-top: 10px; +} + +.widget-layout dl#brain_buster_captcha dd { + display: block !important; +} + +.widget-layout dl#brain_buster_captcha dd label { + display: block; + margin: 0 15px 0 0 !important; +} + +.widget-layout dl#brain_buster_captcha dd #captcha_answer { + display: block; + width: 97%%; +} + +.widget-layout .form-actions .btn-post_topic { + display: block; + width: 100%; + height: auto !important; + font-size: 16px; + font-weight: 700; + -webkit-box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset,0 0 0 rgba(0,0,0,0); + -moz-box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset,0 0 0 rgba(0,0,0,0); + box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset,0 0 0 rgba(0,0,0,0); + -webkit-transition-property: background-color,0.15s; + -moz-transition-property: background-color,0.15s; + -ms-transition-property: background-color,0.15s; + -o-transition-property: background-color,0.15s; + transition-property: background-color,0.15s; + -webkit-transition-duration: box-shadow,0.15s; + -moz-transition-duration: box-shadow,0.15s; + -ms-transition-duration: box-shadow,0.15s; + -o-transition-duration: box-shadow,0.15s; + transition-duration: box-shadow,0.15s; + -webkit-transition-timing-function: ease-out; + -moz-transition-timing-function: ease-out; + -ms-transition-timing-function: ease-out; + -o-transition-timing-function: ease-out; + transition-timing-function: ease-out; + -webkit-transition-delay: 0; + -moz-transition-delay: 0; + -ms-transition-delay: 0; + -o-transition-delay: 0; + transition-delay: 0; + border: 1px solid #34854c; + border-radius: 3px; + background-color: rgba(255,255,255,0.3); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, rgba(255,255,255,0.3)),color-stop(100%, rgba(255,255,255,0))); + background-image: -webkit-linear-gradient(top, rgba(255,255,255,0.3),rgba(255,255,255,0)); + background-image: -moz-linear-gradient(top, rgba(255,255,255,0.3),rgba(255,255,255,0)); + background-image: -ms-linear-gradient(top, rgba(255,255,255,0.3),rgba(255,255,255,0)); + background-image: -o-linear-gradient(top, rgba(255,255,255,0.3),rgba(255,255,255,0)); + background-image: linear-gradient(top, rgba(255,255,255,0.3),rgba(255,255,255,0)); + background-color: #25b85a; + -webkit-box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset; + -moz-box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset; + box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset; + color: #fff; + text-align: center; + margin-top: 20px; + padding: 10px 20px; +} + +.widget-layout .form-actions #private-discussion-opt { + float: none; + text-align: left; margin: 0 0 15px 0; } -.widget-layout .category dt, .widget-layout .category dd { - display: inline-block !important; -} - -.widget-layout .category dt { - margin-right: 15px !important; +.widget-layout .form-actions .btn-post_topic:hover, .widget-layout .form-actions .btn-post_topic:active { + background-color: #16ca57; + color: #fff; } \ No newline at end of file diff --git a/cms/templates/widgets/tender.html b/cms/templates/widgets/tender.html index 300b71701c..27cc574490 100644 --- a/cms/templates/widgets/tender.html +++ b/cms/templates/widgets/tender.html @@ -1,5 +1,6 @@ % if user.is_authenticated(): Provide Feedback + @@ -53,11 +51,14 @@ document.location.protocol + '//www.youtube.com/player_api">\x3C/script>'); - <%block name="content"> - <%include file="widgets/footer.html" /> +
    + <%include file="widgets/header.html" /> + <%block name="content"> + <%include file="widgets/sock.html" /> + <%include file="widgets/footer.html" /> +
    + <%include file="widgets/tender.html" /> <%block name="jsextra"> - - - + \ No newline at end of file diff --git a/cms/templates/course_info.html b/cms/templates/course_info.html index f9166bf166..cbf436ecc5 100644 --- a/cms/templates/course_info.html +++ b/cms/templates/course_info.html @@ -80,5 +80,4 @@
    - \ No newline at end of file diff --git a/cms/templates/howitworks.html b/cms/templates/howitworks.html index 1cf9b17710..7a819fceba 100644 --- a/cms/templates/howitworks.html +++ b/cms/templates/howitworks.html @@ -151,7 +151,7 @@
    Simple two-level outline to organize your couse. Drag and drop, and see your course at a glance.
    - + close modal @@ -164,7 +164,7 @@
    Quickly create videos, text snippets, inline discussions, and a variety of problem types.
    - + close modal @@ -177,7 +177,7 @@
    Simply set the date of a section or subsection, and Studio will publish it to your students for you.
    - + close modal diff --git a/cms/templates/widgets/footer.html b/cms/templates/widgets/footer.html index a3674cfe20..9ff98fa26b 100644 --- a/cms/templates/widgets/footer.html +++ b/cms/templates/widgets/footer.html @@ -16,13 +16,13 @@ --> diff --git a/cms/templates/widgets/sock.html b/cms/templates/widgets/sock.html new file mode 100644 index 0000000000..ff5f9c9ad4 --- /dev/null +++ b/cms/templates/widgets/sock.html @@ -0,0 +1,8 @@ +<%! from django.core.urlresolvers import reverse %> +% if user.is_authenticated(): +
    +
    +

    Sock!

    +
    +
    +% endif \ No newline at end of file From 25acab497e05ab6d9883726d0cba2ec5146fa6ae Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 27 Mar 2013 01:33:04 -0400 Subject: [PATCH 203/436] studio - corrected JQ selector for smoothscrolling in-page links --- cms/static/js/base.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 7135e2780c..211981b05a 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -81,7 +81,7 @@ $(document).ready(function () { }); // general link management - smooth scrolling page links - $('a[rel*="view"][href|="#"]').bind('click', smoothScrollLink); + $('a[rel*="view"][href^="#"]').bind('click', smoothScrollLink); // tender feedback window scrolling $('a.show-tender').bind('click', smoothScrollTop); From 2120481738489d872db41916b75b0336a77a3a9e Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 27 Mar 2013 01:34:25 -0400 Subject: [PATCH 204/436] studio - corrected JQ selector for smoothscrolling in-page links --- cms/static/js/base.js | 4 ++-- cms/templates/howitworks.html | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index bd8dc0bae8..7466233331 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -81,7 +81,7 @@ $(document).ready(function () { }); // general link management - smooth scrolling page links - $('a[rel*="view"]').bind('click', linkSmoothScroll); + $('a[rel*="view"][href^="#"]').bind('click', smoothScrollLink); // toggling overview section details @@ -148,7 +148,7 @@ $(document).ready(function () { }); }); -function linkSmoothScroll(e) { +function smoothScrollLink(e) { (e).preventDefault(); $.smoothScroll({ diff --git a/cms/templates/howitworks.html b/cms/templates/howitworks.html index 1cf9b17710..7a819fceba 100644 --- a/cms/templates/howitworks.html +++ b/cms/templates/howitworks.html @@ -151,7 +151,7 @@
    Simple two-level outline to organize your couse. Drag and drop, and see your course at a glance.
    - + close modal @@ -164,7 +164,7 @@
    Quickly create videos, text snippets, inline discussions, and a variety of problem types.
    - + close modal @@ -177,7 +177,7 @@
    Simply set the date of a section or subsection, and Studio will publish it to your students for you.
    - + close modal From 74439746cc804fc2dfe9bc6b679f0a714093aa74 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 27 Mar 2013 01:38:36 -0400 Subject: [PATCH 205/436] studio - made provide feedback conditional for logged in users --- cms/templates/widgets/footer.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cms/templates/widgets/footer.html b/cms/templates/widgets/footer.html index 9ff98fa26b..18ecf2bc39 100644 --- a/cms/templates/widgets/footer.html +++ b/cms/templates/widgets/footer.html @@ -18,9 +18,11 @@ + % if user.is_authenticated(): + + % endif From e3c646492c1147f74fec0e9df45f58f1c9fe892e Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 27 Mar 2013 01:39:24 -0400 Subject: [PATCH 206/436] studio - made provide feedback conditional for logged in users --- cms/templates/widgets/footer.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cms/templates/widgets/footer.html b/cms/templates/widgets/footer.html index a3674cfe20..c0cf8f73a6 100644 --- a/cms/templates/widgets/footer.html +++ b/cms/templates/widgets/footer.html @@ -18,9 +18,11 @@ + % if user.is_authenticated(): + + % endif From 2c0e5b82ff2535770a5ca605aa1b1bd521c756d4 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 27 Mar 2013 07:29:22 -0400 Subject: [PATCH 207/436] Return a 403 when an anonymous user attempts to hit modx_dispatch. Fixes https://www.pivotaltracker.com/story/show/46916015 and https://www.pivotaltracker.com/story/show/46916029 --- lms/djangoapps/courseware/module_render.py | 4 +++ .../courseware/tests/test_module_render.py | 31 +++++++++---------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 973940d784..4747f7b341 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -8,6 +8,7 @@ from functools import partial from django.conf import settings from django.contrib.auth.models import User +from django.core.exceptions import PermissionDenied from django.core.urlresolvers import reverse from django.http import Http404 from django.http import HttpResponse @@ -412,6 +413,9 @@ def modx_dispatch(request, dispatch, location, course_id): if not Location.is_valid(location): raise Http404("Invalid location") + if not request.user.is_authenticated(): + raise PermissionDenied + # Check for submitted files and basic file size checks p = request.POST.copy() if request.FILES: diff --git a/lms/djangoapps/courseware/tests/test_module_render.py b/lms/djangoapps/courseware/tests/test_module_render.py index 3a3a7ac5ea..90ca796a2f 100644 --- a/lms/djangoapps/courseware/tests/test_module_render.py +++ b/lms/djangoapps/courseware/tests/test_module_render.py @@ -1,14 +1,7 @@ -import logging -from mock import MagicMock, patch +from mock import MagicMock import json -import factory -import unittest -from nose.tools import set_trace -from django.http import Http404, HttpResponse, HttpRequest -from django.conf import settings -from django.contrib.auth.models import User -from django.test.client import Client +from django.http import Http404, HttpResponse from django.conf import settings from django.test import TestCase from django.test.client import RequestFactory @@ -16,13 +9,9 @@ from django.core.urlresolvers import reverse from django.test.utils import override_settings from xmodule.modulestore.exceptions import ItemNotFoundError -from xmodule.exceptions import NotFoundError -from xmodule.modulestore import Location import courseware.module_render as render -from xmodule.modulestore.django import modulestore, _MODULESTORES -from xmodule.seq_module import SequenceModule +from xmodule.modulestore.django import modulestore from courseware.tests.tests import PageLoader -from student.models import Registration from courseware.model_data import ModelDataCache from .factories import UserFactory @@ -52,7 +41,6 @@ TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR) class ModuleRenderTestCase(PageLoader): def setUp(self): self.location = ['i4x', 'edX', 'toy', 'chapter', 'Overview'] - self._MODULESTORES = {} self.course_id = 'edX/toy/2012_Fall' self.toy_course = modulestore().get_course(self.course_id) @@ -104,12 +92,23 @@ class ModuleRenderTestCase(PageLoader): self.assertEquals(render.get_score_bucket(11, 10), 'incorrect') self.assertEquals(render.get_score_bucket(-1, 10), 'incorrect') + def test_anonymous_modx_dispatch(self): + dispatch_url = reverse( + 'modx_dispatch', + args=[ + 'edX/toy/2012_Fall', + 'i4x://edX/toy/videosequence/Toy_Videos', + 'goto_position' + ] + ) + response = self.client.post(dispatch_url, {'position': 2}) + self.assertEquals(403, response.status_code) + @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) class TestTOC(TestCase): """Check the Table of Contents for a course""" def setUp(self): - self._MODULESTORES = {} # Toy courses should be loaded self.course_name = 'edX/toy/2012_Fall' From 521843876efb005303e8ff7423442eb9830ab99e Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 27 Mar 2013 08:10:25 -0400 Subject: [PATCH 208/436] Make the django_comment_client return errors that can't be parsed as JSON just as simple strings when in an ajax context --- .../django_comment_client/middleware.py | 16 +++++++++- .../tests/test_middleware.py | 32 +++++++++---------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/lms/djangoapps/django_comment_client/middleware.py b/lms/djangoapps/django_comment_client/middleware.py index abf2d40cab..b9efc1589e 100644 --- a/lms/djangoapps/django_comment_client/middleware.py +++ b/lms/djangoapps/django_comment_client/middleware.py @@ -1,10 +1,24 @@ from comment_client import CommentClientError from django_comment_client.utils import JsonError import json +import logging + +log = logging.getLogger(__name__) class AjaxExceptionMiddleware(object): + """ + Middleware that captures CommentClientErrors during ajax requests + and tranforms them into json responses + """ def process_exception(self, request, exception): + """ + Processes CommentClientErrors in ajax requests. If the request is an ajax request, + returns a http response that encodes the error as json + """ if isinstance(exception, CommentClientError) and request.is_ajax(): - return JsonError(json.loads(exception.message)) + try: + return JsonError(json.loads(exception.message)) + except ValueError: + return JsonError(exception.message) return None diff --git a/lms/djangoapps/django_comment_client/tests/test_middleware.py b/lms/djangoapps/django_comment_client/tests/test_middleware.py index 55e4c72c75..ab9517c160 100644 --- a/lms/djangoapps/django_comment_client/tests/test_middleware.py +++ b/lms/djangoapps/django_comment_client/tests/test_middleware.py @@ -1,7 +1,3 @@ -import string -import random -import collections - from django.test import TestCase import comment_client @@ -13,17 +9,19 @@ class AjaxExceptionTestCase(TestCase): # TODO: check whether the correct error message is produced. # The error message should be the same as the argument to CommentClientError - def setUp(self): - self.a = middleware.AjaxExceptionMiddleware() - self.request1 = django.http.HttpRequest() - self.request0 = django.http.HttpRequest() - self.exception1 = comment_client.CommentClientError('{}') - self.exception0 = ValueError() - self.request1.META['HTTP_X_REQUESTED_WITH'] = "XMLHttpRequest" - self.request0.META['HTTP_X_REQUESTED_WITH'] = "SHADOWFAX" + def setUp(self): + self.a = middleware.AjaxExceptionMiddleware() + self.request1 = django.http.HttpRequest() + self.request0 = django.http.HttpRequest() + self.exception1 = comment_client.CommentClientError('{}') + self.exception2 = comment_client.CommentClientError('Foo!') + self.exception0 = ValueError() + self.request1.META['HTTP_X_REQUESTED_WITH'] = "XMLHttpRequest" + self.request0.META['HTTP_X_REQUESTED_WITH'] = "SHADOWFAX" - def test_process_exception(self): - self.assertIsInstance(self.a.process_exception(self.request1, self.exception1), middleware.JsonError) - self.assertIsNone(self.a.process_exception(self.request1, self.exception0)) - self.assertIsNone(self.a.process_exception(self.request0, self.exception1)) - self.assertIsNone(self.a.process_exception(self.request0, self.exception0)) + def test_process_exception(self): + self.assertIsInstance(self.a.process_exception(self.request1, self.exception1), middleware.JsonError) + self.assertIsInstance(self.a.process_exception(self.request1, self.exception2), middleware.JsonError) + self.assertIsNone(self.a.process_exception(self.request1, self.exception0)) + self.assertIsNone(self.a.process_exception(self.request0, self.exception1)) + self.assertIsNone(self.a.process_exception(self.request0, self.exception0)) From 285e3ee1edfbb5cbb22f821b05b7a752aab25c73 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 27 Mar 2013 10:49:47 -0400 Subject: [PATCH 209/436] Capa response now displays full stack trace on student input error if the user is a staff member. Otherwise, it displays just the exception message. --- common/lib/capa/capa/responsetypes.py | 7 +++--- common/lib/xmodule/xmodule/capa_module.py | 16 +++++++++++- .../xmodule/xmodule/tests/test_capa_module.py | 25 +++++++++++++++++++ 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 2c556211f8..465c212b30 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -17,6 +17,7 @@ import logging import numbers import numpy import os +import sys import random import re import requests @@ -1233,9 +1234,9 @@ def sympy_check2(): log.debug(msg, exc_info=True) log.debug(traceback.format_exc()) - # Notify student - raise StudentInputError( - "Error: Problem could not be evaluated with your input") + # Notify student with a student input error + _, _, traceback_obj = sys.exc_info() + raise StudentInputError, StudentInputError(err.message), traceback_obj #----------------------------------------------------------------------------- diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index da8b5b4f96..203e14fdc1 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -725,9 +725,23 @@ class CapaModule(CapaFields, XModule): try: correct_map = self.lcp.grade_answers(answers) self.set_state_from_lcp() + except StudentInputError as inst: log.exception("StudentInputError in capa_module:problem_check") - return {'success': inst.message} + + # If the user is a staff member, include + # the full exception, including traceback, + # in the response + if self.system.user_is_staff: + msg = traceback.format_exc() + + # Otherwise, display just the error message, + # without a stack trace + else: + msg = inst.message + + return {'success': msg } + except Exception, err: if self.system.DEBUG: msg = "Error checking problem: " + str(err) diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py index d2458cb3d0..d769b65914 100644 --- a/common/lib/xmodule/xmodule/tests/test_capa_module.py +++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py @@ -505,6 +505,9 @@ class CapaModuleTest(unittest.TestCase): def test_check_problem_student_input_error(self): module = CapaFactory.create(attempts=1) + # Ensure that the user is NOT staff + module.system.user_is_staff = False + # Simulate a student input exception with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade: mock_grade.side_effect = capa.responsetypes.StudentInputError('test error') @@ -515,10 +518,32 @@ class CapaModuleTest(unittest.TestCase): # Expect an AJAX alert message in 'success' self.assertTrue('test error' in result['success']) + # We do NOT include traceback information for + # a non-staff user + self.assertFalse('Traceback' in result['success']) + # Expect that the number of attempts is NOT incremented self.assertEqual(module.attempts, 1) + def test_check_problem_student_input_error_with_staff_user(self): + module = CapaFactory.create(attempts=1) + # Ensure that the user IS staff + module.system.user_is_staff = True + + # Simulate a student input exception + with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade: + mock_grade.side_effect = capa.responsetypes.StudentInputError('test error') + + get_request_dict = { CapaFactory.input_key(): '3.14'} + result = module.check_problem(get_request_dict) + + # Expect an AJAX alert message in 'success' + self.assertTrue('test error' in result['success']) + + # We DO include traceback information for staff users + self.assertTrue('Traceback' in result['success']) + def test_reset_problem(self): module = CapaFactory.create(done=True) module.new_lcp = Mock(wraps=module.new_lcp) From 754e30240d7c62c7efd80c38d552c4d168d23262 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 27 Mar 2013 11:01:25 -0400 Subject: [PATCH 210/436] studio - adjusting tender widget window height based on field removal --- cms/static/sass/elements/_tender-widget.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cms/static/sass/elements/_tender-widget.scss b/cms/static/sass/elements/_tender-widget.scss index 146d5b4111..478489d0e8 100644 --- a/cms/static/sass/elements/_tender-widget.scss +++ b/cms/static/sass/elements/_tender-widget.scss @@ -9,6 +9,7 @@ #tender_window { @include border-radius(3px); @include box-shadow(0 2px 3px $shadow); + height: 650px !important; background: $white !important; border: 1px solid $gray; } @@ -72,7 +73,7 @@ .widget-layout .flash { margin: -10px 0 15px 0; - padding: 5px 10px !important; + padding: 10px 20px !important; background-image: none !important; } From 8252ba15df79f3f2b213d8afed58c3a152f0bb2b Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 27 Mar 2013 11:02:30 -0400 Subject: [PATCH 211/436] Changed error message for StudentInputError for non-staff to a generic message. Otherwise, the default exception messages are cryptic for students (e.g. "cannot convert string to float") --- common/lib/xmodule/xmodule/capa_module.py | 4 ++-- common/lib/xmodule/xmodule/tests/test_capa_module.py | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 203e14fdc1..c3159bb3ee 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -735,10 +735,10 @@ class CapaModule(CapaFields, XModule): if self.system.user_is_staff: msg = traceback.format_exc() - # Otherwise, display just the error message, + # Otherwise, display just an error message, # without a stack trace else: - msg = inst.message + msg = "Error: Problem could not be evaluated with your input" return {'success': msg } diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py index d769b65914..b5e1ff311c 100644 --- a/common/lib/xmodule/xmodule/tests/test_capa_module.py +++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py @@ -516,11 +516,8 @@ class CapaModuleTest(unittest.TestCase): result = module.check_problem(get_request_dict) # Expect an AJAX alert message in 'success' - self.assertTrue('test error' in result['success']) - - # We do NOT include traceback information for - # a non-staff user - self.assertFalse('Traceback' in result['success']) + expected_msg = 'Error: Problem could not be evaluated with your input' + self.assertEqual(expected_msg, result['success']) # Expect that the number of attempts is NOT incremented self.assertEqual(module.attempts, 1) From 5bc44e50da28ca31f10bbf447fd112c948717f86 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 27 Mar 2013 11:13:31 -0400 Subject: [PATCH 212/436] Changed error messages to account for NumericalResponse formatting, which is the only other response type to use StudentInputError. --- common/lib/capa/capa/responsetypes.py | 2 +- common/lib/xmodule/xmodule/capa_module.py | 2 +- common/lib/xmodule/xmodule/tests/test_capa_module.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 465c212b30..08cfa8b9d9 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -834,7 +834,7 @@ class NumericalResponse(LoncapaResponse): import sys type, value, traceback = sys.exc_info() - raise StudentInputError, ("Invalid input: could not interpret '%s' as a number" % + raise StudentInputError, ("Could not interpret '%s' as a number" % cgi.escape(student_answer)), traceback if correct: diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index c3159bb3ee..773ae73d59 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -738,7 +738,7 @@ class CapaModule(CapaFields, XModule): # Otherwise, display just an error message, # without a stack trace else: - msg = "Error: Problem could not be evaluated with your input" + msg = "Error: %s" % str(inst.message) return {'success': msg } diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py index b5e1ff311c..3617086f85 100644 --- a/common/lib/xmodule/xmodule/tests/test_capa_module.py +++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py @@ -516,7 +516,7 @@ class CapaModuleTest(unittest.TestCase): result = module.check_problem(get_request_dict) # Expect an AJAX alert message in 'success' - expected_msg = 'Error: Problem could not be evaluated with your input' + expected_msg = 'Error: test error' self.assertEqual(expected_msg, result['success']) # Expect that the number of attempts is NOT incremented From 0f5e8c5f3bb8acbe8b4396ab172ca1740b7b89fd Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 27 Mar 2013 11:17:21 -0400 Subject: [PATCH 213/436] pep8 fixes --- common/lib/capa/capa/responsetypes.py | 8 ++--- common/lib/xmodule/xmodule/capa_module.py | 10 +++--- .../xmodule/xmodule/tests/test_capa_module.py | 32 +++++++++---------- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 08cfa8b9d9..e79399c5fc 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1141,9 +1141,9 @@ def sympy_check2(): correct = [] messages = [] for input_dict in input_list: - correct.append('correct' + correct.append('correct' if input_dict['ok'] else 'incorrect') - msg = (self.clean_message_html(input_dict['msg']) + msg = (self.clean_message_html(input_dict['msg']) if 'msg' in input_dict else None) messages.append(msg) @@ -1168,7 +1168,7 @@ def sympy_check2(): correct_map.set_overall_message(overall_message) for k in range(len(idset)): - npoints = (self.maxpoints[idset[k]] + npoints = (self.maxpoints[idset[k]] if correct[k] == 'correct' else 0) correct_map.set(idset[k], correct[k], msg=messages[k], npoints=npoints) @@ -2085,7 +2085,7 @@ class AnnotationResponse(LoncapaResponse): option_scoring = dict([(option['id'], { 'correctness': choices.get(option['choice']), 'points': scoring.get(option['choice']) - }) for option in self._find_options(inputfield) ]) + }) for option in self._find_options(inputfield)]) scoring_map[inputfield.get('id')] = option_scoring diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 773ae73d59..af29c4c2fe 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -576,7 +576,7 @@ class CapaModule(CapaFields, XModule): # save any state changes that may occur self.set_state_from_lcp() return response - + def get_answer(self, get): ''' @@ -731,7 +731,7 @@ class CapaModule(CapaFields, XModule): # If the user is a staff member, include # the full exception, including traceback, - # in the response + # in the response if self.system.user_is_staff: msg = traceback.format_exc() @@ -740,7 +740,7 @@ class CapaModule(CapaFields, XModule): else: msg = "Error: %s" % str(inst.message) - return {'success': msg } + return {'success': msg} except Exception, err: if self.system.DEBUG: @@ -792,7 +792,7 @@ class CapaModule(CapaFields, XModule): event_info['answers'] = answers # Too late. Cannot submit - if self.closed() and not self.max_attempts ==0: + if self.closed() and not self.max_attempts == 0: event_info['failure'] = 'closed' self.system.track_function('save_problem_fail', event_info) return {'success': False, @@ -812,7 +812,7 @@ class CapaModule(CapaFields, XModule): self.system.track_function('save_problem_success', event_info) msg = "Your answers have been saved" - if not self.max_attempts ==0: + if not self.max_attempts == 0: msg += " but not graded. Hit 'Check' to grade them." return {'success': True, 'msg': msg} diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py index 3617086f85..18d20a2756 100644 --- a/common/lib/xmodule/xmodule/tests/test_capa_module.py +++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py @@ -407,7 +407,7 @@ class CapaModuleTest(unittest.TestCase): mock_html.return_value = "Test HTML" # Check the problem - get_request_dict = { CapaFactory.input_key(): '3.14'} + get_request_dict = {CapaFactory.input_key(): '3.14'} result = module.check_problem(get_request_dict) # Expect that the problem is marked correct @@ -428,7 +428,7 @@ class CapaModuleTest(unittest.TestCase): mock_is_correct.return_value = False # Check the problem - get_request_dict = { CapaFactory.input_key(): '0'} + get_request_dict = {CapaFactory.input_key(): '0'} result = module.check_problem(get_request_dict) # Expect that the problem is marked correct @@ -446,7 +446,7 @@ class CapaModuleTest(unittest.TestCase): with patch('xmodule.capa_module.CapaModule.closed') as mock_closed: mock_closed.return_value = True with self.assertRaises(xmodule.exceptions.NotFoundError): - get_request_dict = { CapaFactory.input_key(): '3.14'} + get_request_dict = {CapaFactory.input_key(): '3.14'} module.check_problem(get_request_dict) # Expect that number of attempts NOT incremented @@ -492,7 +492,7 @@ class CapaModuleTest(unittest.TestCase): mock_is_queued.return_value = True mock_get_queuetime.return_value = datetime.datetime.now() - get_request_dict = { CapaFactory.input_key(): '3.14'} + get_request_dict = {CapaFactory.input_key(): '3.14'} result = module.check_problem(get_request_dict) # Expect an AJAX alert message in 'success' @@ -512,7 +512,7 @@ class CapaModuleTest(unittest.TestCase): with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade: mock_grade.side_effect = capa.responsetypes.StudentInputError('test error') - get_request_dict = { CapaFactory.input_key(): '3.14'} + get_request_dict = {CapaFactory.input_key(): '3.14'} result = module.check_problem(get_request_dict) # Expect an AJAX alert message in 'success' @@ -532,7 +532,7 @@ class CapaModuleTest(unittest.TestCase): with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade: mock_grade.side_effect = capa.responsetypes.StudentInputError('test error') - get_request_dict = { CapaFactory.input_key(): '3.14'} + get_request_dict = {CapaFactory.input_key(): '3.14'} result = module.check_problem(get_request_dict) # Expect an AJAX alert message in 'success' @@ -540,7 +540,7 @@ class CapaModuleTest(unittest.TestCase): # We DO include traceback information for staff users self.assertTrue('Traceback' in result['success']) - + def test_reset_problem(self): module = CapaFactory.create(done=True) module.new_lcp = Mock(wraps=module.new_lcp) @@ -595,11 +595,11 @@ class CapaModuleTest(unittest.TestCase): module = CapaFactory.create(done=False) # Save the problem - get_request_dict = { CapaFactory.input_key(): '3.14'} + get_request_dict = {CapaFactory.input_key(): '3.14'} result = module.save_problem(get_request_dict) # Expect that answers are saved to the problem - expected_answers = { CapaFactory.answer_key(): '3.14'} + expected_answers = {CapaFactory.answer_key(): '3.14'} self.assertEqual(module.lcp.student_answers, expected_answers) # Expect that the result is success @@ -614,7 +614,7 @@ class CapaModuleTest(unittest.TestCase): mock_closed.return_value = True # Try to save the problem - get_request_dict = { CapaFactory.input_key(): '3.14'} + get_request_dict = {CapaFactory.input_key(): '3.14'} result = module.save_problem(get_request_dict) # Expect that the result is failure @@ -625,7 +625,7 @@ class CapaModuleTest(unittest.TestCase): module = CapaFactory.create(rerandomize='always', done=True) # Try to save - get_request_dict = { CapaFactory.input_key(): '3.14'} + get_request_dict = {CapaFactory.input_key(): '3.14'} result = module.save_problem(get_request_dict) # Expect that we cannot save @@ -636,7 +636,7 @@ class CapaModuleTest(unittest.TestCase): module = CapaFactory.create(rerandomize='never', done=True) # Try to save - get_request_dict = { CapaFactory.input_key(): '3.14'} + get_request_dict = {CapaFactory.input_key(): '3.14'} result = module.save_problem(get_request_dict) # Expect that we succeed @@ -648,7 +648,7 @@ class CapaModuleTest(unittest.TestCase): # Just in case, we also check what happens if we have # more attempts than allowed. attempts = random.randint(1, 10) - module = CapaFactory.create(attempts=attempts -1, max_attempts=attempts) + module = CapaFactory.create(attempts=attempts - 1, max_attempts=attempts) self.assertEqual(module.check_button_name(), "Final Check") module = CapaFactory.create(attempts=attempts, max_attempts=attempts) @@ -658,14 +658,14 @@ class CapaModuleTest(unittest.TestCase): self.assertEqual(module.check_button_name(), "Final Check") # Otherwise, button name is "Check" - module = CapaFactory.create(attempts=attempts -2, max_attempts=attempts) + module = CapaFactory.create(attempts=attempts - 2, max_attempts=attempts) self.assertEqual(module.check_button_name(), "Check") - module = CapaFactory.create(attempts=attempts -3, max_attempts=attempts) + module = CapaFactory.create(attempts=attempts - 3, max_attempts=attempts) self.assertEqual(module.check_button_name(), "Check") # If no limit on attempts, then always show "Check" - module = CapaFactory.create(attempts=attempts -3) + module = CapaFactory.create(attempts=attempts - 3) self.assertEqual(module.check_button_name(), "Check") module = CapaFactory.create(attempts=0) From 6edee96caf528b73f2d9c097800ba8b867942de2 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 27 Mar 2013 11:24:16 -0400 Subject: [PATCH 214/436] Added "Staff Debug Info" prefix to traceback message. --- common/lib/xmodule/xmodule/capa_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index af29c4c2fe..d7346faa67 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -733,7 +733,7 @@ class CapaModule(CapaFields, XModule): # the full exception, including traceback, # in the response if self.system.user_is_staff: - msg = traceback.format_exc() + msg = "Staff debug info: %s" % traceback.format_exc() # Otherwise, display just an error message, # without a stack trace From 7101c76016e4c42b18ba5858556b836da6fde66b Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 27 Mar 2013 12:02:32 -0400 Subject: [PATCH 215/436] comment on rewrite links change --- .../combined_open_ended_modulev1.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py index f88fd9ab82..6fe37b9525 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py @@ -365,6 +365,10 @@ class CombinedOpenEndedV1Module(): html = self.current_task.get_html(self.system) return_html = html try: + #Without try except block, get this error: + # File "/home/vik/mitx_all/mitx/common/lib/xmodule/xmodule/x_module.py", line 263, in rewrite_content_links + # if link.startswith(XASSET_SRCREF_PREFIX): + # Placing try except so that if the error is fixed, this code will start working again. return_html = rewrite_links(html, self.rewrite_content_links) except: pass @@ -786,7 +790,7 @@ class CombinedOpenEndedV1Descriptor(): template_dir_name = "combinedopenended" def __init__(self, system): - self.system =system + self.system = system @classmethod def definition_from_xml(cls, xml_object, system): From 3a4bdf19fb6733dae57b557223481601dbbe8efb Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 27 Mar 2013 12:26:49 -0400 Subject: [PATCH 216/436] studio - tweaking footer navigation for tender widget label --- cms/templates/widgets/footer.html | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/cms/templates/widgets/footer.html b/cms/templates/widgets/footer.html index c0cf8f73a6..c5fc81957f 100644 --- a/cms/templates/widgets/footer.html +++ b/cms/templates/widgets/footer.html @@ -20,12 +20,9 @@ % if user.is_authenticated(): - % endif - + % endif From 15ea32b095abe4d033075640121ad418ced0179d Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 27 Mar 2013 12:53:58 -0400 Subject: [PATCH 217/436] Fixed bug 294, caused by unicode encoding error when creating logging strings. Added unit tests that verify the fix. --- common/djangoapps/student/tests/test_login.py | 107 ++++++++++++++++++ common/djangoapps/student/views.py | 8 +- 2 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 common/djangoapps/student/tests/test_login.py diff --git a/common/djangoapps/student/tests/test_login.py b/common/djangoapps/student/tests/test_login.py new file mode 100644 index 0000000000..dda58a4462 --- /dev/null +++ b/common/djangoapps/student/tests/test_login.py @@ -0,0 +1,107 @@ +from django.test import TestCase +from django.test.client import Client +from django.core.urlresolvers import reverse +from django.contrib.auth.models import User +from student.models import Registration, UserProfile +import json + +class LoginTest(TestCase): + ''' + Test student.views.login_user() view + ''' + + def setUp(self): + + # Create one user and save it to the database + self.user = User.objects.create_user('test', 'test@edx.org', 'test_password') + self.user.is_active = True + self.user.save() + + # Create a registration for the user + Registration().register(self.user) + + # Create a profile for the user + UserProfile(user=self.user).save() + + # Create the test client + self.client = Client() + + # Store the login url + self.url = reverse('login') + + def test_login_success(self): + response = self._login_response('test@edx.org', 'test_password') + self._assert_response(response, success=True) + + def test_login_success_unicode_email(self): + unicode_email = u'test@edx.org' + unichr(40960) + + self.user.email = unicode_email + self.user.save() + + response = self._login_response(unicode_email, 'test_password') + self._assert_response(response, success=True) + + + def test_login_fail_no_user_exists(self): + response = self._login_response('not_a_user@edx.org', 'test_password') + self._assert_response(response, success=False, + value='Email or password is incorrect') + + def test_login_fail_wrong_password(self): + response = self._login_response('test@edx.org', 'wrong_password') + self._assert_response(response, success=False, + value='Email or password is incorrect') + + def test_login_not_activated(self): + + # De-activate the user + self.user.is_active = False + self.user.save() + + # Should now be unable to login + response = self._login_response('test@edx.org', 'test_password') + self._assert_response(response, success=False, + value="This account has not been activated") + + + def test_login_unicode_email(self): + unicode_email = u'test@edx.org' + unichr(40960) + response = self._login_response(unicode_email, 'test_password') + self._assert_response(response, success=False) + + def test_login_unicode_password(self): + unicode_password = u'test_password' + unichr(1972) + response = self._login_response('test@edx.org', unicode_password) + self._assert_response(response, success=False) + + def _login_response(self, email, password): + post_params = {'email': email, 'password': password} + return self.client.post(self.url, post_params) + + def _assert_response(self, response, success=None, value=None): + ''' + Assert that the response had status 200 and returned a valid + JSON-parseable dict. + + If success is provided, assert that the response had that + value for 'success' in the JSON dict. + + If value is provided, assert that the response contained that + value for 'value' in the JSON dict. + ''' + self.assertEqual(response.status_code, 200) + + try: + response_dict = json.loads(response.content) + except ValueError: + self.fail("Could not parse response content as JSON: %s" + % str(response.content)) + + if success is not None: + self.assertEqual(response_dict['success'], success) + + if value is not None: + msg = ("'%s' did not contain '%s'" % + (str(response_dict['value']), str(value))) + self.assertTrue(value in response_dict['value'], msg) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 5dbaf5d2c2..84730421e8 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -369,14 +369,14 @@ def login_user(request, error=""): try: user = User.objects.get(email=email) except User.DoesNotExist: - log.warning("Login failed - Unknown user email: {0}".format(email)) + log.warning(u"Login failed - Unknown user email: {0}".format(email)) return HttpResponse(json.dumps({'success': False, 'value': 'Email or password is incorrect.'})) # TODO: User error message username = user.username user = authenticate(username=username, password=password) if user is None: - log.warning("Login failed - password for {0} is invalid".format(email)) + log.warning(u"Login failed - password for {0} is invalid".format(email)) return HttpResponse(json.dumps({'success': False, 'value': 'Email or password is incorrect.'})) @@ -392,7 +392,7 @@ def login_user(request, error=""): log.critical("Login failed - Could not create session. Is memcached running?") log.exception(e) - log.info("Login success - {0} ({1})".format(username, email)) + log.info(u"Login success - {0} ({1})".format(username, email)) try_change_enrollment(request) @@ -400,7 +400,7 @@ def login_user(request, error=""): return HttpResponse(json.dumps({'success': True})) - log.warning("Login failed - Account not active for user {0}, resending activation".format(username)) + log.warning(u"Login failed - Account not active for user {0}, resending activation".format(username)) reactivation_email_for_user(user) not_activated_msg = "This account has not been activated. We have " + \ From 227a5e8266ddc72e9719eb2b6035a12ee0788c56 Mon Sep 17 00:00:00 2001 From: cahrens Date: Wed, 27 Mar 2013 12:56:06 -0400 Subject: [PATCH 218/436] Delete converters, move unit tests to test_fields, add new additional test cases. --- .../tests/test_course_settings.py | 75 +++-------------- .../models/settings/course_details.py | 28 +++++-- .../models/settings/course_grading.py | 2 - common/djangoapps/util/converters.py | 37 --------- common/lib/xmodule/xmodule/fields.py | 1 - .../lib/xmodule/xmodule/tests/test_fields.py | 80 +++++++++++++++++++ 6 files changed, 113 insertions(+), 110 deletions(-) delete mode 100644 common/djangoapps/util/converters.py create mode 100644 common/lib/xmodule/xmodule/tests/test_fields.py diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index 2e7bc5db83..fe90ad18aa 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -1,8 +1,6 @@ import datetime import json import copy -from util import converters -from util.converters import jsdate_to_time from django.contrib.auth.models import User from django.test.client import Client @@ -15,69 +13,13 @@ from models.settings.course_details import (CourseDetails, from models.settings.course_grading import CourseGradingModel from contentstore.utils import get_modulestore -from django.test import TestCase from .utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory from models.settings.course_metadata import CourseMetadata from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.django import modulestore -import time - - -# YYYY-MM-DDThh:mm:ss.s+/-HH:MM -class ConvertersTestCase(TestCase): - @staticmethod - def struct_to_datetime(struct_time): - return datetime.datetime(struct_time.tm_year, struct_time.tm_mon, - struct_time.tm_mday, struct_time.tm_hour, - struct_time.tm_min, struct_time.tm_sec, tzinfo=UTC()) - - def compare_dates(self, date1, date2, expected_delta): - dt1 = ConvertersTestCase.struct_to_datetime(date1) - dt2 = ConvertersTestCase.struct_to_datetime(date2) - self.assertEqual(dt1 - dt2, expected_delta, str(date1) + "-" - + str(date2) + "!=" + str(expected_delta)) - - def test_iso_to_struct(self): - '''Test conversion from iso compatible date strings to struct_time''' - self.compare_dates(converters.jsdate_to_time("2013-01-01"), - converters.jsdate_to_time("2012-12-31"), - datetime.timedelta(days=1)) - self.compare_dates(converters.jsdate_to_time("2013-01-01T00"), - converters.jsdate_to_time("2012-12-31T23"), - datetime.timedelta(hours=1)) - self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00"), - converters.jsdate_to_time("2012-12-31T23:59"), - datetime.timedelta(minutes=1)) - self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00:00"), - converters.jsdate_to_time("2012-12-31T23:59:59"), - datetime.timedelta(seconds=1)) - self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00:00Z"), - converters.jsdate_to_time("2012-12-31T23:59:59Z"), - datetime.timedelta(seconds=1)) - self.compare_dates( - converters.jsdate_to_time("2012-12-31T23:00:01-01:00"), - converters.jsdate_to_time("2013-01-01T00:00:00+01:00"), - datetime.timedelta(hours=1, seconds=1)) - - def test_struct_to_iso(self): - ''' - Test converting time reprs to iso dates - ''' - self.assertEqual( - converters.time_to_isodate( - time.strptime("2012-12-31T23:59:59Z", "%Y-%m-%dT%H:%M:%SZ")), - "2012-12-31T23:59:59Z") - self.assertEqual( - converters.time_to_isodate( - jsdate_to_time("2012-12-31T23:59:59Z")), - "2012-12-31T23:59:59Z") - self.assertEqual( - converters.time_to_isodate( - jsdate_to_time("2012-12-31T23:00:01-01:00")), - "2013-01-01T00:00:01Z") - +from xmodule.fields import Date class CourseTestCase(ModuleStoreTestCase): def setUp(self): @@ -206,17 +148,24 @@ class CourseDetailsViewTest(CourseTestCase): self.assertEqual(details['intro_video'], encoded.get('intro_video', None), context + " intro_video not ==") self.assertEqual(details['effort'], encoded['effort'], context + " efforts not ==") + @staticmethod + def struct_to_datetime(struct_time): + return datetime.datetime(struct_time.tm_year, struct_time.tm_mon, + struct_time.tm_mday, struct_time.tm_hour, + struct_time.tm_min, struct_time.tm_sec, tzinfo=UTC()) + def compare_date_fields(self, details, encoded, context, field): if details[field] is not None: + date = Date() if field in encoded and encoded[field] is not None: - encoded_encoded = jsdate_to_time(encoded[field]) - dt1 = ConvertersTestCase.struct_to_datetime(encoded_encoded) + encoded_encoded = date.from_json(encoded[field]) + dt1 = CourseDetailsViewTest.struct_to_datetime(encoded_encoded) if isinstance(details[field], datetime.datetime): dt2 = details[field] else: - details_encoded = jsdate_to_time(details[field]) - dt2 = ConvertersTestCase.struct_to_datetime(details_encoded) + details_encoded = date.from_json(details[field]) + dt2 = CourseDetailsViewTest.struct_to_datetime(details_encoded) expected_delta = datetime.timedelta(0) self.assertEqual(dt1 - dt2, expected_delta, str(dt1) + "!=" + str(dt2) + " at " + context) diff --git a/cms/djangoapps/models/settings/course_details.py b/cms/djangoapps/models/settings/course_details.py index d3cd5fe164..09d57774ab 100644 --- a/cms/djangoapps/models/settings/course_details.py +++ b/cms/djangoapps/models/settings/course_details.py @@ -1,14 +1,14 @@ -from xmodule.modulestore.django import modulestore from xmodule.modulestore import Location from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.inheritance import own_metadata import json from json.encoder import JSONEncoder import time +import calendar from contentstore.utils import get_modulestore -from util.converters import jsdate_to_time, time_to_date from models.settings import course_grading from contentstore.utils import update_item +from xmodule.fields import Date import re import logging @@ -81,8 +81,14 @@ class CourseDetails(object): dirty = False + # In the descriptor's setter, the date is converted to JSON using Date's to_json method. + # Calling to_json on something that is already JSON doesn't work. Since reaching directly + # into the model is nasty, convert the JSON Date to a Python date, which is what the + # setter expects as input. + date = Date() + if 'start_date' in jsondict: - converted = jsdate_to_time(jsondict['start_date']) + converted = date.from_json(jsondict['start_date']) else: converted = None if converted != descriptor.start: @@ -90,7 +96,7 @@ class CourseDetails(object): descriptor.start = converted if 'end_date' in jsondict: - converted = jsdate_to_time(jsondict['end_date']) + converted = date.from_json(jsondict['end_date']) else: converted = None @@ -99,7 +105,7 @@ class CourseDetails(object): descriptor.end = converted if 'enrollment_start' in jsondict: - converted = jsdate_to_time(jsondict['enrollment_start']) + converted = date.from_json(jsondict['enrollment_start']) else: converted = None @@ -108,7 +114,7 @@ class CourseDetails(object): descriptor.enrollment_start = converted if 'enrollment_end' in jsondict: - converted = jsdate_to_time(jsondict['enrollment_end']) + converted = date.from_json(jsondict['enrollment_end']) else: converted = None @@ -172,12 +178,20 @@ class CourseDetails(object): # TODO move to a more general util? Is there a better way to do the isinstance model check? class CourseSettingsEncoder(json.JSONEncoder): + @staticmethod + def time_to_date(time_obj): + """ + Convert a time.time_struct to a true universal time (can pass to js Date + constructor) + """ + return calendar.timegm(time_obj) * 1000 + def default(self, obj): if isinstance(obj, CourseDetails) or isinstance(obj, course_grading.CourseGradingModel): return obj.__dict__ elif isinstance(obj, Location): return obj.dict() elif isinstance(obj, time.struct_time): - return time_to_date(obj) + return CourseSettingsEncoder.time_to_date(obj) else: return JSONEncoder.default(self, obj) diff --git a/cms/djangoapps/models/settings/course_grading.py b/cms/djangoapps/models/settings/course_grading.py index b20fb71f66..ee9b4ac0eb 100644 --- a/cms/djangoapps/models/settings/course_grading.py +++ b/cms/djangoapps/models/settings/course_grading.py @@ -1,7 +1,5 @@ from xmodule.modulestore import Location from contentstore.utils import get_modulestore -import re -from util import converters from datetime import timedelta diff --git a/common/djangoapps/util/converters.py b/common/djangoapps/util/converters.py deleted file mode 100644 index 212cceb77d..0000000000 --- a/common/djangoapps/util/converters.py +++ /dev/null @@ -1,37 +0,0 @@ -import time -import datetime -import calendar -import dateutil.parser - - -def time_to_date(time_obj): - """ - Convert a time.time_struct to a true universal time (can pass to js Date - constructor) - """ - return calendar.timegm(time_obj) * 1000 - - -def time_to_isodate(source): - '''Convert to an iso date''' - if isinstance(source, time.struct_time): - return time.strftime('%Y-%m-%dT%H:%M:%SZ', source) - elif isinstance(source, datetime): - return source.isoformat() + 'Z' - - -def jsdate_to_time(field): - """ - Convert a universal time (iso format) or msec since epoch to a time obj - """ - if field is None: - return field - elif isinstance(field, basestring): - d = dateutil.parser.parse(field) - return d.utctimetuple() - elif isinstance(field, (int, long, float)): - return time.gmtime(field / 1000) - elif isinstance(field, time.struct_time): - return field - else: - raise ValueError("Couldn't convert %r to time" % field) diff --git a/common/lib/xmodule/xmodule/fields.py b/common/lib/xmodule/xmodule/fields.py index 0abe850d68..ea857933fc 100644 --- a/common/lib/xmodule/xmodule/fields.py +++ b/common/lib/xmodule/xmodule/fields.py @@ -14,7 +14,6 @@ class Date(ModelType): ''' Date fields know how to parse and produce json (iso) compatible formats. ''' - # NB: these are copies of util.converters.* def from_json(self, field): """ Parse an optional metadata key containing a time: if present, complain diff --git a/common/lib/xmodule/xmodule/tests/test_fields.py b/common/lib/xmodule/xmodule/tests/test_fields.py new file mode 100644 index 0000000000..7c8872efc1 --- /dev/null +++ b/common/lib/xmodule/xmodule/tests/test_fields.py @@ -0,0 +1,80 @@ +"""Tests for Date class defined in fields.py.""" +import datetime +import unittest +from django.utils.timezone import UTC +from xmodule.fields import Date +import time + +class DateTest(unittest.TestCase): + date = Date() + + @staticmethod + def struct_to_datetime(struct_time): + return datetime.datetime(struct_time.tm_year, struct_time.tm_mon, + struct_time.tm_mday, struct_time.tm_hour, + struct_time.tm_min, struct_time.tm_sec, tzinfo=UTC()) + + def compare_dates(self, date1, date2, expected_delta): + dt1 = DateTest.struct_to_datetime(date1) + dt2 = DateTest.struct_to_datetime(date2) + self.assertEqual(dt1 - dt2, expected_delta, str(date1) + "-" + + str(date2) + "!=" + str(expected_delta)) + + def test_from_json(self): + '''Test conversion from iso compatible date strings to struct_time''' + self.compare_dates( + DateTest.date.from_json("2013-01-01"), + DateTest.date.from_json("2012-12-31"), + datetime.timedelta(days=1)) + self.compare_dates( + DateTest.date.from_json("2013-01-01T00"), + DateTest.date.from_json("2012-12-31T23"), + datetime.timedelta(hours=1)) + self.compare_dates( + DateTest.date.from_json("2013-01-01T00:00"), + DateTest.date.from_json("2012-12-31T23:59"), + datetime.timedelta(minutes=1)) + self.compare_dates( + DateTest.date.from_json("2013-01-01T00:00:00"), + DateTest.date.from_json("2012-12-31T23:59:59"), + datetime.timedelta(seconds=1)) + self.compare_dates( + DateTest.date.from_json("2013-01-01T00:00:00Z"), + DateTest.date.from_json("2012-12-31T23:59:59Z"), + datetime.timedelta(seconds=1)) + self.compare_dates( + DateTest.date.from_json("2012-12-31T23:00:01-01:00"), + DateTest.date.from_json("2013-01-01T00:00:00+01:00"), + datetime.timedelta(hours=1, seconds=1)) + + def test_return_None(self): + self.assertIsNone(DateTest.date.from_json("")) + self.assertIsNone(DateTest.date.from_json(None)) + self.assertIsNone(DateTest.date.from_json(['unknown value'])) + + def test_old_due_date_format(self): + current = datetime.datetime.today() + self.assertEqual( + time.struct_time((current.year, 3, 12, 12, 0, 0, 1, 71, 0)), + DateTest.date.from_json("March 12 12:00")) + self.assertEqual( + time.struct_time((current.year, 12, 4, 16, 30, 0, 2, 338, 0)), + DateTest.date.from_json("December 4 16:30")) + + def test_to_json(self): + ''' + Test converting time reprs to iso dates + ''' + self.assertEqual( + DateTest.date.to_json( + time.strptime("2012-12-31T23:59:59Z", "%Y-%m-%dT%H:%M:%SZ")), + "2012-12-31T23:59:59Z") + self.assertEqual( + DateTest.date.to_json( + DateTest.date.from_json("2012-12-31T23:59:59Z")), + "2012-12-31T23:59:59Z") + self.assertEqual( + DateTest.date.to_json( + DateTest.date.from_json("2012-12-31T23:00:01-01:00")), + "2013-01-01T00:00:01Z") + From cddc868656d784da1db5585879c9518918b6a512 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 27 Mar 2013 13:01:10 -0400 Subject: [PATCH 219/436] Login URL resolves differently in LMS and CMS, which breaks login_test when loaded by rake test_cms I moved the test into lms/courseware/tests so they run correctly. --- .../student => lms/djangoapps/courseware}/tests/test_login.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {common/djangoapps/student => lms/djangoapps/courseware}/tests/test_login.py (100%) diff --git a/common/djangoapps/student/tests/test_login.py b/lms/djangoapps/courseware/tests/test_login.py similarity index 100% rename from common/djangoapps/student/tests/test_login.py rename to lms/djangoapps/courseware/tests/test_login.py From ac86687fa104d8c0c96ce3e73b7ad29f7baf5a91 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 27 Mar 2013 14:33:59 -0400 Subject: [PATCH 220/436] Added exception handling that solves SchematicResponse exceptions causing a 500 error. When XModule raises a ProcessingError during an AJAX request, this module_render now returns a 404 to further reduce number of 500 responses. --- common/lib/capa/capa/responsetypes.py | 22 +++++-- common/lib/xmodule/xmodule/capa_module.py | 16 +++-- common/lib/xmodule/xmodule/exceptions.py | 8 ++- .../xmodule/xmodule/tests/test_capa_module.py | 60 ++++++++++++------- lms/djangoapps/courseware/module_render.py | 11 +++- 5 files changed, 86 insertions(+), 31 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index e79399c5fc..bc8e7ff541 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -53,12 +53,17 @@ class LoncapaProblemError(Exception): class ResponseError(Exception): ''' - Error for failure in processing a response + Error for failure in processing a response, including + exceptions that occur when executing a custom script. ''' pass class StudentInputError(Exception): + ''' + Error for an invalid student input. + For example, submitting a string when the problem expects a number + ''' pass #----------------------------------------------------------------------------- @@ -1151,7 +1156,7 @@ def sympy_check2(): # Raise an exception else: log.error(traceback.format_exc()) - raise LoncapaProblemError( + raise ResponseError( "CustomResponse: check function returned an invalid dict") # The check function can return a boolean value, @@ -1226,7 +1231,7 @@ def sympy_check2(): Handle an exception raised during the execution of custom Python code. - Raises a StudentInputError + Raises a ResponseError ''' # Log the error if we are debugging @@ -1236,7 +1241,7 @@ def sympy_check2(): # Notify student with a student input error _, _, traceback_obj = sys.exc_info() - raise StudentInputError, StudentInputError(err.message), traceback_obj + raise ResponseError, ResponseError(err.message), traceback_obj #----------------------------------------------------------------------------- @@ -1912,7 +1917,14 @@ class SchematicResponse(LoncapaResponse): submission = [json.loads(student_answers[ k]) for k in sorted(self.answer_ids)] self.context.update({'submission': submission}) - exec self.code in global_context, self.context + + try: + exec self.code in global_context, self.context + + except Exception as err: + _, _, traceback_obj = sys.exc_info() + raise ResponseError, ResponseError(err.message), traceback_obj + cmap = CorrectMap() cmap.set_dict(dict(zip(sorted( self.answer_ids), self.context['correct']))) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index d7346faa67..fd25016ca5 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -12,12 +12,13 @@ from lxml import etree from pkg_resources import resource_string from capa.capa_problem import LoncapaProblem -from capa.responsetypes import StudentInputError +from capa.responsetypes import StudentInputError, \ + ResponseError, LoncapaProblemError from capa.util import convert_files_to_filenames from .progress import Progress from xmodule.x_module import XModule from xmodule.raw_module import RawDescriptor -from xmodule.exceptions import NotFoundError +from xmodule.exceptions import NotFoundError, ProcessingError from xblock.core import Integer, Scope, BlockScope, ModelType, String, Boolean, Object, Float from .fields import Timedelta @@ -454,7 +455,14 @@ class CapaModule(CapaFields, XModule): return 'Error' before = self.get_progress() - d = handlers[dispatch](get) + + try: + d = handlers[dispatch](get) + + except Exception as err: + _, _, traceback_obj = sys.exc_info() + raise ProcessingError, ProcessingError(err.message), traceback_obj + after = self.get_progress() d.update({ 'progress_changed': after != before, @@ -726,7 +734,7 @@ class CapaModule(CapaFields, XModule): correct_map = self.lcp.grade_answers(answers) self.set_state_from_lcp() - except StudentInputError as inst: + except (StudentInputError, ResponseError, LoncapaProblemError) as inst: log.exception("StudentInputError in capa_module:problem_check") # If the user is a staff member, include diff --git a/common/lib/xmodule/xmodule/exceptions.py b/common/lib/xmodule/xmodule/exceptions.py index 3db5ceccde..d38fbb12bb 100644 --- a/common/lib/xmodule/xmodule/exceptions.py +++ b/common/lib/xmodule/xmodule/exceptions.py @@ -1,6 +1,12 @@ class InvalidDefinitionError(Exception): pass - class NotFoundError(Exception): pass + +class ProcessingError(Exception): + ''' + An error occurred while processing a request to the XModule. + For example: if an exception occurs while checking a capa problem. + ''' + pass diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py index 18d20a2756..cb7d599413 100644 --- a/common/lib/xmodule/xmodule/tests/test_capa_module.py +++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py @@ -7,6 +7,8 @@ import random import xmodule import capa +from capa.responsetypes import StudentInputError, \ + LoncapaProblemError, ResponseError from xmodule.capa_module import CapaModule from xmodule.modulestore import Location from lxml import etree @@ -502,38 +504,52 @@ class CapaModuleTest(unittest.TestCase): self.assertEqual(module.attempts, 1) - def test_check_problem_student_input_error(self): - module = CapaFactory.create(attempts=1) + def test_check_problem_error(self): - # Ensure that the user is NOT staff - module.system.user_is_staff = False + # Try each exception that capa_module should handle + for exception_class in [StudentInputError, + LoncapaProblemError, + ResponseError]: - # Simulate a student input exception - with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade: - mock_grade.side_effect = capa.responsetypes.StudentInputError('test error') + # Create the module + module = CapaFactory.create(attempts=1) - get_request_dict = {CapaFactory.input_key(): '3.14'} - result = module.check_problem(get_request_dict) + # Ensure that the user is NOT staff + module.system.user_is_staff = False + + # Simulate answering a problem that raises the exception + with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade: + mock_grade.side_effect = exception_class('test error') + + get_request_dict = {CapaFactory.input_key(): '3.14'} + result = module.check_problem(get_request_dict) # Expect an AJAX alert message in 'success' expected_msg = 'Error: test error' self.assertEqual(expected_msg, result['success']) - # Expect that the number of attempts is NOT incremented - self.assertEqual(module.attempts, 1) + # Expect that the number of attempts is NOT incremented + self.assertEqual(module.attempts, 1) - def test_check_problem_student_input_error_with_staff_user(self): - module = CapaFactory.create(attempts=1) + def test_check_problem_error_with_staff_user(self): + + # Try each exception that capa module should handle + for exception_class in [StudentInputError, + LoncapaProblemError, + ResponseError]: - # Ensure that the user IS staff - module.system.user_is_staff = True + # Create the module + module = CapaFactory.create(attempts=1) - # Simulate a student input exception - with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade: - mock_grade.side_effect = capa.responsetypes.StudentInputError('test error') + # Ensure that the user IS staff + module.system.user_is_staff = True - get_request_dict = {CapaFactory.input_key(): '3.14'} - result = module.check_problem(get_request_dict) + # Simulate answering a problem that raises an exception + with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade: + mock_grade.side_effect = exception_class('test error') + + get_request_dict = {CapaFactory.input_key(): '3.14'} + result = module.check_problem(get_request_dict) # Expect an AJAX alert message in 'success' self.assertTrue('test error' in result['success']) @@ -541,6 +557,10 @@ class CapaModuleTest(unittest.TestCase): # We DO include traceback information for staff users self.assertTrue('Traceback' in result['success']) + # Expect that the number of attempts is NOT incremented + self.assertEqual(module.attempts, 1) + + def test_reset_problem(self): module = CapaFactory.create(done=True) module.new_lcp = Mock(wraps=module.new_lcp) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 973940d784..182c45775d 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -22,7 +22,7 @@ from .models import StudentModule from psychometrics.psychoanalyze import make_psychometrics_data_update_handler from student.models import unique_id_for_user from xmodule.errortracker import exc_info_to_str -from xmodule.exceptions import NotFoundError +from xmodule.exceptions import NotFoundError, ProcessingError from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore from xmodule.x_module import ModuleSystem @@ -443,9 +443,18 @@ def modx_dispatch(request, dispatch, location, course_id): # Let the module handle the AJAX try: ajax_return = instance.handle_ajax(dispatch, p) + + # If we can't find the module, respond with a 404 except NotFoundError: log.exception("Module indicating to user that request doesn't exist") raise Http404 + + # For XModule-specific errors, we respond with 404 + except ProcessingError: + log.exception("Module encountered an error while prcessing AJAX call") + raise Http404 + + # If any other error occurred, re-raise it to trigger a 500 response except: log.exception("error processing ajax call") raise From 99cd3fafdb5f0d2cbe00ad541cb0a07ad83197e5 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 27 Mar 2013 14:48:44 -0400 Subject: [PATCH 221/436] Added error handling of XModule processing errors to CMS Added tests for SchematicResponse error handling --- cms/djangoapps/contentstore/views.py | 8 ++++++- .../lib/capa/capa/tests/test_responsetypes.py | 21 +++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 561708c833..6ff3e41510 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -42,7 +42,7 @@ from xmodule.modulestore.mongo import MongoUsage from mitxmako.shortcuts import render_to_response, render_to_string from xmodule.modulestore.django import modulestore from xmodule_modifiers import replace_static_urls, wrap_xmodule -from xmodule.exceptions import NotFoundError +from xmodule.exceptions import NotFoundError, ProcessingError from functools import partial from xmodule.contentstore.django import contentstore @@ -448,9 +448,15 @@ def preview_dispatch(request, preview_id, location, dispatch=None): # Let the module handle the AJAX try: ajax_return = instance.handle_ajax(dispatch, request.POST) + except NotFoundError: log.exception("Module indicating to user that request doesn't exist") raise Http404 + + except ProcessingError: + log.exception("Module raised an error while processing AJAX request") + raise Http404 + except: log.exception("error processing ajax call") raise diff --git a/common/lib/capa/capa/tests/test_responsetypes.py b/common/lib/capa/capa/tests/test_responsetypes.py index ac50e6defc..d42e9afcb8 100644 --- a/common/lib/capa/capa/tests/test_responsetypes.py +++ b/common/lib/capa/capa/tests/test_responsetypes.py @@ -13,7 +13,8 @@ import textwrap from . import test_system import capa.capa_problem as lcp -from capa.responsetypes import LoncapaProblemError, StudentInputError +from capa.responsetypes import LoncapaProblemError, \ + StudentInputError, ResponseError from capa.correctmap import CorrectMap from capa.util import convert_files_to_filenames from capa.xqueue_interface import dateformat @@ -865,7 +866,7 @@ class CustomResponseTest(ResponseTest): problem = self.build_problem(script=script, cfn="check_func") # Expect that an exception gets raised when we check the answer - with self.assertRaises(StudentInputError): + with self.assertRaises(ResponseError): problem.grade_answers({'1_2_1': '42'}) def test_script_exception_inline(self): @@ -875,7 +876,7 @@ class CustomResponseTest(ResponseTest): problem = self.build_problem(answer=script) # Expect that an exception gets raised when we check the answer - with self.assertRaises(StudentInputError): + with self.assertRaises(ResponseError): problem.grade_answers({'1_2_1': '42'}) def test_invalid_dict_exception(self): @@ -889,7 +890,7 @@ class CustomResponseTest(ResponseTest): problem = self.build_problem(script=script, cfn="check_func") # Expect that an exception gets raised when we check the answer - with self.assertRaises(LoncapaProblemError): + with self.assertRaises(ResponseError): problem.grade_answers({'1_2_1': '42'}) @@ -922,6 +923,18 @@ class SchematicResponseTest(ResponseTest): # is what we expect) self.assertEqual(correct_map.get_correctness('1_2_1'), 'correct') + def test_script_exception(self): + + # Construct a script that will raise an exception + script = "raise Exception('test')" + problem = self.build_problem(answer=script) + + # Expect that an exception gets raised when we check the answer + with self.assertRaises(ResponseError): + submission_dict = {'test': 'test'} + input_dict = {'1_2_1': json.dumps(submission_dict)} + problem.grade_answers(input_dict) + class AnnotationResponseTest(ResponseTest): from response_xml_factory import AnnotationResponseXMLFactory From 22537ffd3b05269b688972e7e2ad81e118cc1da7 Mon Sep 17 00:00:00 2001 From: cahrens Date: Wed, 27 Mar 2013 14:51:39 -0400 Subject: [PATCH 222/436] Don't need to convert to milliseconds. --- cms/djangoapps/models/settings/course_details.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/cms/djangoapps/models/settings/course_details.py b/cms/djangoapps/models/settings/course_details.py index 09d57774ab..b45f5bd343 100644 --- a/cms/djangoapps/models/settings/course_details.py +++ b/cms/djangoapps/models/settings/course_details.py @@ -178,20 +178,12 @@ class CourseDetails(object): # TODO move to a more general util? Is there a better way to do the isinstance model check? class CourseSettingsEncoder(json.JSONEncoder): - @staticmethod - def time_to_date(time_obj): - """ - Convert a time.time_struct to a true universal time (can pass to js Date - constructor) - """ - return calendar.timegm(time_obj) * 1000 - def default(self, obj): if isinstance(obj, CourseDetails) or isinstance(obj, course_grading.CourseGradingModel): return obj.__dict__ elif isinstance(obj, Location): return obj.dict() elif isinstance(obj, time.struct_time): - return CourseSettingsEncoder.time_to_date(obj) + return Date().to_json(obj) else: return JSONEncoder.default(self, obj) From 5c78218b1360bf9e0eb6bcee41cbc44e1aeb1dac Mon Sep 17 00:00:00 2001 From: cahrens Date: Wed, 27 Mar 2013 14:52:27 -0400 Subject: [PATCH 223/436] Don't need to convert to milliseconds. --- cms/djangoapps/models/settings/course_details.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cms/djangoapps/models/settings/course_details.py b/cms/djangoapps/models/settings/course_details.py index b45f5bd343..876000c7fe 100644 --- a/cms/djangoapps/models/settings/course_details.py +++ b/cms/djangoapps/models/settings/course_details.py @@ -4,7 +4,6 @@ from xmodule.modulestore.inheritance import own_metadata import json from json.encoder import JSONEncoder import time -import calendar from contentstore.utils import get_modulestore from models.settings import course_grading from contentstore.utils import update_item From 122c8567c5d370a6e54e075d4e736c96bcfef646 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 27 Mar 2013 15:00:08 -0400 Subject: [PATCH 224/436] An integrity error while creating an enrollment just means that our work has already been done. Fixes https://www.pivotaltracker.com/story/show/46915947 --- common/djangoapps/student/views.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 5dbaf5d2c2..d0deffd7b9 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -325,7 +325,12 @@ def change_enrollment(request): "course:{0}".format(course_num), "run:{0}".format(run)]) - enrollment, created = CourseEnrollment.objects.get_or_create(user=user, course_id=course.id) + try: + enrollment, created = CourseEnrollment.objects.get_or_create(user=user, course_id=course.id) + except IntegrityError: + # If we've already created this enrollment in a separate transaction, + # then just continue + pass return {'success': True} elif action == "unenroll": From df1be877390c6869b766870c7d5e40bbfe258913 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 27 Mar 2013 15:20:40 -0400 Subject: [PATCH 225/436] * Changed 404 errors to 400 errors * Removed duplicate traceback log message * Now provide string, not Exception, as second tuple item to raise --- cms/djangoapps/contentstore/views.py | 2 +- common/lib/capa/capa/responsetypes.py | 3 +-- common/lib/xmodule/xmodule/capa_module.py | 2 +- lms/djangoapps/courseware/module_render.py | 6 +++--- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 6ff3e41510..24f3eae8a4 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -455,7 +455,7 @@ def preview_dispatch(request, preview_id, location, dispatch=None): except ProcessingError: log.exception("Module raised an error while processing AJAX request") - raise Http404 + return HttpResponseBadRequest() except: log.exception("error processing ajax call") diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index bc8e7ff541..3d19fb4cb1 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1237,11 +1237,10 @@ def sympy_check2(): # Log the error if we are debugging msg = 'Error occurred while evaluating CustomResponse' log.debug(msg, exc_info=True) - log.debug(traceback.format_exc()) # Notify student with a student input error _, _, traceback_obj = sys.exc_info() - raise ResponseError, ResponseError(err.message), traceback_obj + raise ResponseError, err.message, traceback_obj #----------------------------------------------------------------------------- diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index fd25016ca5..4975478421 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -461,7 +461,7 @@ class CapaModule(CapaFields, XModule): except Exception as err: _, _, traceback_obj = sys.exc_info() - raise ProcessingError, ProcessingError(err.message), traceback_obj + raise ProcessingError, err.message, traceback_obj after = self.get_progress() d.update({ diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 182c45775d..39d16dbb19 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -10,7 +10,7 @@ from django.conf import settings from django.contrib.auth.models import User from django.core.urlresolvers import reverse from django.http import Http404 -from django.http import HttpResponse +from django.http import HttpResponse, HttpResponseBadRequest from django.views.decorators.csrf import csrf_exempt from requests.auth import HTTPBasicAuth @@ -449,10 +449,10 @@ def modx_dispatch(request, dispatch, location, course_id): log.exception("Module indicating to user that request doesn't exist") raise Http404 - # For XModule-specific errors, we respond with 404 + # For XModule-specific errors, we respond with 400 except ProcessingError: log.exception("Module encountered an error while prcessing AJAX call") - raise Http404 + return HttpResponseBadRequest() # If any other error occurred, re-raise it to trigger a 500 response except: From 756f75951dca8311ff0d642af2ef3abddbf2c9b3 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 27 Mar 2013 16:28:49 -0400 Subject: [PATCH 226/436] studio - in progress work on help UI --- cms/static/client_templates/checklist.html | 2 +- cms/static/sass/_base.scss | 3 +- cms/static/sass/_variables.scss | 2 +- cms/static/sass/base-style.scss | 2 + cms/static/sass/elements/_sock.scss | 95 +++++++++++++++++++++- cms/static/sass/views/_account.scss | 2 +- cms/templates/howitworks.html | 4 +- cms/templates/widgets/footer.html | 5 +- cms/templates/widgets/sock.html | 41 +++++++++- 9 files changed, 143 insertions(+), 13 deletions(-) diff --git a/cms/static/client_templates/checklist.html b/cms/static/client_templates/checklist.html index ec6ff4e892..6884b0e9c9 100644 --- a/cms/static/client_templates/checklist.html +++ b/cms/static/client_templates/checklist.html @@ -44,7 +44,7 @@ <% if (item['action_text'] !== '' && item['action_url'] !== '') { %>
      -
    • +
    • rel="external" title="This link will open in a new browser window/tab" diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss index 5901b19306..5ce131288e 100644 --- a/cms/static/sass/_base.scss +++ b/cms/static/sass/_base.scss @@ -357,7 +357,8 @@ h1 { // layout - grandfathered .main-wrapper { position: relative; - margin: 40px; + margin: ($baseline*2); + padding-bottom: $footer-primary-height; } .inner-wrapper { diff --git a/cms/static/sass/_variables.scss b/cms/static/sass/_variables.scss index ccbd3ed7b0..ffa99e3fc6 100644 --- a/cms/static/sass/_variables.scss +++ b/cms/static/sass/_variables.scss @@ -155,7 +155,7 @@ $shadow-l1: rgba(0,0,0,0.1); $shadow-d1: rgba(0,0,0,0.4); // colors - inherited -$baseFontColor: #3c3c3c; +$baseFontColor: $gray-d2; $offBlack: #3c3c3c; $green: #108614; $lightGrey: #edf1f5; diff --git a/cms/static/sass/base-style.scss b/cms/static/sass/base-style.scss index bc6638bf14..e1afa45804 100644 --- a/cms/static/sass/base-style.scss +++ b/cms/static/sass/base-style.scss @@ -21,6 +21,8 @@ @import 'base'; // elements +@import 'elements/typography'; +@import 'elements/icons'; @import 'elements/header'; @import 'elements/sock'; @import 'elements/footer'; diff --git a/cms/static/sass/elements/_sock.scss b/cms/static/sass/elements/_sock.scss index e1b147b849..e8fbcc3ef2 100644 --- a/cms/static/sass/elements/_sock.scss +++ b/cms/static/sass/elements/_sock.scss @@ -9,10 +9,101 @@ .sock { @include clearfix(); - @include font-size(13); + @extend .t-copy-sub2; max-width: $fg-max-width; min-width: $fg-min-width; width: flex-grid(12); - margin: 0 auto ($baseline*2) auto; + margin: 0 auto $baseline auto; + + header { + + .title { + @extend .t-title-3; + } + + .ss-icon { + @extend .t-icon-inline; + } + } + + // shared elements + .support, .feedback { + @include box-sizing(border-box); + + .title { + + } + + .copy { + margin: 0 0 $baseline 0; + } + + .list-actions { + @include clearfix(); + + .action-item { + float: left; + margin-right: ($baseline/2); + + &:last-child { + margin-right: 0; + } + + .action { + display: block; + + .ss-icon { + @include transition(color .25s ease-in-out); + @include font-size(15); + @extend .t-icon-inline; + @extend .icon-inline; + margin-right: ($baseline/4); + color: $blue-l2; + } + + &:hover, &:active { + + .ss-icon { + color: $white; + } + } + } + + .tip { + @extend .sr; + } + } + + .action-primary { + @include blue-button; + @include transition(all .15s); + @include font-size(13); + font-weight: 500; + padding: ($baseline/4) ($baseline/2); + text-align: center; + } + } + } + + // studio support content + .support { + width: flex-grid(8,12); + float: left; + margin-right: flex-gutter(); + + .action-item { + width: flexgrid(4,8); + } + } + + // studio feedback content + .feedback { + width: flex-grid(4,12); + float: left; + + .action-item { + width: flexgrid(4,4); + } + } } } \ No newline at end of file diff --git a/cms/static/sass/views/_account.scss b/cms/static/sass/views/_account.scss index 1206db5e76..2be94a81ea 100644 --- a/cms/static/sass/views/_account.scss +++ b/cms/static/sass/views/_account.scss @@ -71,7 +71,7 @@ body.signup, body.signin { @include blue-button; @include transition(all .15s); @include font-size(15); - display:block; + display: block; width: 100%; padding: ($baseline*0.75) ($baseline/2); font-weight: 600; diff --git a/cms/templates/howitworks.html b/cms/templates/howitworks.html index 7a819fceba..6c0029c425 100644 --- a/cms/templates/howitworks.html +++ b/cms/templates/howitworks.html @@ -134,10 +134,10 @@ diff --git a/cms/templates/widgets/footer.html b/cms/templates/widgets/footer.html index 18ecf2bc39..c00bf0187a 100644 --- a/cms/templates/widgets/footer.html +++ b/cms/templates/widgets/footer.html @@ -20,12 +20,9 @@
    • % if user.is_authenticated(): % endif - diff --git a/cms/templates/widgets/sock.html b/cms/templates/widgets/sock.html index ff5f9c9ad4..d8d191a773 100644 --- a/cms/templates/widgets/sock.html +++ b/cms/templates/widgets/sock.html @@ -2,7 +2,46 @@ % if user.is_authenticated():
      -

      Sock!

      +
      +

      Studio Support

      +
      + +
      +

      Studio Support

      + +
      +

      Need help getting started with Studio? Want to know how to manage a particular part of your course using Studio? Take advantage of our documentation, help forums, as well as our edX101 introduction course for course authors.

      +
      + + +
      + +
      % endif \ No newline at end of file From 6564cc57e6f7a2bddfab8a9dabbcc012687135a1 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Wed, 27 Mar 2013 16:29:55 -0400 Subject: [PATCH 227/436] Fix typo with hyphen in cms lettuce feature files --- .../contentstore/features/advanced-settings.feature | 6 +++--- .../features/studio-overview-togglesection.feature | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cms/djangoapps/contentstore/features/advanced-settings.feature b/cms/djangoapps/contentstore/features/advanced-settings.feature index 66039e19b1..db7294c14c 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.feature +++ b/cms/djangoapps/contentstore/features/advanced-settings.feature @@ -27,16 +27,16 @@ Feature: Advanced (manual) course policy And I reload the page Then the policy key value is changed - Scenario: Test how multi -line input appears + Scenario: Test how multi-line input appears Given I am on the Advanced Course Settings page in Studio When I create a JSON object as a value Then it is displayed as formatted And I reload the page Then it is displayed as formatted - Scenario: Test automatic quoting of non -JSON values + Scenario: Test automatic quoting of non-JSON values Given I am on the Advanced Course Settings page in Studio - When I create a non -JSON value not in quotes + When I create a non-JSON value not in quotes Then it is displayed as a string And I reload the page Then it is displayed as a string diff --git a/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature b/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature index 88492d55e3..762dea6838 100644 --- a/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature +++ b/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature @@ -21,7 +21,7 @@ Feature: Overview Toggle Section Then I see the "Collapse All Sections" link And all sections are expanded - @skip -phantom + @skip-phantom Scenario: Collapse link is not removed after last section of a course is deleted Given I have a course with 1 section And I navigate to the course overview page From f038237ee9f2d7a5dae9c2ebdb6a2ba57db860c8 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 27 Mar 2013 16:34:08 -0400 Subject: [PATCH 228/436] Changed log.exception to log.warning --- cms/djangoapps/contentstore/views.py | 2 +- common/lib/capa/capa/responsetypes.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 24f3eae8a4..bfdfb1742b 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -454,7 +454,7 @@ def preview_dispatch(request, preview_id, location, dispatch=None): raise Http404 except ProcessingError: - log.exception("Module raised an error while processing AJAX request") + log.warning("Module raised an error while processing AJAX request") return HttpResponseBadRequest() except: diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 3d19fb4cb1..bc62654bef 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1236,7 +1236,7 @@ def sympy_check2(): # Log the error if we are debugging msg = 'Error occurred while evaluating CustomResponse' - log.debug(msg, exc_info=True) + log.warning(msg, exc_info=True) # Notify student with a student input error _, _, traceback_obj = sys.exc_info() From 9c671163fdf1be224cf4d3f310380fa9caa75cf8 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 27 Mar 2013 17:11:02 -0400 Subject: [PATCH 229/436] Added exc_info=True to log.warning Changed log.exception to log.warning --- cms/djangoapps/contentstore/views.py | 3 ++- common/lib/xmodule/xmodule/capa_module.py | 3 ++- lms/djangoapps/courseware/module_render.py | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index bfdfb1742b..0d58133763 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -454,7 +454,8 @@ def preview_dispatch(request, preview_id, location, dispatch=None): raise Http404 except ProcessingError: - log.warning("Module raised an error while processing AJAX request") + log.warning("Module raised an error while processing AJAX request", + exc_info=True) return HttpResponseBadRequest() except: diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 4975478421..3e594f9d46 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -735,7 +735,8 @@ class CapaModule(CapaFields, XModule): self.set_state_from_lcp() except (StudentInputError, ResponseError, LoncapaProblemError) as inst: - log.exception("StudentInputError in capa_module:problem_check") + log.warning("StudentInputError in capa_module:problem_check", + exc_info=True) # If the user is a staff member, include # the full exception, including traceback, diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 39d16dbb19..48aab024df 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -451,7 +451,8 @@ def modx_dispatch(request, dispatch, location, course_id): # For XModule-specific errors, we respond with 400 except ProcessingError: - log.exception("Module encountered an error while prcessing AJAX call") + log.warning("Module encountered an error while prcessing AJAX call", + exc_info=True) return HttpResponseBadRequest() # If any other error occurred, re-raise it to trigger a 500 response From c8ab45cc579c675265b0d9223d61eb2c3310614c Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 27 Mar 2013 17:40:24 -0400 Subject: [PATCH 230/436] studio - footer help ui revamp - animation, structure, styling - WIP --- cms/static/js/base.js | 11 +- cms/static/sass/_base.scss | 8 ++ cms/static/sass/_cms_mixins.scss | 15 +++ cms/static/sass/_variables.scss | 2 +- cms/static/sass/base-style.scss | 1 - cms/static/sass/elements/_footer.scss | 121 ++++++++++++++++++++++ cms/static/sass/elements/_icons.scss | 16 +++ cms/static/sass/elements/_sock.scss | 109 ------------------- cms/static/sass/elements/_typography.scss | 82 +++++++++++++++ cms/templates/base.html | 5 +- cms/templates/widgets/footer.html | 45 +++++++- cms/templates/widgets/sock.html | 47 --------- 12 files changed, 299 insertions(+), 163 deletions(-) create mode 100644 cms/static/sass/elements/_icons.scss delete mode 100644 cms/static/sass/elements/_sock.scss create mode 100644 cms/static/sass/elements/_typography.scss delete mode 100644 cms/templates/widgets/sock.html diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 211981b05a..d93d2fd8d4 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -34,11 +34,11 @@ $(document).ready(function () { $(this).select(); }); + $('body').addClass('js'); + $('.unit .item-actions .delete-button').bind('click', deleteUnit); $('.new-unit-item').bind('click', createNewUnit); - $('body').addClass('js'); - // lean/simple modal $('a[rel*=modal]').leanModal({overlay : 0.80, closeButton: '.action-modal-close' }); $('a.action-modal-close').click(function(e){ @@ -86,6 +86,8 @@ $(document).ready(function () { // tender feedback window scrolling $('a.show-tender').bind('click', smoothScrollTop); + // toggling footer additional support + $('.show-support').bind('click', toggleSupport); // toggling overview section details $(function () { @@ -456,6 +458,11 @@ function onKeyUp(e) { } } +function toggleSupport(e) { + e.preventDefault(); + $body.toggleClass('footer-is-expanded'); +} + function toggleSubmodules(e) { e.preventDefault(); $(this).toggleClass('expand').toggleClass('collapse'); diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss index 5ce131288e..328c7e99c3 100644 --- a/cms/static/sass/_base.scss +++ b/cms/static/sass/_base.scss @@ -56,6 +56,14 @@ h1 { min-height: 100%; } +.wrapper-main { + padding-bottom: $footer-primary-height; +} + +.js.footer-is-expanded .wrapper-main { + padding-bottom: ($footer-primary-height*4); +} + // layout - basic page header .wrapper-mast { margin: 0; diff --git a/cms/static/sass/_cms_mixins.scss b/cms/static/sass/_cms_mixins.scss index 015a94b762..d837000a24 100644 --- a/cms/static/sass/_cms_mixins.scss +++ b/cms/static/sass/_cms_mixins.scss @@ -110,6 +110,21 @@ } } +@mixin gray-button { + @include button; + border: 1px solid $gray-d1; + border-radius: 3px; + @include linear-gradient(top, $white-t1, rgba(255, 255, 255, 0)); + background-color: $gray-d2; + @include box-shadow(0 1px 0 $white-t1 inset); + color: $gray-l3; + + &:hover { + background-color: $gray-d3; + color: $white; + } +} + @mixin green-button { @include button; border: 1px solid $darkGreen; diff --git a/cms/static/sass/_variables.scss b/cms/static/sass/_variables.scss index ffa99e3fc6..806fbc8c57 100644 --- a/cms/static/sass/_variables.scss +++ b/cms/static/sass/_variables.scss @@ -13,7 +13,7 @@ $fg-max-width: 1280px; $fg-min-width: 900px; // elements -$footer-primary-height: ($baseline*3); +$footer-primary-height: (60px); // type $sans-serif: 'Open Sans', $verdana; diff --git a/cms/static/sass/base-style.scss b/cms/static/sass/base-style.scss index e1afa45804..3015c3592d 100644 --- a/cms/static/sass/base-style.scss +++ b/cms/static/sass/base-style.scss @@ -24,7 +24,6 @@ @import 'elements/typography'; @import 'elements/icons'; @import 'elements/header'; -@import 'elements/sock'; @import 'elements/footer'; @import 'elements/navigation'; @import 'elements/forms'; diff --git a/cms/static/sass/elements/_footer.scss b/cms/static/sass/elements/_footer.scss index dfebc6d44c..f2941e11be 100644 --- a/cms/static/sass/elements/_footer.scss +++ b/cms/static/sass/elements/_footer.scss @@ -75,4 +75,125 @@ } } } + + // sock - additional help + .sock { + @include clearfix(); + @extend .t-copy-sub2; + max-width: $fg-max-width; + min-width: $fg-min-width; + width: flex-grid(12); + margin: 0 auto $baseline auto; + border-bottom: 1px solid $gray-l2; + padding-bottom: $baseline; + + header { + + .title { + @extend .t-title-3; + } + + .ss-icon { + @extend .t-icon; + @extend .icon-inline; + } + } + + // shared elements + .support, .feedback { + @include box-sizing(border-box); + + .title { + + } + + .copy { + margin: 0 0 $baseline 0; + } + + .list-actions { + @include clearfix(); + + .action-item { + float: left; + margin-right: ($baseline/2); + + &:last-child { + margin-right: 0; + } + + .action { + display: block; + + .ss-icon { + @include font-size(15); + @extend .t-icon; + @extend .icon-inline; + } + + &:hover, &:active { + + .ss-icon { + } + } + } + + .tip { + @extend .sr; + } + } + + .action-primary { + @include gray-button; + @include transition(all .15s); + @include font-size(13); + font-weight: 500; + padding: ($baseline/4) ($baseline/2); + text-align: center; + } + } + } + + // studio support content + .support { + width: flex-grid(8,12); + float: left; + margin-right: flex-gutter(); + + .action-item { + width: flexgrid(4,8); + } + } + + // studio feedback content + .feedback { + width: flex-grid(4,12); + float: left; + + .action-item { + width: flexgrid(4,4); + } + } + } +} + + +// js-enabled styling +.js .wrapper-footer { + @include transition(height 2s ease-in-out); + height: $footer-primary-height; + overflow: hidden; + + .sock { + display: none; + } +} + + // expanded view +.js.footer-is-expanded .wrapper-footer { + height: ($footer-primary-height*4); + + .sock { + display: block; + } } \ No newline at end of file diff --git a/cms/static/sass/elements/_icons.scss b/cms/static/sass/elements/_icons.scss new file mode 100644 index 0000000000..2bc73d8b8d --- /dev/null +++ b/cms/static/sass/elements/_icons.scss @@ -0,0 +1,16 @@ +// studio - elements - icons +// ==================== + +.icon { + +} + +.ss-icon { + +} + +.icon-inline { + display: inline-block; + vertical-align: middle; + margin-right: ($baseline/4); +} \ No newline at end of file diff --git a/cms/static/sass/elements/_sock.scss b/cms/static/sass/elements/_sock.scss deleted file mode 100644 index e8fbcc3ef2..0000000000 --- a/cms/static/sass/elements/_sock.scss +++ /dev/null @@ -1,109 +0,0 @@ -// studio - elements - sock -// ==================== - -.wrapper-sock { - margin: 0; - padding: $baseline $baseline $footer-primary-height $baseline; - position: relative; - width: 100%; - - .sock { - @include clearfix(); - @extend .t-copy-sub2; - max-width: $fg-max-width; - min-width: $fg-min-width; - width: flex-grid(12); - margin: 0 auto $baseline auto; - - header { - - .title { - @extend .t-title-3; - } - - .ss-icon { - @extend .t-icon-inline; - } - } - - // shared elements - .support, .feedback { - @include box-sizing(border-box); - - .title { - - } - - .copy { - margin: 0 0 $baseline 0; - } - - .list-actions { - @include clearfix(); - - .action-item { - float: left; - margin-right: ($baseline/2); - - &:last-child { - margin-right: 0; - } - - .action { - display: block; - - .ss-icon { - @include transition(color .25s ease-in-out); - @include font-size(15); - @extend .t-icon-inline; - @extend .icon-inline; - margin-right: ($baseline/4); - color: $blue-l2; - } - - &:hover, &:active { - - .ss-icon { - color: $white; - } - } - } - - .tip { - @extend .sr; - } - } - - .action-primary { - @include blue-button; - @include transition(all .15s); - @include font-size(13); - font-weight: 500; - padding: ($baseline/4) ($baseline/2); - text-align: center; - } - } - } - - // studio support content - .support { - width: flex-grid(8,12); - float: left; - margin-right: flex-gutter(); - - .action-item { - width: flexgrid(4,8); - } - } - - // studio feedback content - .feedback { - width: flex-grid(4,12); - float: left; - - .action-item { - width: flexgrid(4,4); - } - } - } -} \ No newline at end of file diff --git a/cms/static/sass/elements/_typography.scss b/cms/static/sass/elements/_typography.scss new file mode 100644 index 0000000000..a9b3d362ee --- /dev/null +++ b/cms/static/sass/elements/_typography.scss @@ -0,0 +1,82 @@ +// studio - elements - typography +// ==================== + +// headings/titles +.t-title-1, .t-title-2, .t-title-3, .t-title-4, .t-title-5, .t-title-5 { + color: $gray-d3; +} + +.t-title-1 { + @include font-size(32); +} + +.t-title-2 { + @include font-size(24); + margin: 0 0 ($baseline/2) 0; + font-weight: 600; +} + +.t-title-3 { + @include font-size(16); + margin: 0 0 ($baseline/2) 0; + font-weight: 600; +} + +.t-title-4 { + +} + +.t-title-5 { + +} + +// ==================== + +// copy +.t-copy-base { + @include font-size(16); +} + +.t-copy-lead1 { + @include font-size(18); +} + +.t-copy-lead2 { + @include font-size(20); +} + +.t-copy-sub1 { + @include font-size(14); +} + +.t-copy-sub2 { + @include font-size(13); +} + +.t-copy-sub3 { + @include font-size(12); +} + +// ==================== + +// actions/labels +.t-action { + @include font-size(14); + font-weight: 600; +} + +.t-action-primary { + @include font-size(14); + font-weight: 600; +} + +.t-action-primary-s { + @include font-size(13); +} + +// ==================== + +// misc +.t-icon { + line-height: 0; +} \ No newline at end of file diff --git a/cms/templates/base.html b/cms/templates/base.html index 44847c1da4..e587619bae 100644 --- a/cms/templates/base.html +++ b/cms/templates/base.html @@ -53,8 +53,9 @@
      <%include file="widgets/header.html" /> - <%block name="content"> - <%include file="widgets/sock.html" /> +
      + <%block name="content"> +
      <%include file="widgets/footer.html" />
      diff --git a/cms/templates/widgets/footer.html b/cms/templates/widgets/footer.html index c00bf0187a..b6a3b84272 100644 --- a/cms/templates/widgets/footer.html +++ b/cms/templates/widgets/footer.html @@ -1,6 +1,49 @@ <%! from django.core.urlresolvers import reverse %>
    +
    %endif @@ -202,6 +203,8 @@ function goto( mode) %if instructor_access:

    You may also delete the entire state of a student for a problem:

    +

    To delete the state of other XBlocks specify modulename/urlname, eg + combinedopenended/Humanities_SA_Peer

    %endif %endif From 57e5eb683bbb745c911d6988a3f08ac3a7e08bc2 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Mon, 1 Apr 2013 16:54:56 -0400 Subject: [PATCH 322/436] studio - basic design, interaction, and content for static in-page help --- cms/static/js/base.js | 14 +- cms/static/sass/_base.scss | 27 ++-- cms/static/sass/_cms_mixins.scss | 103 +++++++++++++-- cms/static/sass/_variables.scss | 4 +- cms/static/sass/base-style.scss | 4 +- cms/static/sass/elements/_controls.scss | 143 ++++++++++++++++++++ cms/static/sass/elements/_footer.scss | 133 +------------------ cms/static/sass/elements/_header.scss | 2 +- cms/static/sass/elements/_sock.scss | 152 ++++++++++++++++++++++ cms/static/sass/elements/_typography.scss | 23 ++-- cms/static/sass/views/_index.scss | 3 +- cms/templates/base.html | 11 +- cms/templates/widgets/footer.html | 52 +------- cms/templates/widgets/sock.html | 52 ++++++++ 14 files changed, 499 insertions(+), 224 deletions(-) create mode 100644 cms/static/sass/elements/_controls.scss create mode 100644 cms/static/sass/elements/_sock.scss create mode 100644 cms/templates/widgets/sock.html diff --git a/cms/static/js/base.js b/cms/static/js/base.js index d93d2fd8d4..7bea860531 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -87,7 +87,7 @@ $(document).ready(function () { $('a.show-tender').bind('click', smoothScrollTop); // toggling footer additional support - $('.show-support').bind('click', toggleSupport); + $('.cta-show-sock').bind('click', toggleSock); // toggling overview section details $(function () { @@ -458,9 +458,17 @@ function onKeyUp(e) { } } -function toggleSupport(e) { +function toggleSock(e) { e.preventDefault(); - $body.toggleClass('footer-is-expanded'); + $body.toggleClass('sock-is-shown'); + + $.smoothScroll({ + offset: -200, + easing: 'swing', + speed: 1000, + scrollElement: null, + scrollTarget: $('.wrapper-sock') + }); } function toggleSubmodules(e) { diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss index 328c7e99c3..10c046d22a 100644 --- a/cms/static/sass/_base.scss +++ b/cms/static/sass/_base.scss @@ -22,7 +22,7 @@ body, input { a { text-decoration: none; color: $blue; - @include transition(color .15s); + @include transition(color 0.25s ease-in-out); &:hover { color: #cb9c40; @@ -50,18 +50,22 @@ h1 { // ==================== -// layout - view +// layout - overall view .wrapper-view { - position: relative; min-height: 100%; + position: relative; } .wrapper-main { - padding-bottom: $footer-primary-height; + background: $gray-l5; + margin: ($baseline*1.5) 0 0 0; + padding-bottom: $bottom-height; } -.js.footer-is-expanded .wrapper-main { - padding-bottom: ($footer-primary-height*4); +.wrapper-bottom { + position: absolute; + bottom: 0; + height: $bottom-height; } // layout - basic page header @@ -286,19 +290,17 @@ h1 { } .title-1 { - + @extend .t-title-1; } .title-2 { - @include font-size(24); + @extend .t-title-2; margin: 0 0 ($baseline/2) 0; - font-weight: 600; } .title-3 { - @include font-size(16); + @extend .t-title-3; margin: 0 0 ($baseline/2) 0; - font-weight: 600; } .title-4 { @@ -365,8 +367,7 @@ h1 { // layout - grandfathered .main-wrapper { position: relative; - margin: ($baseline*2); - padding-bottom: $footer-primary-height; + margin: 0 ($baseline*2); } .inner-wrapper { diff --git a/cms/static/sass/_cms_mixins.scss b/cms/static/sass/_cms_mixins.scss index d837000a24..a25a07cb73 100644 --- a/cms/static/sass/_cms_mixins.scss +++ b/cms/static/sass/_cms_mixins.scss @@ -1,6 +1,7 @@ // studio - utilities - mixins and extends // ==================== +// mixins - utility @mixin clearfix { &:after { content: ''; @@ -11,6 +12,7 @@ } } +// mixins - grandfathered @mixin button { display: inline-block; padding: 4px 20px 6px; @@ -294,20 +296,97 @@ } } -@mixin sr-text { - border: 0; - clip: rect(0 0 0 0); - height: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - position: absolute; - width: 1px; -} - @mixin active { @include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0)); background-color: rgba(255, 255, 255, .3); @include box-shadow(0 -1px 0 rgba(0, 0, 0, .2) inset, 0 1px 0 #fff inset); text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); -} \ No newline at end of file +} + +// ==================== + +// extends - buttons +.btn { + @include box-sizing(border-box); + @include transition(color 0.25s ease-in-out, border-color 0.25s ease-in-out, background 0.25s ease-in-out, box-shadow 0.25s ease-in-out); + display: inline-block; + cursor: pointer; + + &:hover, &:active { + + } + + &.disabled, &[disabled] { + cursor: default; + pointer-events: none; + opacity: 0.5; + } + + .icon-inline { + display: inline-block; + vertical-align: middle; + margin-right: ($baseline/4); + } +} + +// pill button +.btn-pill { + @include border-radius($baseline/5); +} + +.btn-rounded { + @include border-radius($baseline/2); +} + +// primary button +.btn-primary { + @extend .btn; + @extend .btn-pill; + padding:($baseline/2) $baseline; + border-width: 1px; + border-style: solid; + line-height: 1.5em; + text-align: center; + + &:hover, &:active { + @include box-shadow(0 2px 1px $shadow-l1); + } + + &.current, &.active { + @include box-shadow(inset 1px 1px 2px $shadow-d1); + + &:hover, &:active { + @include box-shadow(inset 1px 1px 1px $shadow-d1); + } + } +} + +// secondary button +.btn-secondary { + @extend .btn; + @extend .btn-pill; + border-width: 1px; + border-style: solid; + padding:($baseline/2) $baseline; + background: transparent; + line-height: 1.5em; + text-align: center; + + &:hover, &:active { + + } + + &.current, &.active { + + } +} + +// ==================== + +// extends - depth levels +.depth0 { z-index: 0; } +.depth1 { z-index: 10; } +.depth2 { z-index: 100; } +.depth3 { z-index: 1000; } +.depth4 { z-index: 10000; } +.depth5 { z-index: 100000; } \ No newline at end of file diff --git a/cms/static/sass/_variables.scss b/cms/static/sass/_variables.scss index 806fbc8c57..88143c2365 100644 --- a/cms/static/sass/_variables.scss +++ b/cms/static/sass/_variables.scss @@ -13,7 +13,7 @@ $fg-max-width: 1280px; $fg-min-width: 900px; // elements -$footer-primary-height: (60px); +$bottom-height: ($baseline*3); // type $sans-serif: 'Open Sans', $verdana; @@ -170,4 +170,4 @@ $disabledGreen: rgb(124, 206, 153); $darkGreen: rgb(52, 133, 76); $lightBluishGrey: rgb(197, 207, 223); $lightBluishGrey2: rgb(213, 220, 228); -$error-red: rgb(253, 87, 87); +$error-red: rgb(253, 87, 87); \ No newline at end of file diff --git a/cms/static/sass/base-style.scss b/cms/static/sass/base-style.scss index 3015c3592d..da17441c71 100644 --- a/cms/static/sass/base-style.scss +++ b/cms/static/sass/base-style.scss @@ -23,9 +23,11 @@ // elements @import 'elements/typography'; @import 'elements/icons'; +@import 'elements/controls'; +@import 'elements/navigation'; @import 'elements/header'; @import 'elements/footer'; -@import 'elements/navigation'; +@import 'elements/sock'; @import 'elements/forms'; @import 'elements/modal'; @import 'elements/alerts'; diff --git a/cms/static/sass/elements/_controls.scss b/cms/static/sass/elements/_controls.scss new file mode 100644 index 0000000000..c4e96616a8 --- /dev/null +++ b/cms/static/sass/elements/_controls.scss @@ -0,0 +1,143 @@ +// studio - elements - UI controls +// ==================== + +// gray primary button +.btn-primary-gray { + @extend .btn-primary; + background: $gray-l1; + border-color: $gray-l2; + color: $white; + + &:hover, &:active { + border-color: $gray-l1; + background: $gray; + } + + &.current, &.active { + background: $gray-d1; + color: $gray-l1; + + &:hover, &:active { + background: $gray-d1; + } + } +} + +// blue primary button +.btn-primary-blue { + @extend .btn-primary; + background: $blue; + border-color: $blue-s1; + color: $white; + + &:hover, &:active { + background: $blue-s2; + border-color: $blue-s2; + } + + &.current, &.active { + background: $blue-d1; + color: $blue-l4; + border-color: $blue-d2; + + &:hover, &:active { + background: $blue-d1; + } + } +} + +// green primary button +.btn-primary-green { + @extend .btn-primary; + background: $green; + border-color: $green; + color: $white; + + &:hover, &:active { + background: $green-s1; + border-color: $green-s1; + } + + &.current, &.active { + background: $green-d1; + color: $green-l4; + border-color: $green-d2; + + &:hover, &:active { + background: $green-d1; + } + } +} + +// gray secondary button +.btn-secondary-gray { + @extend .btn-secondary; + border-color: $gray-l3; + color: $gray-l1; + + &:hover, &:active { + background: $gray-l3; + color: $gray-d2; + } + + &.current, &.active { + background: $gray-d2; + color: $gray-l5; + + &:hover, &:active { + background: $gray-d2; + } + } +} + +// blue secondary button +.btn-secondary-blue { + @extend .btn-secondary; + border-color: $blue-l3; + color: $blue; + + &:hover, &:active { + background: $blue-l3; + color: $blue-s2; + } + + &.current, &.active { + border-color: $blue-l3; + background: $blue-l3; + color: $blue-d1; + + &:hover, &:active { + + } + } +} + +// green secondary button +.btn-secondary-green { + @extend .btn-secondary; + border-color: $green-l4; + color: $green-l2; + + &:hover, &:active { + background: $green-l4; + color: $green-s1; + } + + &.current, &.active { + background: $green-s1; + color: $green-l4; + + &:hover, &:active { + background: $green-s1; + } + } +} + +// ==================== + +// layout-based buttons + +// ==================== + +// calls-to-action + diff --git a/cms/static/sass/elements/_footer.scss b/cms/static/sass/elements/_footer.scss index f2941e11be..d0bb9b9dee 100644 --- a/cms/static/sass/elements/_footer.scss +++ b/cms/static/sass/elements/_footer.scss @@ -3,13 +3,10 @@ .wrapper-footer { @include box-shadow(inset 0 1px 2px $shadow-d1); - margin: ($baseline*1.5) 0 0 0; - padding: $baseline; - position: absolute; - bottom: 0; + position: relative; width: 100%; - height: $footer-primary-height; - background: $gray-l3; + padding: $baseline; + background: $gray-l4; footer.primary { @include clearfix(); @@ -55,8 +52,6 @@ .ss-icon { @include transition(top .25s ease-in-out .25s); @include font-size(15); - position: relative; - top: 0; display: inline-block; vertical-align: middle; margin-right: ($baseline/4); @@ -67,133 +62,15 @@ color: $gray-d2; .ss-icon { - top: -($baseline/10); color: $gray-d2; } } - } - } - } - } - // sock - additional help - .sock { - @include clearfix(); - @extend .t-copy-sub2; - max-width: $fg-max-width; - min-width: $fg-min-width; - width: flex-grid(12); - margin: 0 auto $baseline auto; - border-bottom: 1px solid $gray-l2; - padding-bottom: $baseline; - - header { - - .title { - @extend .t-title-3; - } - - .ss-icon { - @extend .t-icon; - @extend .icon-inline; - } - } - - // shared elements - .support, .feedback { - @include box-sizing(border-box); - - .title { - - } - - .copy { - margin: 0 0 $baseline 0; - } - - .list-actions { - @include clearfix(); - - .action-item { - float: left; - margin-right: ($baseline/2); - - &:last-child { - margin-right: 0; - } - - .action { - display: block; - - .ss-icon { - @include font-size(15); - @extend .t-icon; - @extend .icon-inline; - } - - &:hover, &:active { - - .ss-icon { - } - } - } - - .tip { - @extend .sr; + &.is-active { + color: $gray-d2; } } - - .action-primary { - @include gray-button; - @include transition(all .15s); - @include font-size(13); - font-weight: 500; - padding: ($baseline/4) ($baseline/2); - text-align: center; - } } } - - // studio support content - .support { - width: flex-grid(8,12); - float: left; - margin-right: flex-gutter(); - - .action-item { - width: flexgrid(4,8); - } - } - - // studio feedback content - .feedback { - width: flex-grid(4,12); - float: left; - - .action-item { - width: flexgrid(4,4); - } - } - } -} - - -// js-enabled styling -.js .wrapper-footer { - @include transition(height 2s ease-in-out); - height: $footer-primary-height; - overflow: hidden; - - .sock { - display: none; - } -} - - // expanded view -.js.footer-is-expanded .wrapper-footer { - height: ($footer-primary-height*4); - - .sock { - display: block; } } \ No newline at end of file diff --git a/cms/static/sass/elements/_header.scss b/cms/static/sass/elements/_header.scss index e8df37f57f..12c736de7d 100644 --- a/cms/static/sass/elements/_header.scss +++ b/cms/static/sass/elements/_header.scss @@ -2,7 +2,7 @@ // ==================== .wrapper-header { - margin: 0 0 ($baseline*1.5) 0; + margin: 0; padding: $baseline; border-bottom: 1px solid $gray; @include box-shadow(0 1px 5px 0 rgba(0,0,0, 0.1)); diff --git a/cms/static/sass/elements/_sock.scss b/cms/static/sass/elements/_sock.scss new file mode 100644 index 0000000000..27f43935f6 --- /dev/null +++ b/cms/static/sass/elements/_sock.scss @@ -0,0 +1,152 @@ +// studio - elements - support sock +// ==================== + +.wrapper-sock { + @include transition(background 0.25s ease-in-out); + @include clearfix(); + position: relative; + width: 100%; + margin: ($baseline*2.5) 0; + padding: 0 $baseline; + border-top: 1px solid $gray-l4; + + // actions + .list-cta { + position: relative; + top: -($baseline); + margin: 0 auto; + text-align: center; + + .cta-show-sock { + @extend .btn-secondary-gray; + @extend .t-action3; + background: $gray-l5; + padding: ($baseline/2) $baseline; + + .icon { + @include font-size(17); + } + } + } + + // sock - additional help + .sock { + @include clearfix(); + @extend .t-copy-sub2; + display: none; + opacity: 0.0; + pointer-events: none; + max-width: $fg-max-width; + min-width: $fg-min-width; + width: flex-grid(12); + margin: 0 auto; + color: $gray-l3; + + // support body + header { + + .title { + @extend .t-title-3; + margin-bottom: ($baseline/2); + } + + .ss-icon { + @extend .t-icon; + @extend .icon-inline; + } + } + + // shared elements + .support, .feedback { + @include box-sizing(border-box); + + .title { + @extend .t-title-3; + color: $white; + } + + .copy { + margin: 0 0 $baseline 0; + } + + .list-actions { + @include clearfix(); + + .action-item { + float: left; + margin-right: ($baseline/2); + + &:last-child { + margin-right: 0; + } + + .action { + display: block; + + .icon { + @include font-size(18); + } + + &:hover, &:active { + + } + } + + .tip { + @extend .sr; + } + } + + .action-primary { + @extend .btn-primary-blue; + @extend .t-action3; + } + } + } + + // studio support content + .support { + width: flex-grid(8,12); + float: left; + margin-right: flex-gutter(); + + .action-item { + width: flexgrid(4,8); + } + } + + // studio feedback content + .feedback { + width: flex-grid(4,12); + float: left; + + .action-item { + width: flexgrid(4,4); + } + } + } +} + +// case: sock content is shown +.sock-is-shown { + + .wrapper-sock { + @include linear-gradient($gray-d4 0%, $gray-d3 2%, $gray-d3 98%, $gray-d4 100%); + border-bottom: 1px solid $white; + border-top: 1px solid $white; + padding-bottom: ($baseline*2); + padding-top: ($baseline*2); + + .cta-show-sock { + display: none; + opacity: 0.0; + pointer-events: none; + } + + .sock { + display: block; + opacity: 1.0; + pointer-events: auto; + } + } +} \ No newline at end of file diff --git a/cms/static/sass/elements/_typography.scss b/cms/static/sass/elements/_typography.scss index a9b3d362ee..32c4b3928b 100644 --- a/cms/static/sass/elements/_typography.scss +++ b/cms/static/sass/elements/_typography.scss @@ -7,18 +7,16 @@ } .t-title-1 { - @include font-size(32); + @include font-size(36); } .t-title-2 { @include font-size(24); - margin: 0 0 ($baseline/2) 0; font-weight: 600; } .t-title-3 { @include font-size(16); - margin: 0 0 ($baseline/2) 0; font-weight: 600; } @@ -60,18 +58,23 @@ // ==================== // actions/labels -.t-action { +.t-action1 { @include font-size(14); font-weight: 600; } -.t-action-primary { - @include font-size(14); - font-weight: 600; -} - -.t-action-primary-s { +.t-action2 { @include font-size(13); + font-weight: 600; + text-transform: uppercase; +} + +.t-action3 { + @include font-size(13); +} + +.t-action4 { + @include font-size(12); } // ==================== diff --git a/cms/static/sass/views/_index.scss b/cms/static/sass/views/_index.scss index 88beccc82d..7c45530339 100644 --- a/cms/static/sass/views/_index.scss +++ b/cms/static/sass/views/_index.scss @@ -296,8 +296,7 @@ body.index { // call to action content .wrapper-content-cta { position: relative; - padding-bottom: ($footer-primary-height*2); - padding-top: ($baseline*2); + padding: ($baseline*2) 0; background: $white; } diff --git a/cms/templates/base.html b/cms/templates/base.html index e587619bae..46d566116e 100644 --- a/cms/templates/base.html +++ b/cms/templates/base.html @@ -53,10 +53,17 @@
    <%include file="widgets/header.html" /> +
    - <%block name="content"> + <%block name="content"> + % if user.is_authenticated(): + <%include file="widgets/sock.html" /> + % endif +
    + +
    + <%include file="widgets/footer.html" />
    - <%include file="widgets/footer.html" />
    <%include file="widgets/tender.html" /> diff --git a/cms/templates/widgets/footer.html b/cms/templates/widgets/footer.html index b6a3b84272..eea155ce19 100644 --- a/cms/templates/widgets/footer.html +++ b/cms/templates/widgets/footer.html @@ -1,49 +1,5 @@ <%! from django.core.urlresolvers import reverse %> - -
    + +
    +
    +

    TRAINER

    +

    All those Universities on the edX homepage are full of incredible professors and teaching teams, designing on-line courses that will change the face of education. The edX team is constantly training whole new university teams on how to make their visions shine using edX software. We’re looking for some truly talented people to help train people on the tools that are enabling the future of education.

    +

    Responsibilities:

    +
      +
    • Facilitate training programs as required ensuring that best practices are incorporated in all learning environments.
    • +
    • Create and design learning materials for training curriculums, incorporate edX best practices into training curriculum.
    • +
    • Incorporate key performance metrics into training modules; participate in strategic initiatives
    • +
    • Measure, monitor and share training results with business units to identify future training opportunities.
    • +
    • Identify and leverage existing resources to maximize partner efficiency and productivity.
    • +
    • Work with both Universities and edX to provide strategic input based on future training needs.
    • +
    • Communicate effectively in oral and written presentations.
    • +
    • Analyze learners training needs and identify cross training opportunities.
    • +
    • Mentor and train others on training tools to expand training efficiency and uniformity.
    • +
    • Build relationships with universities to be viewed as a trusted training partner.
    • +
    +

    Requirements:

    +
      +
    • Minimum of 1-3 years experience developing and delivering educational training, preferably in an educational technology organization.
    • +
    • Lean and Agile thinking and training. Experienced in Scrum or kanban preferred.
    • +
    • Excellent interpersonal skills including proven presentation and facilitation skills.
    • +
    • Strong oral and written communication skills.
    • +
    • Flexibility to work on a variety of initiatives; prior startup experience preferred.
    • +
    • Outstanding work ethic, results-oriented, and creative/innovative style.
    • +
    • Proactive, optimistic approach to problem solving.
    • +
    • Commitment to constant personal and organizational improvement.
    • +
    • Willingness to travel to partner sites as needed.
    • +
    • Bachelors or Master’s in Education, organizational learning, instructional design or other related field preferred. But we're all about education, so let us know how you gained what you need to succeed in this role: projects after completing 6.00x or CS50x, Xbox cheevos, on-line guilds led, large scale innovations championed.
    • +
    + +

    If you are interested in this position, please send an email to jobs@edx.org.

    +
    +
    + +

    INSTRUCTIONAL DESIGNER

    @@ -187,8 +224,10 @@

    Qualifications:

      -
    • Master's Degree in Educational Technology, Instructional Design or related field. Experience in higher education with additional experience in a start-up or research environment preferable.
    • -
    • Excellent interpersonal and communication (written and verbal), project management, problem-solving and time management skills. The ability to be flexible with projects and to work on multiple courses essential.
    • Ability to meet deadlines and manage expectations of constituents. +
    • Master's Degree in Educational Technology, Instructional Design or related field. Experience in higher education with additional experience in a start-up or research environment preferable. But we're all about education, so let us know how you gained what you need to succeed in this role: projects after completing 6.00x or CS50x, Xbox cheevos, on-line guilds led, large scale innovations championed.
    • +
    • Experience in higher education with additional experience in a start-up or research environment preferable.
    • +
    • Excellent interpersonal and communication (written and verbal), project management, problem-solving and time management skills. The ability to be flexible with projects and to work on multiple courses essential.
    • +
    • Ability to meet deadlines and manage expectations of constituents.
    • Capacity to develop new and relevant technology skills. Experience using game theory design and learning analytics to inform instructional design decisions and strategy.
    • Technical Skills: Video and screencasting experience. LMS Platform experience, xml, HTML, CSS, Adobe Design Suite, Camtasia or Captivate experience. Experience with web 2.0 collaboration tools.
    @@ -205,16 +244,16 @@

    edX Program Managers (PM) lead the edX's course production process. They are systems thinkers who manage the creation of a course from start to finish. PMs work with University Professors and course staff to help them take advantage of edX services to create world class online learning offerings and encourage the exploration of an emerging form of higher education.

    Responsibilities:

      -
    • Create and execute the course production cycle. PMs are able to examine and explain what they do in great detail and able to think abstractly about people, time, and processes. They coordinate the efforts of multiple
    • teams engaged in the production of the courses assigned to them. +
    • Create and execute the course production cycle. PMs are able to examine and explain what they do in great detail and able to think abstractly about people, time, and processes. They coordinate the efforts of multiple teams engaged in the production of the courses assigned to them.
    • Train partners and drive best practices adoption. PMs train course staff from partner institutions and help them adopt best practices for workflow and tools.
    • Build capacity. Mentor staff at partner institutions, train the trainers that help them scale their course production ability.
    • -
    • Create visibility. PMs are responsible for making the state of the course production system accessible and comprehensible to all stakeholders. They are capable of training Course development teams in Scrum and
    • Kanban, and are Lean thinkers and educators. +
    • Create visibility. PMs are responsible for making the state of the course production system accessible and comprehensible to all stakeholders. They are capable of training Course development teams in Scrum and Kanban, and are Lean thinkers and educators.
    • Improve workflows. PMs are responsible for carefully assessing the methods and outputs of each course and adjusting them to take best advantage of available resources.
    • Encourage innovation. Spark creativity in course teams to build new courses that could never be produced in brick and mortar settings.

    Qualifications:

      -
    • Bachelor's Degree. Master's Degree preferred.
    • +
    • Bachelor's Degree. Master's Degree preferred. But we're all about education, so let us know how you gained what you need to succeed in this role: projects after completing 6.00x or CS50x, Xbox cheevos, on-line guilds led, large scale innovations championed.
    • At least 2 years of experience working with University faculty and administrators.
    • Proven record of successful Scrum or Kanban project management, including use of project management tools.
    • Ability to create processes that systematically provide solutions to open ended challenges.
    • @@ -256,7 +295,7 @@

    Skills:

      -
    • Bachelor’s degree or higher
    • +
    • Bachelor’s degree or higher. But we're all about education, so let us know how you gained what you need to succeed in this role: projects after completing 6.00x or CS50x, Xbox cheevos, on-line guilds led, large scale innovations championed.
    • Exquisite communication skills, especially listening
    • Inexhaustible attention to detail with the ability to let go of perfection
    • Deep commitment to Lean project management, including a dedication to its best intentions not just its rituals
    • @@ -286,8 +325,7 @@

    Qualifications:

      -
    • Bachelor’s degree or higher in a Technical Area
    • -
    • MBA or Masters in Design preferred
    • +
    • Bachelor’s degree or higher in a Technical Area, MBA or Masters in Design preferred. But we're all about education, so let us know how you gained what you need to succeed in this role: projects after completing 6.00x or CS50x, Xbox cheevos, on-line guilds led, large scale innovations championed.
    • Proven ability to develop and implement strategy
    • Exquisite organizational skills
    • Deep analytical skills
    • @@ -319,7 +357,7 @@

    Qualifications:

      -
    • Bachelor’s degree or higher
    • +
    • Bachelor’s degree or higher. But we're all about education, so let us know how you gained what you need to succeed in this role: projects after completing 6.00x or CS50x, Xbox cheevos, on-line guilds led, large scale innovations championed.
    • Thorough knowledge of Python, DJango, XML,HTML, CSS , Javascript and backbone.js
    • Ability to work on multiple projects simultaneously without splintering
    • Tactfully escalate conflicting deadlines or priorities only when needed. Otherwise help the team members negotiate a solution.
    • @@ -358,7 +396,7 @@
    • Test Driven Development
    • Committed to Documentation best practices so your code can be consumed in an open source environment
    • Contributor to or consumer of Open Source Frameworks
    • -
    • BS in Computer Science from top-tier institution
    • +
    • BS in Computer Science from top-tier institution. But we're all about education, so let us know how you gained what you need to succeed in this role: projects after completing 6.00x or CS50x, Xbox cheevos, on-line guilds led, large scale innovations championed.
    • Acknowledged by peers as a technology leader
    @@ -367,6 +405,51 @@
    +
    +
    +

    DEVOPS ENGINEER – SYESTEMS ADMINISTRATOR

    +

    The Devop Engineers at edX help develop and maintain the infrastructure in AWS for all services and systems required to run edX. We're seeking a capable systems administrator who is unafraid of scripting languages and development to build out tools in order to improve the functionality of edX. The devops team primarily focuses on the provisioning, configuration, and deployment of services at edX. If you have a passion for automation and constant improvement then we want to hear from you. Our production environment is primarily built on Ubuntu (in AWS) and we use Puppet and Fabric to manage most of the environment.

    +

    In addition to the primary task of building infrastructure the Devops team supports the developers in a variety of other contexts, including helping with desktop development environments if required. We participate in on-call and emergency support and there will be occasional out of normal hours work required.

    +

    Responsibilities:

    +
      +
    • Work with developers and staff to maintain and improve the infrastructure of edX.
    • +
    • Assist where needed with other technical support tasks to support the fast moving pace of edX.
    • +
    • Rapidly diagnose and resolve faults with organization-wide servers and services, and communicate to users as appropriate.
    • +
    +

    Requirements:

    +
      +
    • Bachelor's degree in engineering or computer science. But we're all about education, so let us know how you gained what you need to succeed in this role: projects after completing 6.00x or CS50x, Xbox cheevos, on-line guilds led, large scale innovations championed.3 or more years of systems administration.
    • +
    • Must have an excellent working knowledge of Linux both as an end-user and as an administrator.
    • +
    • Must be adept in programming/scripting languages such as Python, Ruby, Bash.
    • +
    • Must be familiar with a configuration management system such as Puppet, Chef, Ansible.
    • +
    • Must have experience running web applications in a production environment.
    • +
    • Must have excellent personal interaction skills as the position requires interfacing with a wide range of people up to board level.
    • +
    • Ideally possesses experience with some of the following technologies: nginx, mysql, mongodb, django environments, splunk, git.
    • +
    + +

    If you are interested in this position, please send an email to jobs@edx.org.

    +
    +
    + + +
    +
    +

    LEARNING SCIENCES ENGINEER

    +

    In 2012, edX reinvented education. In 2013, the edX learning sciences team is charged with reinventing education, again. The goal of the team is to prototype and develop technologies which will radically change the way students learn and instructors teach. We will engage in projects in learning analytics, crowdsourced content development, intelligent tutoring, as well as radical changes to the ways course content is structured. We are looking to opportunistically build a small (3 person), fast-moving team capable of rapidly bringing advanced development projects to prototype and to market. All members of the team must be spectacular software engineers capable of working in or adapting to dynamic, duck typed, functional languages (Python and JavaScript). In addition, we are looking for some combination of:

    +
      +
    • Deep expertise in mathematics, and in particular, advanced linear algebra, machine learning, big data, psychometrics, and probability.
    • +
    • UX design. Capable of envisioning user interface for software that does things that have never been done before, and bringing them through to market. Skills should be broad and range the full gamut: graphic design, UX, HTML5, basic JavaScript, and CSS.
    • +
    • Interest and experience in both research and practice of education, cognitive science, and related fields.
    • +
    • Core backend experience (Python, Django, MongoDB, SQL)
    • +
    • Background in social networks and social network analysis (both social science and mathematics) is desirable as well.
    • +
    +

    More than anything, we’re looking for spectacular people capable of very rapidly building things which have never been built before. We’re capable of providing both traditional employment, and potentially, in partnership with MIT, more academic opportunities.

    + +

    If you are interested in this position, please send an email to jobs@edx.org.

    +
    +
    + +
    @@ -374,12 +457,15 @@

    How to Apply

    E-mail your resume, cover letter and any other materials to jobs@edx.org

    From d498694659bc000f301dac9e7afb3ae35551b37b Mon Sep 17 00:00:00 2001 From: Greg Price Date: Thu, 4 Apr 2013 13:46:46 -0400 Subject: [PATCH 386/436] Fix date on Stanford press release Lighthouse ticket [#328 state:resolved] --- .../static_templates/press_releases/stanford_announcement.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/templates/static_templates/press_releases/stanford_announcement.html b/lms/templates/static_templates/press_releases/stanford_announcement.html index be788f319c..2b19b62de3 100644 --- a/lms/templates/static_templates/press_releases/stanford_announcement.html +++ b/lms/templates/static_templates/press_releases/stanford_announcement.html @@ -73,7 +73,7 @@ Stanford University and edX, the not-for-profi

    Positions

    How to Apply

    E-mail your resume, cover letter and any other materials to jobs@edx.org

    From 5137119f2b67938e5fc22d4e934682894acf6b81 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Mon, 8 Apr 2013 13:55:38 -0400 Subject: [PATCH 430/436] add a simple Studio walkthough to check page loads --- .../contentstore/tests/test_contentstore.py | 107 ++++++++++++++++++ common/test/data/simple/course.xml | 6 +- 2 files changed, 110 insertions(+), 3 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 1791d845cd..451ab96ca6 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -628,6 +628,113 @@ class ContentStoreTest(ModuleStoreTestCase): self.assertIn('markdown', context, "markdown is missing from context") self.assertNotIn('markdown', problem.editable_metadata_fields, "Markdown slipped into the editable metadata fields") + def test_cms_imported_course_walkthrough(self): + """ + Import and walk through some common URL endpoints. This just verifies non-500 and no other + correct behavior, so it is not a deep test + """ + import_from_xml(modulestore(), 'common/test/data/', ['simple']) + loc = Location(['i4x', 'edX', 'simple', 'course', '2012_Fall', None]) + resp = self.client.get(reverse('course_index', + kwargs={'org': loc.org, + 'course': loc.course, + 'name': loc.name})) + + self.assertEqual(200, resp.status_code) + self.assertContains(resp, 'Chapter 2') + + # go to various pages + + # import page + resp = self.client.get(reverse('import_course', + kwargs={'org': loc.org, + 'course': loc.course, + 'name': loc.name})) + self.assertEqual(200, resp.status_code) + + # export page + resp = self.client.get(reverse('export_course', + kwargs={'org': loc.org, + 'course': loc.course, + 'name': loc.name})) + self.assertEqual(200, resp.status_code) + + # manage users + resp = self.client.get(reverse('manage_users', + kwargs={'location': loc.url()})) + self.assertEqual(200, resp.status_code) + + # course info + resp = self.client.get(reverse('course_info', + kwargs={'org': loc.org, + 'course': loc.course, + 'name': loc.name})) + self.assertEqual(200, resp.status_code) + + # settings_details + resp = self.client.get(reverse('settings_details', + kwargs={'org': loc.org, + 'course': loc.course, + 'name': loc.name})) + self.assertEqual(200, resp.status_code) + + # settings_details + resp = self.client.get(reverse('settings_grading', + kwargs={'org': loc.org, + 'course': loc.course, + 'name': loc.name})) + self.assertEqual(200, resp.status_code) + + # static_pages + resp = self.client.get(reverse('static_pages', + kwargs={'org': loc.org, + 'course': loc.course, + 'coursename': loc.name})) + self.assertEqual(200, resp.status_code) + + # static_pages + resp = self.client.get(reverse('asset_index', + kwargs={'org': loc.org, + 'course': loc.course, + 'name': loc.name})) + self.assertEqual(200, resp.status_code) + + # go look at a subsection page + subsection_location = loc._replace(category='sequential', name='test_sequence') + resp = self.client.get(reverse('edit_subsection', + kwargs={'location': subsection_location.url()})) + self.assertEqual(200, resp.status_code) + + # go look at the Edit page + unit_location = loc._replace(category='vertical', name='test_vertical') + resp = self.client.get(reverse('edit_unit', + kwargs={'location': unit_location.url()})) + self.assertEqual(200, resp.status_code) + + # delete a component + del_loc = loc._replace(category='html', name='test_html') + resp = self.client.post(reverse('delete_item'), + json.dumps({'id': del_loc.url()}), "application/json") + self.assertEqual(200, resp.status_code) + + # delete a unit + del_loc = loc._replace(category='vertical', name='test_vertical') + resp = self.client.post(reverse('delete_item'), + json.dumps({'id': del_loc.url()}), "application/json") + self.assertEqual(200, resp.status_code) + + # delete a unit + del_loc = loc._replace(category='sequential', name='test_sequence') + resp = self.client.post(reverse('delete_item'), + json.dumps({'id': del_loc.url()}), "application/json") + self.assertEqual(200, resp.status_code) + + # delete a chapter + del_loc = loc._replace(category='chapter', name='chapter_2') + resp = self.client.post(reverse('delete_item'), + json.dumps({'id': del_loc.url()}), "application/json") + self.assertEqual(200, resp.status_code) + def test_import_metadata_with_attempts_empty_string(self): import_from_xml(modulestore(), 'common/test/data/', ['simple']) module_store = modulestore('direct') diff --git a/common/test/data/simple/course.xml b/common/test/data/simple/course.xml index 532a55792c..b86a1898f9 100644 --- a/common/test/data/simple/course.xml +++ b/common/test/data/simple/course.xml @@ -12,15 +12,15 @@
    - +