From 66657db0bee2e26a3966e6ca04b2483f88918754 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Mon, 4 Mar 2013 09:49:41 -0700 Subject: [PATCH 001/241] 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 002/241] 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 49f85211fa5c5550897d25aceb786ac82d1259ee Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Fri, 8 Mar 2013 03:39:34 -0700 Subject: [PATCH 003/241] 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 004/241] 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 005/241] 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 006/241] 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 007/241] 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 008/241] 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 009/241] 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 20b04c9bf20bdd9be4e57ce7a1b9e0033b2ea325 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 6 Mar 2013 13:06:15 -0500 Subject: [PATCH 010/241] Fixed broken calculator test in lms coffee scripts --- lms/static/coffee/spec/calculator_spec.coffee | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/lms/static/coffee/spec/calculator_spec.coffee b/lms/static/coffee/spec/calculator_spec.coffee index 072d220a44..a8210ce68b 100644 --- a/lms/static/coffee/spec/calculator_spec.coffee +++ b/lms/static/coffee/spec/calculator_spec.coffee @@ -31,12 +31,19 @@ describe 'Calculator', -> $('form#calculator').submit() describe 'toggle', -> - it 'toggle the calculator and focus the input', -> - spyOn $.fn, 'focus' - @calculator.toggle(jQuery.Event("click")) + it 'focuses the input when toggled', -> - expect($('li.calc-main')).toHaveClass('open') - expect($('#calculator_wrapper #calculator_input').focus).toHaveBeenCalled() + # Since the focus is called asynchronously, we need to + # wait until focus() is called. + didFocus = false + runs -> + spyOn($.fn, 'focus').andCallFake (elementName) -> didFocus = true + @calculator.toggle(jQuery.Event("click")) + + waitsFor (-> didFocus), "focus() should have been called on the input", 1000 + + runs -> + expect($('#calculator_wrapper #calculator_input').focus).toHaveBeenCalled() it 'toggle the close button on the calculator button', -> @calculator.toggle(jQuery.Event("click")) From 618de5df25642c4962b5870438aa13586b2ffd36 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 6 Mar 2013 17:08:12 -0500 Subject: [PATCH 011/241] Fixed import error for histogram.coffee --- common/templates/jasmine/base.html | 7 ++++--- lms/static/coffee/files.json | 3 --- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/common/templates/jasmine/base.html b/common/templates/jasmine/base.html index 96507bdebf..40308c7bbe 100644 --- a/common/templates/jasmine/base.html +++ b/common/templates/jasmine/base.html @@ -13,14 +13,15 @@ + {% load compressed %} + {# static files #} + {% compressed_js 'js-test-source' %} + {# source files #} {% for url in suite.js_files %} {% endfor %} - {% load compressed %} - {# static files #} - {% compressed_js 'js-test-source' %} {# spec files #} {% compressed_js 'spec' %} diff --git a/lms/static/coffee/files.json b/lms/static/coffee/files.json index 5dc03613b9..0efe488dd9 100644 --- a/lms/static/coffee/files.json +++ b/lms/static/coffee/files.json @@ -5,8 +5,5 @@ "/static/js/vendor/jquery-ui.min.js", "/static/js/vendor/jquery.leanModal.min.js", "/static/js/vendor/flot/jquery.flot.js" - ], - "static_files": [ - "js/application.js" ] } From 6b94090e673ab80723de67d3d95be819714ede44 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 6 Mar 2013 17:41:44 -0500 Subject: [PATCH 012/241] Fixed jasmine test issue with cms caused by previous change to order of file import. Introduced static_files list in files.json (supported in original django_jasmine implementation) that loads static files before any compiled files. --- cms/static/coffee/files.json | 18 +++++++++--------- common/templates/jasmine/base.html | 4 ++++ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/cms/static/coffee/files.json b/cms/static/coffee/files.json index 2249813b04..e7a66b5bc0 100644 --- a/cms/static/coffee/files.json +++ b/cms/static/coffee/files.json @@ -1,12 +1,12 @@ { - "js_files": [ - "/static/js/vendor/RequireJS.js", - "/static/js/vendor/jquery.min.js", - "/static/js/vendor/jquery-ui.min.js", - "/static/js/vendor/jquery.ui.draggable.js", - "/static/js/vendor/jquery.cookie.js", - "/static/js/vendor/json2.js", - "/static/js/vendor/underscore-min.js", - "/static/js/vendor/backbone-min.js" + "static_files": [ + "js/vendor/RequireJS.js", + "js/vendor/jquery.min.js", + "js/vendor/jquery-ui.min.js", + "js/vendor/jquery.ui.draggable.js", + "js/vendor/jquery.cookie.js", + "js/vendor/json2.js", + "js/vendor/underscore-min.js", + "js/vendor/backbone-min.js" ] } diff --git a/common/templates/jasmine/base.html b/common/templates/jasmine/base.html index 40308c7bbe..9a1b3bed92 100644 --- a/common/templates/jasmine/base.html +++ b/common/templates/jasmine/base.html @@ -15,6 +15,10 @@ {% load compressed %} {# static files #} + {% for url in suite.static_files %} + + {% endfor %} + {% compressed_js 'js-test-source' %} {# source files #} From a777a9e6e3673d9c049e2c3ad7cba6d7ec756c62 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Mon, 11 Mar 2013 11:20:20 -0400 Subject: [PATCH 013/241] Updated broken tabs.js Jasmine tests. --- lms/static/coffee/spec/modules/tab_spec.coffee | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lms/static/coffee/spec/modules/tab_spec.coffee b/lms/static/coffee/spec/modules/tab_spec.coffee index 909f0d7cda..6fba470974 100644 --- a/lms/static/coffee/spec/modules/tab_spec.coffee +++ b/lms/static/coffee/spec/modules/tab_spec.coffee @@ -22,18 +22,23 @@ describe 'Tab', -> it 'bind the tabs', -> expect($.fn.tabs).toHaveBeenCalledWith show: @tab.onShow + # As of jQuery 1.9, the onShow callback is deprecated + # http://jqueryui.com/upgrade-guide/1.9/#deprecated-show-event-renamed-to-activate + # The code below tests that onShow does what is expected, + # but note that onShow will NOT be called when the user + # clicks on the tab if we're using jQuery version >= 1.9 describe 'onShow', -> beforeEach -> @tab = new Tab 1, @items - $('[href="#tab-1-0"]').click() + @tab.onShow($('#tab-1-0'), {'index': 1}) it 'replace content in the container', -> - $('[href="#tab-1-1"]').click() + @tab.onShow($('#tab-1-1'), {'index': 1}) expect($('#tab-1-0').html()).toEqual '' expect($('#tab-1-1').html()).toEqual 'Video 2' expect($('#tab-1-2').html()).toEqual '' it 'trigger contentChanged event on the element', -> spyOnEvent @tab.el, 'contentChanged' - $('[href="#tab-1-1"]').click() + @tab.onShow($('#tab-1-1'), {'index': 1}) expect('contentChanged').toHaveBeenTriggeredOn @tab.el From a496cc0a1e64d1c8c74ccbddba7cd0f3fcf70655 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Mon, 11 Mar 2013 13:28:28 -0400 Subject: [PATCH 014/241] 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 3568db73e39ed31611ebfad4d02e8673723d48aa Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Mon, 11 Mar 2013 13:54:44 -0400 Subject: [PATCH 015/241] initial wiring of 404/500 error pages for Studio --- cms/djangoapps/contentstore/views.py | 10 +++++++++- cms/urls.py | 6 ++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 34003d71a4..345861f979 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -18,7 +18,7 @@ from django.core.files.temp import NamedTemporaryFile # to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz' from PIL import Image -from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden +from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseServerError, HttpResponseNotFound from django.contrib.auth.decorators import login_required from django.core.exceptions import PermissionDenied from django.core.context_processors import csrf @@ -1569,3 +1569,11 @@ def event(request): console logs don't get distracted :-) ''' return HttpResponse(True) + + +def render_404(request): + return HttpResponseNotFound(render_to_string('404.html', {})) + + +def render_500(request): + return HttpResponseServerError(render_to_string('500.html', {})) diff --git a/cms/urls.py b/cms/urls.py index d43b9bc44c..69ce4a540d 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -104,3 +104,9 @@ if settings.ENABLE_JASMINE: urlpatterns = urlpatterns + (url(r'^_jasmine/', include('django_jasmine.urls')),) urlpatterns = patterns(*urlpatterns) + +#Custom error pages +handler404 = 'contentstore.views.render_404' +handler500 = 'contentstore.views.render_500' + + From 9d8c023acb05271a71e040236df5db7bbddb530b Mon Sep 17 00:00:00 2001 From: Will Daly Date: Mon, 11 Mar 2013 15:58:31 -0400 Subject: [PATCH 016/241] Removed calls to Navigation.bind() and Calculator.bind() that were causing phantomjs interpreter to fail with undefined method error. --- lms/static/coffee/spec/calculator_spec.coffee | 3 --- lms/static/coffee/spec/navigation_spec.coffee | 3 --- 2 files changed, 6 deletions(-) diff --git a/lms/static/coffee/spec/calculator_spec.coffee b/lms/static/coffee/spec/calculator_spec.coffee index a8210ce68b..8258d8965a 100644 --- a/lms/static/coffee/spec/calculator_spec.coffee +++ b/lms/static/coffee/spec/calculator_spec.coffee @@ -4,9 +4,6 @@ describe 'Calculator', -> @calculator = new Calculator describe 'bind', -> - beforeEach -> - Calculator.bind() - it 'bind the calculator button', -> expect($('.calc')).toHandleWith 'click', @calculator.toggle diff --git a/lms/static/coffee/spec/navigation_spec.coffee b/lms/static/coffee/spec/navigation_spec.coffee index 1340984e52..b351164b63 100644 --- a/lms/static/coffee/spec/navigation_spec.coffee +++ b/lms/static/coffee/spec/navigation_spec.coffee @@ -32,11 +32,9 @@ describe 'Navigation', -> heightStyle: 'content' it 'binds the accordionchange event', -> - Navigation.bind() expect($('#accordion')).toHandleWith 'accordionchange', @navigation.log it 'bind the navigation toggle', -> - Navigation.bind() expect($('#open_close_accordion a')).toHandleWith 'click', @navigation.toggle describe 'when the #accordion does not exists', -> @@ -45,7 +43,6 @@ describe 'Navigation', -> it 'does not activate the accordion', -> spyOn $.fn, 'accordion' - Navigation.bind() expect($('#accordion').accordion).wasNotCalled() describe 'toggle', -> From 1b5f0400212cbd7f1d975cd4da3c523685395aad Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Tue, 12 Mar 2013 21:57:40 -0400 Subject: [PATCH 017/241] 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 018/241] 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 020/241] 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 024/241] 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 017fd06bfc1ff28635bb54f427c5256ac64d89ba Mon Sep 17 00:00:00 2001 From: cahrens Date: Thu, 14 Mar 2013 13:37:22 -0400 Subject: [PATCH 027/241] 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 ac25b02ce6842babfc1ed1107f49778e8d876f6b Mon Sep 17 00:00:00 2001 From: cahrens Date: Thu, 14 Mar 2013 13:48:17 -0400 Subject: [PATCH 028/241] 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 029/241] 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 + +
    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 098/241] 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 099/241] 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 100/241] 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 101/241] 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 102/241] 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 103/241] 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 104/241] 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 105/241] 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 106/241] 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 107/241] 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 108/241] 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 109/241] 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 110/241] 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 111/241] 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 112/241] 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 113/241] 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 114/241] 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 115/241] 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 116/241] 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 117/241] 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 118/241] 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 119/241] 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 120/241] 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 121/241] 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 122/241] 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 123/241] 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 124/241] 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 125/241] 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 126/241] 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 127/241] 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 128/241] 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 129/241] 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 130/241] 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 182/241] 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 183/241] 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 184/241] 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 185/241] 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 186/241] 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 187/241] 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 188/241] 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 189/241] 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 69c95ca785b60da37ea5e3aadadf177bef1f4c01 Mon Sep 17 00:00:00 2001 From: cahrens Date: Tue, 26 Mar 2013 09:51:24 -0400 Subject: [PATCH 190/241] 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 191/241] 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 192/241] 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 193/241] 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 194/241] 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 195/241] 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 24301d2a0761510143f7bc62bc9d7d0d01abd5ca Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 22 Mar 2013 11:30:31 -0400 Subject: [PATCH 196/241] 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 197/241] 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 198/241] 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 199/241] 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 200/241] 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 201/241] 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 202/241] 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 203/241] 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 204/241] 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 205/241] 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 206/241] 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 207/241] 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 208/241] 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 209/241] 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 210/241] 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 211/241] 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 6298a4ceabe221adba40acfda523d17c3f172355 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Tue, 26 Mar 2013 13:05:09 -0400 Subject: [PATCH 212/241] Fixed lettuce tests in cms that were broken in the last rebase --- .../contentstore/features/checklists.py | 28 +++++++++++-------- .../contentstore/features/course-settings.py | 26 +++++++++-------- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/cms/djangoapps/contentstore/features/checklists.py b/cms/djangoapps/contentstore/features/checklists.py index 9ef66c8096..dc399f5fac 100644 --- a/cms/djangoapps/contentstore/features/checklists.py +++ b/cms/djangoapps/contentstore/features/checklists.py @@ -1,15 +1,19 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step -from common import * +from nose.tools import assert_true, assert_equal from terrain.steps import reload_the_page +from selenium.common.exceptions import StaleElementReferenceException ############### ACTIONS #################### @step('I select Checklists from the Tools menu$') def i_select_checklists(step): expand_icon_css = 'li.nav-course-tools 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-tools-checklists a' - css_click(link_css) + world.css_click(link_css) @step('I have opened Checklists$') @@ -20,7 +24,7 @@ def i_have_opened_checklists(step): @step('I see the four default edX checklists$') def i_see_default_checklists(step): - checklists = css_find('.checklist-title') + checklists = world.css_find('.checklist-title') assert_equal(4, len(checklists)) assert_true(checklists[0].text.endswith('Getting Started With Studio')) assert_true(checklists[1].text.endswith('Draft a Rough Course Outline')) @@ -58,7 +62,7 @@ def i_select_a_link_to_the_course_outline(step): @step('I am brought to the course outline page$') def i_am_brought_to_course_outline(step): - assert_equal('Course Outline', css_find('.outline .title-1')[0].text) + assert_equal('Course Outline', world.css_find('.outline .title-1')[0].text) assert_equal(1, len(world.browser.windows)) @@ -90,30 +94,30 @@ def i_am_brought_to_help_page_in_new_window(step): def verifyChecklist2Status(completed, total, percentage): def verify_count(driver): try: - statusCount = css_find('#course-checklist1 .status-count').first + statusCount = world.css_find('#course-checklist1 .status-count').first return statusCount.text == str(completed) except StaleElementReferenceException: return False - wait_for(verify_count) - assert_equal(str(total), css_find('#course-checklist1 .status-amount').first.text) + world.wait_for(verify_count) + assert_equal(str(total), world.css_find('#course-checklist1 .status-amount').first.text) # Would like to check the CSS width, but not sure how to do that. - assert_equal(str(percentage), css_find('#course-checklist1 .viz-checklist-status-value .int').first.text) + assert_equal(str(percentage), world.css_find('#course-checklist1 .viz-checklist-status-value .int').first.text) def toggleTask(checklist, task): - css_click('#course-checklist' + str(checklist) +'-task' + str(task)) + world.css_click('#course-checklist' + str(checklist) +'-task' + str(task)) def clickActionLink(checklist, task, actionText): # toggle checklist item to make sure that the link button is showing toggleTask(checklist, task) - action_link = css_find('#course-checklist' + str(checklist) + ' a')[task] + action_link = world.css_find('#course-checklist' + str(checklist) + ' a')[task] # text will be empty initially, wait for it to populate def verify_action_link_text(driver): return action_link.text == actionText - wait_for(verify_action_link_text) + world.wait_for(verify_action_link_text) action_link.click() diff --git a/cms/djangoapps/contentstore/features/course-settings.py b/cms/djangoapps/contentstore/features/course-settings.py index a0c25045f2..9eb5b0951d 100644 --- a/cms/djangoapps/contentstore/features/course-settings.py +++ b/cms/djangoapps/contentstore/features/course-settings.py @@ -1,5 +1,7 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step -from common import * from terrain.steps import reload_the_page from selenium.webdriver.common.keys import Keys import time @@ -25,9 +27,9 @@ DEFAULT_TIME = "12:00am" 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) + world.css_click(expand_icon_css) link_css = 'li.nav-course-settings-schedule a' - css_click(link_css) + world.css_click(link_css) @step('I have set course dates$') @@ -97,9 +99,9 @@ def test_i_clear_the_course_start_date(step): @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')) + assert_true(world.css_has_text('.message-error', 'The course must have an assigned start date.')) + assert_true('error' in world.css_find(COURSE_START_DATE_CSS).first._element.get_attribute('class')) + assert_true('error' in world.css_find(COURSE_START_TIME_CSS).first._element.get_attribute('class')) @step('The previously set start date is shown on refresh$') @@ -124,9 +126,9 @@ def test_i_have_entered_a_new_course_start_date(step): @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')) + assert_equal(0, len(world.css_find('.message-error'))) + assert_false('error' in world.css_find(COURSE_START_DATE_CSS).first._element.get_attribute('class')) + assert_false('error' in world.css_find(COURSE_START_TIME_CSS).first._element.get_attribute('class')) @step('My new course start date is shown on refresh$') @@ -142,8 +144,8 @@ 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 + world.css_fill(css, date_or_time) + e = world.css_find(css).first # hit Enter to apply the changes e._element.send_keys(Keys.ENTER) @@ -152,7 +154,7 @@ def verify_date_or_time(css, date_or_time): """ Verifies date or time field. """ - assert_equal(date_or_time, css_find(css).first.value) + assert_equal(date_or_time, world.css_find(css).first.value) def pause(): From 0612b0eb94c4514b4bb6c9f48abc642ff93e63ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Tue, 26 Mar 2013 13:13:55 -0400 Subject: [PATCH 213/241] Add tests to advertised_start for partially parsable dates --- .../lib/xmodule/xmodule/tests/test_course_module.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/common/lib/xmodule/xmodule/tests/test_course_module.py b/common/lib/xmodule/xmodule/tests/test_course_module.py index 28095979ec..e1de8a1ed0 100644 --- a/common/lib/xmodule/xmodule/tests/test_course_module.py +++ b/common/lib/xmodule/xmodule/tests/test_course_module.py @@ -89,18 +89,19 @@ class IsNewCourseTestCase(unittest.TestCase): ((day2, None, None), (day1, None, None), self.assertLess), ((day1, None, None), (day1, None, None), self.assertEqual), - # Non-parseable advertised starts are ignored in preference - # to actual starts - ((day2, None, "Spring 2013"), (day1, None, "Fall 2012"), self.assertLess), - ((day1, None, "Spring 2013"), (day1, None, "Fall 2012"), self.assertEqual), + # Non-parseable advertised starts are ignored in preference to actual starts + ((day2, None, "Spring"), (day1, None, "Fall"), self.assertLess), + ((day1, None, "Spring"), (day1, None, "Fall"), self.assertEqual), + + # Partially parsable advertised starts should take priority over start dates + ((day2, None, "October 2013"), (day2, None, "October 2012"), self.assertLess), + ((day2, None, "October 2013"), (day1, None, "October 2013"), self.assertEqual), # Parseable advertised starts take priority over start dates ((day1, None, day2), (day1, None, day1), self.assertLess), ((day2, None, day2), (day1, None, day2), self.assertEqual), - ] - data = [] for a, b, assertion in dates: a_score = self.get_dummy_course(start=a[0], announcement=a[1], advertised_start=a[2]).sorting_score b_score = self.get_dummy_course(start=b[0], announcement=b[1], advertised_start=b[2]).sorting_score From 967cf7e6f36e98b1db7c8560e535c25c1327f476 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Tue, 26 Mar 2013 14:43:41 -0400 Subject: [PATCH 214/241] Fix a problem where trying to show image response answers was causing 500 errors. Add test to verify that this won't happen again. --- common/lib/capa/capa/responsetypes.py | 42 ++++++++++++++++--- .../lib/capa/capa/tests/test_responsetypes.py | 11 +++++ 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 8ab716735c..2035c42661 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1961,9 +1961,10 @@ class ImageResponse(LoncapaResponse): self.ielements = self.inputfields self.answer_ids = [ie.get('id') for ie in self.ielements] + def get_score(self, student_answers): correct_map = CorrectMap() - expectedset = self.get_answers() + expectedset = self.get_mapped_answers() for aid in self.answer_ids: # loop through IDs of # fields in our stanza given = student_answers[ @@ -2018,11 +2019,42 @@ class ImageResponse(LoncapaResponse): break return correct_map - def get_answers(self): - return ( + def get_mapped_answers(self): + ''' + Returns the internal representation of the answers + + Input: + None + Returns: + tuple (dict, dict) - + rectangles (dict) - a map of inputs to the defined rectangle for that input + regions (dict) - a map of inputs to the defined region for that input + ''' + answers = ( dict([(ie.get('id'), ie.get( 'rectangle')) for ie in self.ielements]), dict([(ie.get('id'), ie.get('regions')) for ie in self.ielements])) + return answers + + def get_answers(self): + ''' + Returns the external representation of the answers + + Input: + None + Returns: + dict (str, (str, str)) - a map of inputs to a tuple of their rectange + and their regions + ''' + answers = {} + for ie in self.ielements: + ie_id = ie.get('id') + answers[ie_id] = (ie.get('rectangle'), ie.get('regions')) + + return answers + + + #----------------------------------------------------------------------------- @@ -2087,8 +2119,8 @@ class AnnotationResponse(LoncapaResponse): correct_option = self._find_option_with_choice( inputfield, 'correct') if correct_option is not None: - answer_map[inputfield.get( - 'id')] = correct_option.get('description') + input_id = inputfield.get('id') + answer_map[input_id] = correct_option.get('description') return answer_map def _get_max_points(self): diff --git a/common/lib/capa/capa/tests/test_responsetypes.py b/common/lib/capa/capa/tests/test_responsetypes.py index 0c007f83b2..e009c26aef 100644 --- a/common/lib/capa/capa/tests/test_responsetypes.py +++ b/common/lib/capa/capa/tests/test_responsetypes.py @@ -36,6 +36,10 @@ class ResponseTest(unittest.TestCase): correct_map = problem.grade_answers(input_dict) self.assertEquals(correct_map.get_correctness('1_2_1'), expected_correctness) + def assert_answer_format(self, problem): + answers = problem.get_question_answers() + self.assertTrue(answers['1_2_1'] is not None) + def assert_multiple_grade(self, problem, correct_answers, incorrect_answers): for input_str in correct_answers: result = problem.grade_answers({'1_2_1': input_str}).get_correctness('1_2_1') @@ -166,6 +170,13 @@ class ImageResponseTest(ResponseTest): incorrect_inputs = ["[0,0]", "[600,300]"] self.assert_multiple_grade(problem, correct_inputs, incorrect_inputs) + def test_show_answer(self): + rectangle_str = "(100,100)-(200,200)" + region_str = "[[10,10], [20,10], [20, 30]]" + + problem = self.build_problem(regions=region_str, rectangle=rectangle_str) + self.assert_answer_format(problem) + class SymbolicResponseTest(unittest.TestCase): def test_sr_grade(self): From 84f2cc8af6713b3d41efc545a7fd42a5a80e3b92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Tue, 26 Mar 2013 15:08:22 -0400 Subject: [PATCH 215/241] Display advertised date correctly if it is an ISO date --- common/lib/xmodule/xmodule/course_module.py | 11 ++++++++++- .../xmodule/xmodule/tests/test_course_module.py | 16 ++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 7999f8d6da..6f3b8e94c9 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -635,8 +635,17 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): @property def start_date_text(self): + def try_parse_iso_8601(text): + try: + result = datetime.strptime(text, "%Y-%m-%dT%H:%M") + result = result.strftime("%b %d, %Y") + except ValueError: + result = text.title() + + return result + if isinstance(self.advertised_start, basestring): - return self.advertised_start + return try_parse_iso_8601(self.advertised_start) elif self.advertised_start is None and self.start is None: return 'TBD' else: diff --git a/common/lib/xmodule/xmodule/tests/test_course_module.py b/common/lib/xmodule/xmodule/tests/test_course_module.py index e1de8a1ed0..eda9cf386c 100644 --- a/common/lib/xmodule/xmodule/tests/test_course_module.py +++ b/common/lib/xmodule/xmodule/tests/test_course_module.py @@ -1,5 +1,6 @@ import unittest from time import strptime + from fs.memoryfs import MemoryFS from mock import Mock, patch @@ -108,7 +109,22 @@ class IsNewCourseTestCase(unittest.TestCase): print "Comparing %s to %s" % (a, b) assertion(a_score, b_score) + @patch('xmodule.course_module.time.gmtime') + def test_start_date_text(self, gmtime_mock): + gmtime_mock.return_value = NOW + settings = [ + # start, advertized, result + ('2012-12-02T12:00', None, 'Dec 02, 2012'), + ('2012-12-02T12:00', '2011-11-01T12:00', 'Nov 01, 2011'), + ('2012-12-02T12:00', 'Spring 2012', 'Spring 2012'), + ('2012-12-02T12:00', 'November, 2011', 'November, 2011'), + ] + + for s in settings: + d = self.get_dummy_course(start=s[0], advertised_start=s[1]) + print "Checking start=%s advertised=%s" % (s[0], s[1]) + self.assertEqual(d.start_date_text, s[2]) @patch('xmodule.course_module.time.gmtime') def test_is_newish(self, gmtime_mock): From 90553a1b1d842600588fe0ecab9402b15c11de3c Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 26 Mar 2013 16:19:34 -0400 Subject: [PATCH 216/241] Use get_many and set_many to cut down on the number of metadata trees to retrieve, and only retrieve them once per call to _load_items --- .../lib/xmodule/xmodule/modulestore/mongo.py | 73 +++++++++++-------- .../xmodule/modulestore/tests/test_mongo.py | 58 ++++++++++++++- 2 files changed, 100 insertions(+), 31 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py index b76251bb99..27a5ffbc26 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -9,6 +9,7 @@ from fs.osfs import OSFS from itertools import repeat from path import path from datetime import datetime +from operator import attrgetter from importlib import import_module from xmodule.errortracker import null_error_tracker, exc_info_to_str @@ -107,7 +108,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem): references to metadata_inheritance_tree """ def __init__(self, modulestore, module_data, default_class, resources_fs, - error_tracker, render_template, metadata_inheritance_tree = None): + error_tracker, render_template, metadata_cache = None): """ modulestore: the module store that can be used to retrieve additional modules @@ -132,7 +133,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem): # cdodge: other Systems have a course_id attribute defined. To keep things consistent, let's # define an attribute here as well, even though it's None self.course_id = None - self.metadata_inheritance_tree = metadata_inheritance_tree + self.metadata_cache = metadata_cache def load_item(self, location): location = Location(location) @@ -165,8 +166,8 @@ class CachingDescriptorSystem(MakoDescriptorSystem): model_data = DbModel(kvs, class_, None, MongoUsage(self.course_id, location)) module = class_(self, location, model_data) - if self.metadata_inheritance_tree is not None: - metadata_to_inherit = self.metadata_inheritance_tree.get('parent_metadata', {}).get(location.url(), {}) + if self.metadata_cache is not None: + metadata_to_inherit = self.metadata_cache.get(metadata_cache_key(location), {}).get('parent_metadata', {}).get(location.url(), {}) inherit_metadata(module, metadata_to_inherit) return module except: @@ -206,6 +207,9 @@ def namedtuple_to_son(namedtuple, prefix=''): return son +metadata_cache_key = attrgetter('org', 'course') + + class MongoModuleStore(ModuleStoreBase): """ A Mongodb backed ModuleStore @@ -278,6 +282,7 @@ class MongoModuleStore(ModuleStoreBase): # now traverse the tree and compute down the inherited metadata metadata_to_inherit = {} + def _compute_inherited_metadata(url): my_metadata = {} # check for presence of metadata key. Note that a given module may not yet be fully formed. @@ -293,7 +298,7 @@ class MongoModuleStore(ModuleStoreBase): # go through all the children and recurse, but only if we have # in the result set. Remember results will not contain leaf nodes - for child in results_by_url[url].get('definition',{}).get('children',[]): + for child in results_by_url[url].get('definition', {}).get('children', []): if child in results_by_url: new_child_metadata = copy.deepcopy(my_metadata) new_child_metadata.update(results_by_url[child].get('metadata', {})) @@ -304,42 +309,42 @@ class MongoModuleStore(ModuleStoreBase): # this is likely a leaf node, so let's record what metadata we need to inherit metadata_to_inherit[child] = my_metadata - if root is not None: _compute_inherited_metadata(root) return {'parent_metadata': metadata_to_inherit, - 'timestamp' : datetime.now()} + 'timestamp': datetime.now()} - def get_cached_metadata_inheritance_tree(self, location, force_refresh=False): + def get_cached_metadata_inheritance_trees(self, locations, force_refresh=False): ''' TODO (cdodge) This method can be deleted when the 'split module store' work has been completed ''' - key_name = '{0}/{1}'.format(location.org, location.course) - tree = None - if self.metadata_inheritance_cache is not None: - tree = self.metadata_inheritance_cache.get(key_name) + trees = {} + if locations and self.metadata_inheritance_cache is not None and not force_refresh: + trees = self.metadata_inheritance_cache.get_many(list(set([metadata_cache_key(loc) for loc in locations]))) else: # This is to help guard against an accident prod runtime without a cache logging.warning('Running MongoModuleStore without metadata_inheritance_cache. This should not happen in production!') - if tree is None or force_refresh: - tree = self.get_metadata_inheritance_tree(location) - if self.metadata_inheritance_cache is not None: - self.metadata_inheritance_cache.set(key_name, tree) + to_cache = {} + for loc in locations: + if metadata_cache_key(loc) not in trees: + to_cache[metadata_cache_key(loc)] = trees[metadata_cache_key(loc)] = self.get_metadata_inheritance_tree(loc) - return tree + if to_cache and self.metadata_inheritance_cache is not None: + self.metadata_inheritance_cache.set_many(to_cache) + + return trees def refresh_cached_metadata_inheritance_tree(self, location): pseudo_course_id = '/'.join([location.org, location.course]) if pseudo_course_id not in self.ignore_write_events_on_courses: - self.get_cached_metadata_inheritance_tree(location, force_refresh = True) + self.get_cached_metadata_inheritance_trees([location], force_refresh=True) def clear_cached_metadata_inheritance_tree(self, location): - key_name = '{0}/{1}'.format(location.org, location.course) if self.metadata_inheritance_cache is not None: - self.metadata_inheritance_cache.delete(key_name) + self.metadata_inheritance_cache.delete(metadata_cache_key(location)) def _clean_item_data(self, item): """ @@ -385,7 +390,18 @@ class MongoModuleStore(ModuleStoreBase): return data - def _load_item(self, item, data_cache, should_apply_metadata_inheritence=True): + def _cache_metadata_inheritance(self, items, depth, force_refresh=False): + """ + Retrieves all course metadata inheritance trees needed to load items + """ + + locations = [ + Location(item['location']) for item in items + if not (item['location']['category'] == 'course' and depth == 0) + ] + return self.get_cached_metadata_inheritance_trees(locations, force_refresh=force_refresh) + + def _load_item(self, item, data_cache, metadata_cache): """ Load an XModuleDescriptor from item, using the children stored in data_cache """ @@ -399,9 +415,6 @@ class MongoModuleStore(ModuleStoreBase): metadata_inheritance_tree = None - 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 # the 'metadata_inheritance_tree' parameter system = CachingDescriptorSystem( @@ -411,7 +424,7 @@ class MongoModuleStore(ModuleStoreBase): resource_fs, self.error_tracker, self.render_template, - metadata_inheritance_tree = metadata_inheritance_tree + metadata_cache, ) return system.load_item(item['location']) @@ -421,11 +434,11 @@ class MongoModuleStore(ModuleStoreBase): to specified depth """ data_cache = self._cache_children(items, depth) + inheritance_cache = self._cache_metadata_inheritance(items, depth) # 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] + # bother with the metadata inheritence + return [self._load_item(item, data_cache, inheritance_cache) for item in items] def get_courses(self): ''' @@ -631,7 +644,7 @@ class MongoModuleStore(ModuleStoreBase): self._update_single_item(location, {'metadata': metadata}) # recompute (and update) the metadata inheritance tree which is cached - self.refresh_cached_metadata_inheritance_tree(loc) + self.refresh_cached_metadata_inheritance_tree(loc) def delete_item(self, location): """ @@ -654,7 +667,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 - self.refresh_cached_metadata_inheritance_tree(Location(location)) + self.refresh_cached_metadata_inheritance_tree(Location(location)) def get_parent_locations(self, location, course_id): '''Find all locations that are the parents of this location in this diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py index 6f6f47ba85..3e29c07ea4 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py @@ -1,6 +1,7 @@ import pymongo -from nose.tools import assert_equals, assert_raises, assert_not_equals, with_setup +from mock import Mock +from nose.tools import assert_equals, assert_raises, assert_not_equals, with_setup, assert_false from pprint import pprint from xmodule.modulestore import Location @@ -102,3 +103,58 @@ class TestMongoModuleStore(object): def test_path_to_location(self): '''Make sure that path_to_location works''' check_path_to_location(self.store) + + def test_metadata_inheritance_query_count(self): + ''' + When retrieving items from mongo, we should only query the cache a number of times + equal to the number of courses being retrieved from. + + We should also not query + ''' + self.store.metadata_inheritance_cache = Mock() + get_many = self.store.metadata_inheritance_cache.get_many + set_many = self.store.metadata_inheritance_cache.set_many + get_many.return_value = {('edX', 'toy'): {}} + + self.store.get_item(Location("i4x://edX/toy/course/2012_Fall"), depth=0) + assert_false(get_many.called) + assert_false(set_many.called) + get_many.reset_mock() + + self.store.get_item(Location("i4x://edX/toy/course/2012_Fall"), depth=3) + get_many.assert_called_with([('edX', 'toy')]) + assert_equals(0, set_many.call_count) + get_many.reset_mock() + + self.store.get_items(Location('i4x', 'edX', None, 'course', None), depth=0) + assert_false(get_many.called) + assert_false(set_many.called) + get_many.reset_mock() + + self.store.get_items(Location('i4x', 'edX', None, 'course', None), depth=3) + assert_equals(1, get_many.call_count) + assert_equals([('edX', 'simple'), ('edX', 'toy')], sorted(get_many.call_args[0][0])) + assert_equals(1, set_many.call_count) + assert_equals([('edX', 'simple')], sorted(set_many.call_args[0][0].keys())) + get_many.reset_mock() + + self.store.get_items(Location('i4x', 'edX', None, None, None), depth=0) + assert_equals(1, get_many.call_count) + assert_equals([('edX', 'simple'), ('edX', 'toy')], sorted(get_many.call_args[0][0])) + assert_equals(1, set_many.call_count) + assert_equals([('edX', 'simple')], sorted(set_many.call_args[0][0].keys())) + get_many.reset_mock() + + def test_metadata_inheritance_query_count_forced_refresh(self): + self.store.metadata_inheritance_cache = Mock() + get_many = self.store.metadata_inheritance_cache.get_many + set_many = self.store.metadata_inheritance_cache.set_many + get_many.return_value = {('edX', 'toy'): {}} + + self.store.get_cached_metadata_inheritance_trees( + [Location("i4x://edX/toy/course/2012_Fall"), Location("i4x://edX/simple/course/2012_Fall")], + True + ) + assert_false(get_many.called) + assert_equals(1, set_many.call_count) + assert_equals([('edX', 'simple'), ('edX', 'toy')], sorted(set_many.call_args[0][0].keys())) From 1f11508ac6b4057a137d35a11835db4f6d231588 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 26 Mar 2013 16:30:55 -0400 Subject: [PATCH 217/241] Pylint cleanup --- .../lib/xmodule/xmodule/modulestore/mongo.py | 44 +++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py index 27a5ffbc26..47049b9fd6 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -97,6 +97,7 @@ class MongoKeyValueStore(KeyValueStore): else: return False + MongoUsage = namedtuple('MongoUsage', 'id, def_id') @@ -108,7 +109,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem): references to metadata_inheritance_tree """ def __init__(self, modulestore, module_data, default_class, resources_fs, - error_tracker, render_template, metadata_cache = None): + error_tracker, render_template, metadata_cache=None): """ modulestore: the module store that can be used to retrieve additional modules @@ -136,6 +137,9 @@ class CachingDescriptorSystem(MakoDescriptorSystem): self.metadata_cache = metadata_cache def load_item(self, location): + """ + Return an XModule instance for the specified location + """ location = Location(location) json_data = self.module_data.get(location) if json_data is None: @@ -197,12 +201,12 @@ def location_to_query(location, wildcard=True): return query -def namedtuple_to_son(namedtuple, prefix=''): +def namedtuple_to_son(ntuple, prefix=''): """ Converts a namedtuple into a SON object with the same key order """ son = SON() - for idx, field_name in enumerate(namedtuple._fields): + for idx, field_name in enumerate(ntuple._fields): son[prefix + field_name] = namedtuple[idx] return son @@ -232,7 +236,6 @@ class MongoModuleStore(ModuleStoreBase): if user is not None and password is not None: self.collection.database.authenticate(user, password) - # Force mongo to report errors, at the expense of performance self.collection.safe = True @@ -262,7 +265,7 @@ class MongoModuleStore(ModuleStoreBase): query = { '_id.org': location.org, '_id.course': location.course, - '_id.category': {'$in': [ 'course', 'chapter', 'sequential', 'vertical']} + '_id.category': {'$in': ['course', 'chapter', 'sequential', 'vertical']} } # we just want the Location, children, and metadata record_filter = {'_id': 1, 'definition.children': 1, 'metadata': 1} @@ -284,6 +287,9 @@ class MongoModuleStore(ModuleStoreBase): metadata_to_inherit = {} def _compute_inherited_metadata(url): + """ + Helper method for computing inherited metadata for a specific location url + """ my_metadata = {} # check for presence of metadata key. Note that a given module may not yet be fully formed. # example: update_item -> update_children -> update_metadata sequence on new item create @@ -325,12 +331,14 @@ class MongoModuleStore(ModuleStoreBase): trees = self.metadata_inheritance_cache.get_many(list(set([metadata_cache_key(loc) for loc in locations]))) else: # This is to help guard against an accident prod runtime without a cache - logging.warning('Running MongoModuleStore without metadata_inheritance_cache. This should not happen in production!') + logging.warning('Running MongoModuleStore without metadata_inheritance_cache. ' + 'This should not happen in production!') to_cache = {} for loc in locations: - if metadata_cache_key(loc) not in trees: - to_cache[metadata_cache_key(loc)] = trees[metadata_cache_key(loc)] = self.get_metadata_inheritance_tree(loc) + cache_key = metadata_cache_key(loc) + if cache_key not in trees: + to_cache[cache_key] = trees[cache_key] = self.get_metadata_inheritance_tree(loc) if to_cache and self.metadata_inheritance_cache is not None: self.metadata_inheritance_cache.set_many(to_cache) @@ -338,11 +346,19 @@ class MongoModuleStore(ModuleStoreBase): return trees def refresh_cached_metadata_inheritance_tree(self, location): + """ + Refresh the cached metadata inheritance tree for the org/course combination + for location + """ pseudo_course_id = '/'.join([location.org, location.course]) if pseudo_course_id not in self.ignore_write_events_on_courses: - self.get_cached_metadata_inheritance_trees([location], force_refresh=True) + self.get_cached_metadata_inheritance_trees([location], force_refresh=True) def clear_cached_metadata_inheritance_tree(self, location): + """ + Delete the cached metadata inheritance tree for the org/course combination + for location + """ if self.metadata_inheritance_cache is not None: self.metadata_inheritance_cache.delete(metadata_cache_key(location)) @@ -372,7 +388,7 @@ class MongoModuleStore(ModuleStoreBase): data[Location(item['location'])] = item if depth == 0: - break; + break # Load all children by id. See # http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%24or @@ -413,8 +429,6 @@ class MongoModuleStore(ModuleStoreBase): resource_fs = OSFS(root) - metadata_inheritance_tree = None - # TODO (cdodge): When the 'split module store' work has been completed, we should remove # the 'metadata_inheritance_tree' parameter system = CachingDescriptorSystem( @@ -572,7 +586,8 @@ class MongoModuleStore(ModuleStoreBase): raise Exception('Could not find course at {0}'.format(course_search_location)) if found_cnt > 1: - raise Exception('Found more than one course at {0}. There should only be one!!! Dump = {1}'.format(course_search_location, courses)) + raise Exception('Found more than one course at {0}. There should only be one!!! ' + 'Dump = {1}'.format(course_search_location, courses)) return courses[0] @@ -688,4 +703,7 @@ class MongoModuleStore(ModuleStoreBase): # DraftModuleStore is first, because it needs to intercept calls to MongoModuleStore class DraftMongoModuleStore(DraftModuleStore, MongoModuleStore): + """ + Version of MongoModuleStore with draft capability mixed in + """ pass From e0343342b0d20f87818ec74535a0f96e8e91c70c Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 26 Mar 2013 16:36:58 -0400 Subject: [PATCH 218/241] Fix typo during pylint fixes --- common/lib/xmodule/xmodule/modulestore/mongo.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py index 47049b9fd6..fdc34913ee 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -207,7 +207,7 @@ def namedtuple_to_son(ntuple, prefix=''): """ son = SON() for idx, field_name in enumerate(ntuple._fields): - son[prefix + field_name] = namedtuple[idx] + son[prefix + field_name] = ntuple[idx] return son @@ -703,6 +703,9 @@ class MongoModuleStore(ModuleStoreBase): # DraftModuleStore is first, because it needs to intercept calls to MongoModuleStore class DraftMongoModuleStore(DraftModuleStore, MongoModuleStore): + """ + Version of MongoModuleStore with draft capability mixed in + """ """ Version of MongoModuleStore with draft capability mixed in """ From b975d4d90ce1a5e3fc79ab18c6eec96b2052bea3 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 26 Mar 2013 16:43:58 -0400 Subject: [PATCH 219/241] Fix tests --- cms/djangoapps/contentstore/tests/test_contentstore.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index edb20561bc..e6b5933d66 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -211,7 +211,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']) @@ -328,11 +328,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): 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', + 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', + self.assertFalse(Location(['i4x', 'edX', 'full', 'vertical', 'vertical_58', None]) in course.system.module_data) def test_export_course_with_unknown_metadata(self): @@ -556,7 +556,7 @@ class ContentStoreTest(ModuleStoreTestCase): module_store.update_children(parent.location, parent.children + [new_component_location.url()]) # flush the cache - module_store.get_cached_metadata_inheritance_tree(new_component_location, -1) + module_store.refresh_cached_metadata_inheritance_tree(new_component_location) new_module = module_store.get_item(new_component_location) # check for grace period definition which should be defined at the course level @@ -571,7 +571,7 @@ class ContentStoreTest(ModuleStoreTestCase): module_store.update_metadata(new_module.location, own_metadata(new_module)) # flush the cache and refetch - module_store.get_cached_metadata_inheritance_tree(new_component_location, -1) + module_store.refresh_cached_metadata_inheritance_tree(new_component_location) new_module = module_store.get_item(new_component_location) self.assertEqual(timedelta(1), new_module.lms.graceperiod) From a44ecdfcd6e01ea6f38b82f6c9348a955808c87f Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Tue, 26 Mar 2013 16:45:47 -0400 Subject: [PATCH 220/241] if we parse an invalid location in the content store middleware, then return a 404, not a 500 --- common/djangoapps/contentserver/middleware.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/common/djangoapps/contentserver/middleware.py b/common/djangoapps/contentserver/middleware.py index c5e887801e..e8674a1e9e 100644 --- a/common/djangoapps/contentserver/middleware.py +++ b/common/djangoapps/contentserver/middleware.py @@ -5,6 +5,7 @@ from django.http import HttpResponse, Http404, HttpResponseNotModified from xmodule.contentstore.django import contentstore from xmodule.contentstore.content import StaticContent, XASSET_LOCATION_TAG +from xmodule.modulestore import InvalidLocationError from cache_toolbox.core import get_cached_content, set_cached_content from xmodule.exceptions import NotFoundError @@ -13,7 +14,13 @@ class StaticContentServer(object): def process_request(self, request): # look to see if the request is prefixed with 'c4x' tag if request.path.startswith('/' + XASSET_LOCATION_TAG + '/'): - loc = StaticContent.get_location_from_path(request.path) + try: + loc = StaticContent.get_location_from_path(request.path) + except InvalidLocationError: + response = HttpResponse() + response.status_code = 404 + return response + # first look in our cache so we don't have to round-trip to the DB content = get_cached_content(loc) if content is None: From b0e2c82ad3619bea30674562e347cd76b9856de4 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Tue, 26 Mar 2013 20:02:29 -0400 Subject: [PATCH 221/241] actually.. return a 400 rather than a 404 because the request is malformed. Also add unit test. --- cms/djangoapps/contentstore/tests/test_contentstore.py | 4 ++++ common/djangoapps/contentserver/middleware.py | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index edb20561bc..a8cde71379 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -213,6 +213,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): resp = self.client.get(reverse('edit_unit', kwargs={'location': new_loc.url()})) self.assertEqual(resp.status_code, 200) + def test_bad_contentstore_request(self): + resp = self.client.get('http://localhost:8001/c4x/CDX/123123/asset/&images_circuits_Lab7Solution2.png') + self.assertEqual(resp.status_code, 400) + def test_delete_course(self): import_from_xml(modulestore(), 'common/test/data/', ['full']) diff --git a/common/djangoapps/contentserver/middleware.py b/common/djangoapps/contentserver/middleware.py index e8674a1e9e..8e9e70046d 100644 --- a/common/djangoapps/contentserver/middleware.py +++ b/common/djangoapps/contentserver/middleware.py @@ -17,8 +17,9 @@ class StaticContentServer(object): try: loc = StaticContent.get_location_from_path(request.path) except InvalidLocationError: + # return a 'Bad Request' to browser as we have a malformed Location response = HttpResponse() - response.status_code = 404 + response.status_code = 400 return response # first look in our cache so we don't have to round-trip to the DB From 195fd2d1fe3b591e8bd5380707272a170a3b000d Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Tue, 26 Mar 2013 23:48:06 -0400 Subject: [PATCH 222/241] optimize the result-set that gets returned from Mongo on metadata inheritence. We just need the fields which are actually inheritable, so no need to return anything else as it gets filtered out during the computation --- common/lib/xmodule/xmodule/modulestore/mongo.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py index fdc34913ee..38b15ab76e 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -267,8 +267,13 @@ class MongoModuleStore(ModuleStoreBase): '_id.course': location.course, '_id.category': {'$in': ['course', 'chapter', 'sequential', 'vertical']} } - # we just want the Location, children, and metadata - record_filter = {'_id': 1, 'definition.children': 1, 'metadata': 1} + # we just want the Location, children, and inheritable metadata + record_filter = {'_id': 1, 'definition.children': 1} + + # just get the inheritable metadata since that is all we need for the computation + # this minimizes both data pushed over the wire + for attr in INHERITABLE_METADATA: + record_filter['metadata.{0}'.format(attr)] = 1 # call out to the DB resultset = self.collection.find(query, record_filter) From 2120481738489d872db41916b75b0336a77a3a9e Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 27 Mar 2013 01:34:25 -0400 Subject: [PATCH 223/241] 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 2c0e5b82ff2535770a5ca605aa1b1bd521c756d4 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 27 Mar 2013 07:29:22 -0400 Subject: [PATCH 224/241] 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 225/241] 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 7101c76016e4c42b18ba5858556b836da6fde66b Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 27 Mar 2013 12:02:32 -0400 Subject: [PATCH 226/241] 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 15ea32b095abe4d033075640121ad418ced0179d Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 27 Mar 2013 12:53:58 -0400 Subject: [PATCH 227/241] 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 228/241] 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 229/241] 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 22537ffd3b05269b688972e7e2ad81e118cc1da7 Mon Sep 17 00:00:00 2001 From: cahrens Date: Wed, 27 Mar 2013 14:51:39 -0400 Subject: [PATCH 230/241] 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 231/241] 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 232/241] 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 6564cc57e6f7a2bddfab8a9dabbcc012687135a1 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Wed, 27 Mar 2013 16:29:55 -0400 Subject: [PATCH 233/241] 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 99778b1b5bc7dcf6d9cc29865a85b8d8bc0e2107 Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Wed, 27 Mar 2013 17:59:56 -0400 Subject: [PATCH 234/241] add fixes for 500 bugs (lighthouse 287 and 293) by requiring login for accessing licenses and inline discussions. Add unit tests for licenses, and rearrange tests based off of (the former) PageLoad. Also cleanups for pylint and pep8. --- lms/djangoapps/course_wiki/tests/tests.py | 11 +- .../courseware/tests/test_module_render.py | 85 +++---- lms/djangoapps/courseware/tests/tests.py | 216 +++++++++--------- .../django_comment_client/forum/views.py | 44 ++-- lms/djangoapps/django_comment_client/tests.py | 71 +----- lms/djangoapps/instructor/tests.py | 26 +-- lms/djangoapps/licenses/tests.py | 136 ++++++++++- lms/djangoapps/licenses/views.py | 22 +- lms/djangoapps/open_ended_grading/tests.py | 30 +-- 9 files changed, 334 insertions(+), 307 deletions(-) diff --git a/lms/djangoapps/course_wiki/tests/tests.py b/lms/djangoapps/course_wiki/tests/tests.py index cecc4f9cf9..620cf104d7 100644 --- a/lms/djangoapps/course_wiki/tests/tests.py +++ b/lms/djangoapps/course_wiki/tests/tests.py @@ -3,13 +3,11 @@ from django.test.utils import override_settings import xmodule.modulestore.django -from courseware.tests.tests import PageLoader, TEST_DATA_XML_MODULESTORE +from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE from xmodule.modulestore.django import modulestore -from xmodule.modulestore.xml_importer import import_from_xml - @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) -class WikiRedirectTestCase(PageLoader): +class WikiRedirectTestCase(LoginEnrollmentTestCase): def setUp(self): xmodule.modulestore.django._MODULESTORES = {} courses = modulestore().get_courses() @@ -30,8 +28,6 @@ class WikiRedirectTestCase(PageLoader): self.activate_user(self.student) self.activate_user(self.instructor) - - def test_wiki_redirect(self): """ Test that requesting wiki URLs redirect properly to or out of classes. @@ -69,7 +65,6 @@ class WikiRedirectTestCase(PageLoader): self.assertEqual(resp.status_code, 302) self.assertEqual(resp['Location'], 'http://testserver' + destination) - def create_course_page(self, course): """ Test that loading the course wiki page creates the wiki page. @@ -98,7 +93,6 @@ class WikiRedirectTestCase(PageLoader): self.assertTrue("course info" in resp.content.lower()) self.assertTrue("courseware" in resp.content.lower()) - def test_course_navigator(self): """" Test that going from a course page to a wiki page contains the course navigator. @@ -108,7 +102,6 @@ class WikiRedirectTestCase(PageLoader): self.enroll(self.toy) self.create_course_page(self.toy) - course_wiki_page = reverse('wiki:get', kwargs={'path': self.toy.wiki_slug + '/'}) referer = reverse("courseware", kwargs={'course_id': self.toy.id}) diff --git a/lms/djangoapps/courseware/tests/test_module_render.py b/lms/djangoapps/courseware/tests/test_module_render.py index 3a3a7ac5ea..53f775267c 100644 --- a/lms/djangoapps/courseware/tests/test_module_render.py +++ b/lms/djangoapps/courseware/tests/test_module_render.py @@ -1,28 +1,16 @@ -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 -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 courseware.tests.tests import PageLoader -from student.models import Registration +from courseware.tests.tests import LoginEnrollmentTestCase from courseware.model_data import ModelDataCache from .factories import UserFactory @@ -49,7 +37,7 @@ TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR) @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) -class ModuleRenderTestCase(PageLoader): +class ModuleRenderTestCase(LoginEnrollmentTestCase): def setUp(self): self.location = ['i4x', 'edX', 'toy', 'chapter', 'Overview'] self._MODULESTORES = {} @@ -66,10 +54,9 @@ class ModuleRenderTestCase(PageLoader): mock_request = MagicMock() mock_request.FILES.keys.return_value = ['file_id'] mock_request.FILES.getlist.return_value = ['file'] * (settings.MAX_FILEUPLOADS_PER_INPUT + 1) - self.assertEquals(render.modx_dispatch(mock_request, 'dummy', self.location, - 'dummy').content, - json.dumps({'success': 'Submission aborted! Maximum %d files may be submitted at once' % - settings.MAX_FILEUPLOADS_PER_INPUT})) + self.assertEquals(render.modx_dispatch(mock_request, 'dummy', self.location, 'dummy').content, + json.dumps({'success': 'Submission aborted! Maximum %d files may be submitted at once' % + settings.MAX_FILEUPLOADS_PER_INPUT})) mock_request_2 = MagicMock() mock_request_2.FILES.keys.return_value = ['file_id'] inputfile = Stub() @@ -80,7 +67,7 @@ class ModuleRenderTestCase(PageLoader): self.assertEquals(render.modx_dispatch(mock_request_2, 'dummy', self.location, 'dummy').content, json.dumps({'success': 'Submission aborted! Your file "%s" is too large (max size: %d MB)' % - (inputfile.name, settings.STUDENT_FILEUPLOAD_MAX_SIZE / (1000 ** 2))})) + (inputfile.name, settings.STUDENT_FILEUPLOAD_MAX_SIZE / (1000 ** 2))})) mock_request_3 = MagicMock() mock_request_3.POST.copy.return_value = {} mock_request_3.FILES = False @@ -91,10 +78,10 @@ class ModuleRenderTestCase(PageLoader): self.assertRaises(ItemNotFoundError, render.modx_dispatch, mock_request_3, 'dummy', self.location, 'toy') self.assertRaises(Http404, render.modx_dispatch, mock_request_3, 'dummy', - self.location, self.course_id) + self.location, self.course_id) mock_request_3.POST.copy.return_value = {'position': 1} self.assertIsInstance(render.modx_dispatch(mock_request_3, 'goto_position', - self.location, self.course_id), HttpResponse) + self.location, self.course_id), HttpResponse) def test_get_score_bucket(self): self.assertEquals(render.get_score_bucket(0, 10), 'incorrect') @@ -125,19 +112,19 @@ class TestTOC(TestCase): self.toy_course.id, self.portal_user, self.toy_course, depth=2) expected = ([{'active': True, 'sections': - [{'url_name': 'Toy_Videos', 'display_name': u'Toy Videos', 'graded': True, - 'format': u'Lecture Sequence', 'due': '', 'active': False}, - {'url_name': 'Welcome', 'display_name': u'Welcome', 'graded': True, - 'format': '', 'due': '', 'active': False}, - {'url_name': 'video_123456789012', 'display_name': 'video 123456789012', 'graded': True, - 'format': '', 'due': '', 'active': False}, - {'url_name': 'video_4f66f493ac8f', 'display_name': 'video 4f66f493ac8f', 'graded': True, - 'format': '', 'due': '', 'active': False}], - 'url_name': 'Overview', 'display_name': u'Overview'}, - {'active': False, 'sections': - [{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True, - 'format': '', 'due': '', 'active': False}], - 'url_name': 'secret:magic', 'display_name': 'secret:magic'}]) + [{'url_name': 'Toy_Videos', 'display_name': u'Toy Videos', 'graded': True, + 'format': u'Lecture Sequence', 'due': '', 'active': False}, + {'url_name': 'Welcome', 'display_name': u'Welcome', 'graded': True, + 'format': '', 'due': '', 'active': False}, + {'url_name': 'video_123456789012', 'display_name': 'video 123456789012', 'graded': True, + 'format': '', 'due': '', 'active': False}, + {'url_name': 'video_4f66f493ac8f', 'display_name': 'video 4f66f493ac8f', 'graded': True, + 'format': '', 'due': '', 'active': False}], + 'url_name': 'Overview', 'display_name': u'Overview'}, + {'active': False, 'sections': + [{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True, + 'format': '', 'due': '', 'active': False}], + 'url_name': 'secret:magic', 'display_name': 'secret:magic'}]) actual = render.toc_for_course(self.portal_user, request, self.toy_course, chapter, None, model_data_cache) self.assertEqual(expected, actual) @@ -152,19 +139,19 @@ class TestTOC(TestCase): self.toy_course.id, self.portal_user, self.toy_course, depth=2) expected = ([{'active': True, 'sections': - [{'url_name': 'Toy_Videos', 'display_name': u'Toy Videos', 'graded': True, - 'format': u'Lecture Sequence', 'due': '', 'active': False}, - {'url_name': 'Welcome', 'display_name': u'Welcome', 'graded': True, - 'format': '', 'due': '', 'active': True}, - {'url_name': 'video_123456789012', 'display_name': 'video 123456789012', 'graded': True, - 'format': '', 'due': '', 'active': False}, - {'url_name': 'video_4f66f493ac8f', 'display_name': 'video 4f66f493ac8f', 'graded': True, - 'format': '', 'due': '', 'active': False}], - 'url_name': 'Overview', 'display_name': u'Overview'}, - {'active': False, 'sections': - [{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True, - 'format': '', 'due': '', 'active': False}], - 'url_name': 'secret:magic', 'display_name': 'secret:magic'}]) + [{'url_name': 'Toy_Videos', 'display_name': u'Toy Videos', 'graded': True, + 'format': u'Lecture Sequence', 'due': '', 'active': False}, + {'url_name': 'Welcome', 'display_name': u'Welcome', 'graded': True, + 'format': '', 'due': '', 'active': True}, + {'url_name': 'video_123456789012', 'display_name': 'video 123456789012', 'graded': True, + 'format': '', 'due': '', 'active': False}, + {'url_name': 'video_4f66f493ac8f', 'display_name': 'video 4f66f493ac8f', 'graded': True, + 'format': '', 'due': '', 'active': False}], + 'url_name': 'Overview', 'display_name': u'Overview'}, + {'active': False, 'sections': + [{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True, + 'format': '', 'due': '', 'active': False}], + 'url_name': 'secret:magic', 'display_name': 'secret:magic'}]) actual = render.toc_for_course(self.portal_user, request, self.toy_course, chapter, section, model_data_cache) self.assertEqual(expected, actual) diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py index cd845b1e44..9845477032 100644 --- a/lms/djangoapps/courseware/tests/tests.py +++ b/lms/djangoapps/courseware/tests/tests.py @@ -1,9 +1,6 @@ import logging -log = logging.getLogger("mitx." + __name__) - import json import time - from urlparse import urlsplit, urlunsplit from django.contrib.auth.models import User, Group @@ -29,29 +26,30 @@ from xmodule.modulestore.django import modulestore from xmodule.modulestore import Location from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.xml import XMLModuleStore -from xmodule.timeparse import stringify_time +log = logging.getLogger("mitx." + __name__) def parse_json(response): """Parse response, which is assumed to be json""" return json.loads(response.content) -def user(email): +def get_user(email): '''look up a user by email''' return User.objects.get(email=email) -def registration(email): +def get_registration(email): '''look up registration object by email''' return Registration.objects.get(user__email=email) -# A bit of a hack--want mongo modulestore for these tests, until -# jump_to works with the xmlmodulestore or we have an even better solution -# NOTE: this means this test requires mongo to be running. - def mongo_store_config(data_dir): + ''' + Defines default module store using MongoModuleStore + + Use of this config requires mongo to be running + ''' return { 'default': { 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', @@ -68,6 +66,7 @@ def mongo_store_config(data_dir): def draft_mongo_store_config(data_dir): + '''Defines default module store using DraftMongoModuleStore''' return { 'default': { 'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore', @@ -84,6 +83,7 @@ def draft_mongo_store_config(data_dir): def xml_store_config(data_dir): + '''Defines default module store using XMLModuleStore''' return { 'default': { 'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore', @@ -100,8 +100,8 @@ TEST_DATA_MONGO_MODULESTORE = mongo_store_config(TEST_DATA_DIR) TEST_DATA_DRAFT_MONGO_MODULESTORE = draft_mongo_store_config(TEST_DATA_DIR) -class ActivateLoginTestCase(TestCase): - '''Check that we can activate and log in''' +class LoginEnrollmentTestCase(TestCase): + '''Base TestCase providing support for user creation, activation, login, and course enrollment''' def assertRedirectsNoFollow(self, response, expected_url): """ @@ -117,32 +117,33 @@ class ActivateLoginTestCase(TestCase): e_scheme, e_netloc, e_path, e_query, e_fragment = urlsplit(expected_url) if not (e_scheme or e_netloc): - expected_url = urlunsplit(('http', 'testserver', e_path, - e_query, e_fragment)) + expected_url = urlunsplit(('http', 'testserver', e_path, e_query, e_fragment)) self.assertEqual(url, expected_url, "Response redirected to '{0}', expected '{1}'".format( url, expected_url)) - def setUp(self): - email = 'view@test.com' - password = 'foo' - self.create_account('viewtest', email, password) - self.activate_user(email) - self.login(email, password) + def setup_viewtest_user(self): + '''create a user account, activate, and log in''' + self.viewtest_email = 'view@test.com' + self.viewtest_password = 'foo' + self.viewtest_username = 'viewtest' + self.create_account(self.viewtest_username, self.viewtest_email, self.viewtest_password) + self.activate_user(self.viewtest_email) + self.login(self.viewtest_email, self.viewtest_password) # ============ User creation and login ============== - def _login(self, email, pw): + def _login(self, email, password): '''Login. View should always return 200. The success/fail is in the returned json''' resp = self.client.post(reverse('login'), - {'email': email, 'password': pw}) + {'email': email, 'password': password}) self.assertEqual(resp.status_code, 200) return resp - def login(self, email, pw): + def login(self, email, password): '''Login, check that it worked.''' - resp = self._login(email, pw) + resp = self._login(email, password) data = parse_json(resp) self.assertTrue(data['success']) return resp @@ -154,34 +155,34 @@ class ActivateLoginTestCase(TestCase): self.assertEqual(resp.status_code, 302) return resp - def _create_account(self, username, email, pw): + def _create_account(self, username, email, password): '''Try to create an account. No error checking''' resp = self.client.post('/create_account', { 'username': username, 'email': email, - 'password': pw, + 'password': password, 'name': 'Fred Weasley', 'terms_of_service': 'true', 'honor_code': 'true', }) return resp - def create_account(self, username, email, pw): + def create_account(self, username, email, password): '''Create the account and check that it worked''' - resp = self._create_account(username, email, pw) + resp = self._create_account(username, email, password) self.assertEqual(resp.status_code, 200) data = parse_json(resp) self.assertEqual(data['success'], True) # Check both that the user is created, and inactive - self.assertFalse(user(email).is_active) + self.assertFalse(get_user(email).is_active) return resp def _activate_user(self, email): '''Look up the activation key for the user, then hit the activate view. No error checking''' - activation_key = registration(email).activation_key + activation_key = get_registration(email).activation_key # and now we try to activate resp = self.client.get(reverse('activate', kwargs={'key': activation_key})) @@ -191,19 +192,7 @@ class ActivateLoginTestCase(TestCase): resp = self._activate_user(email) self.assertEqual(resp.status_code, 200) # Now make sure that the user is now actually activated - self.assertTrue(user(email).is_active) - - def test_activate_login(self): - '''The setup function does all the work''' - pass - - def test_logout(self): - '''Setup function does login''' - self.logout() - - -class PageLoader(ActivateLoginTestCase): - ''' Base class that adds a function to load all pages in a modulestore ''' + self.assertTrue(get_user(email).is_active) def _enroll(self, course): """Post to the enrollment view, and return the parsed json response""" @@ -240,8 +229,7 @@ class PageLoader(ActivateLoginTestCase): """ resp = self.client.get(url) self.assertEqual(resp.status_code, code, - "got code {0} for url '{1}'. Expected code {2}" - .format(resp.status_code, url, code)) + "got code {0} for url '{1}'. Expected code {2}".format(resp.status_code, url, code)) return resp def check_for_post_code(self, code, url, data={}): @@ -251,10 +239,27 @@ class PageLoader(ActivateLoginTestCase): """ resp = self.client.post(url, data) self.assertEqual(resp.status_code, code, - "got code {0} for url '{1}'. Expected code {2}" - .format(resp.status_code, url, code)) + "got code {0} for url '{1}'. Expected code {2}".format(resp.status_code, url, code)) return resp + +class ActivateLoginTest(LoginEnrollmentTestCase): + '''Test logging in and logging out''' + def setUp(self): + self.setup_viewtest_user() + + def test_activate_login(self): + '''Test login -- the setup function does all the work''' + pass + + def test_logout(self): + '''Test logout -- setup function does login''' + self.logout() + + +class PageLoaderTestCase(LoginEnrollmentTestCase): + ''' Base class that adds a function to load all pages in a modulestore ''' + def check_pages_load(self, module_store): """Make all locations in course load""" # enroll in the course before trying to access pages @@ -264,14 +269,14 @@ class PageLoader(ActivateLoginTestCase): self.enroll(course) course_id = course.id - n = 0 + num = 0 num_bad = 0 all_ok = True for descriptor in module_store.get_items( Location(None, None, None, None, None)): - n += 1 + num += 1 print "Checking ", descriptor.location.url() # We have ancillary course information now as modules and we can't simply use 'jump_to' to view them @@ -332,45 +337,43 @@ class PageLoader(ActivateLoginTestCase): print msg self.assertTrue(all_ok) # fail fast - print "{0}/{1} good".format(n - num_bad, n) - log.info("{0}/{1} good".format(n - num_bad, n)) + print "{0}/{1} good".format(num - num_bad, num) + log.info("{0}/{1} good".format(num - num_bad, num)) self.assertTrue(all_ok) @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) -class TestCoursesLoadTestCase_XmlModulestore(PageLoader): - '''Check that all pages in test courses load properly''' +class TestCoursesLoadTestCase_XmlModulestore(PageLoaderTestCase): + '''Check that all pages in test courses load properly from XML''' def setUp(self): - ActivateLoginTestCase.setUp(self) + self.setup_viewtest_user() xmodule.modulestore.django._MODULESTORES = {} def test_toy_course_loads(self): - module_store = XMLModuleStore( - TEST_DATA_DIR, - default_class='xmodule.hidden_module.HiddenDescriptor', - course_dirs=['toy'], - load_error_modules=True, + module_store = XMLModuleStore(TEST_DATA_DIR, + default_class='xmodule.hidden_module.HiddenDescriptor', + course_dirs=['toy'], + load_error_modules=True, ) self.check_pages_load(module_store) def test_full_course_loads(self): - module_store = XMLModuleStore( - TEST_DATA_DIR, - default_class='xmodule.hidden_module.HiddenDescriptor', - course_dirs=['full'], - load_error_modules=True, + module_store = XMLModuleStore(TEST_DATA_DIR, + default_class='xmodule.hidden_module.HiddenDescriptor', + course_dirs=['full'], + load_error_modules=True, ) self.check_pages_load(module_store) @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) -class TestCoursesLoadTestCase_MongoModulestore(PageLoader): - '''Check that all pages in test courses load properly''' +class TestCoursesLoadTestCase_MongoModulestore(PageLoaderTestCase): + '''Check that all pages in test courses load properly from Mongo''' def setUp(self): - ActivateLoginTestCase.setUp(self) + self.setup_viewtest_user() xmodule.modulestore.django._MODULESTORES = {} modulestore().collection.drop() @@ -386,7 +389,7 @@ class TestCoursesLoadTestCase_MongoModulestore(PageLoader): @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) -class TestNavigation(PageLoader): +class TestNavigation(LoginEnrollmentTestCase): """Check that navigation state is saved properly""" def setUp(self): @@ -447,7 +450,7 @@ class TestDraftModuleStore(TestCase): @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) -class TestViewAuth(PageLoader): +class TestViewAuth(LoginEnrollmentTestCase): """Check that view authentication works properly""" # NOTE: setUpClass() runs before override_settings takes effect, so @@ -492,7 +495,7 @@ class TestViewAuth(PageLoader): 'gradebook', 'grade_summary',)] urls.append(reverse('student_progress', kwargs={'course_id': course.id, - 'student_id': user(self.student).id})) + 'student_id': get_user(self.student).id})) return urls # shouldn't be able to get to the instructor pages @@ -502,8 +505,8 @@ class TestViewAuth(PageLoader): # Make the instructor staff in the toy course group_name = _course_staff_group_name(self.toy.location) - g = Group.objects.create(name=group_name) - g.user_set.add(user(self.instructor)) + group = Group.objects.create(name=group_name) + group.user_set.add(get_user(self.instructor)) self.logout() self.login(self.instructor, self.password) @@ -518,9 +521,9 @@ class TestViewAuth(PageLoader): self.check_for_get_code(404, url) # now also make the instructor staff - u = user(self.instructor) - u.is_staff = True - u.save() + instructor = get_user(self.instructor) + instructor.is_staff = True + instructor.save() # and now should be able to load both for url in instructor_urls(self.toy) + instructor_urls(self.full): @@ -627,7 +630,7 @@ class TestViewAuth(PageLoader): # to make access checking smarter and understand both the effective # user (the student), and the requesting user (the prof) url = reverse('student_progress', kwargs={'course_id': course.id, - 'student_id': user(self.student).id}) + 'student_id': get_user(self.student).id}) print 'checking for 404 on view-as-student: {0}'.format(url) self.check_for_get_code(404, url) @@ -648,8 +651,8 @@ class TestViewAuth(PageLoader): print '=== Testing course instructor access....' # Make the instructor staff in the toy course group_name = _course_staff_group_name(self.toy.location) - g = Group.objects.create(name=group_name) - g.user_set.add(user(self.instructor)) + group = Group.objects.create(name=group_name) + group.user_set.add(get_user(self.instructor)) self.logout() self.login(self.instructor, self.password) @@ -663,9 +666,9 @@ class TestViewAuth(PageLoader): print '=== Testing staff access....' # now also make the instructor staff - u = user(self.instructor) - u.is_staff = True - u.save() + instructor = get_user(self.instructor) + instructor.is_staff = True + instructor.save() # and now should be able to load both check_staff(self.toy) @@ -698,8 +701,8 @@ class TestViewAuth(PageLoader): print '=== Testing course instructor access....' # Make the instructor staff in the toy course group_name = _course_staff_group_name(self.toy.location) - g = Group.objects.create(name=group_name) - g.user_set.add(user(self.instructor)) + group = Group.objects.create(name=group_name) + group.user_set.add(get_user(self.instructor)) print "logout/login" self.logout() @@ -709,10 +712,10 @@ class TestViewAuth(PageLoader): print '=== Testing staff access....' # now make the instructor global staff, but not in the instructor group - g.user_set.remove(user(self.instructor)) - u = user(self.instructor) - u.is_staff = True - u.save() + group.user_set.remove(get_user(self.instructor)) + instructor = get_user(self.instructor) + instructor.is_staff = True + instructor.save() # unenroll and try again self.unenroll(self.toy) @@ -726,8 +729,8 @@ class TestViewAuth(PageLoader): # Make courses start in the future tomorrow = time.time() + 24 * 3600 - nextday = tomorrow + 24 * 3600 - yesterday = time.time() - 24 * 3600 + # nextday = tomorrow + 24 * 3600 + # yesterday = time.time() - 24 * 3600 # toy course's hasn't started self.toy.lms.start = time.gmtime(tomorrow) @@ -737,20 +740,20 @@ class TestViewAuth(PageLoader): self.toy.lms.days_early_for_beta = 2 # student user shouldn't see it - student_user = user(self.student) + student_user = get_user(self.student) self.assertFalse(has_access(student_user, self.toy, 'load')) # now add the student to the beta test group group_name = course_beta_test_group_name(self.toy.location) - g = Group.objects.create(name=group_name) - g.user_set.add(student_user) + group = Group.objects.create(name=group_name) + group.user_set.add(student_user) # now the student should see it self.assertTrue(has_access(student_user, self.toy, 'load')) @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) -class TestCourseGrader(PageLoader): +class TestCourseGrader(LoginEnrollmentTestCase): """Check that a course gets graded properly""" # NOTE: setUpClass() runs before override_settings takes effect, so @@ -773,35 +776,39 @@ class TestCourseGrader(PageLoader): self.activate_user(self.student) self.enroll(self.graded_course) - self.student_user = user(self.student) + self.student_user = get_user(self.student) self.factory = RequestFactory() def get_grade_summary(self): + '''calls grades.grade for current user and course''' model_data_cache = ModelDataCache.cache_for_descriptor_descendents( self.graded_course.id, self.student_user, self.graded_course) fake_request = self.factory.get(reverse('progress', - kwargs={'course_id': self.graded_course.id})) + kwargs={'course_id': self.graded_course.id})) return grades.grade(self.student_user, fake_request, self.graded_course, model_data_cache) def get_homework_scores(self): + '''get scores for homeworks''' return self.get_grade_summary()['totaled_scores']['Homework'] def get_progress_summary(self): + '''return progress summary structure for current user and course''' model_data_cache = ModelDataCache.cache_for_descriptor_descendents( self.graded_course.id, self.student_user, self.graded_course) fake_request = self.factory.get(reverse('progress', - kwargs={'course_id': self.graded_course.id})) + kwargs={'course_id': self.graded_course.id})) progress_summary = grades.progress_summary(self.student_user, fake_request, self.graded_course, model_data_cache) return progress_summary def check_grade_percent(self, percent): + '''assert that percent grade is as expected''' grade_summary = self.get_grade_summary() self.assertEqual(grade_summary['percent'], percent) @@ -816,10 +823,9 @@ class TestCourseGrader(PageLoader): problem_location = "i4x://edX/graded/problem/{0}".format(problem_url_name) modx_url = reverse('modx_dispatch', - kwargs={ - 'course_id': self.graded_course.id, - 'location': problem_location, - 'dispatch': 'problem_check', }) + kwargs={'course_id': self.graded_course.id, + 'location': problem_location, + 'dispatch': 'problem_check', }) resp = self.client.post(modx_url, { 'input_i4x-edX-graded-problem-{0}_2_1'.format(problem_url_name): responses[0], @@ -831,16 +837,17 @@ class TestCourseGrader(PageLoader): return resp def problem_location(self, problem_url_name): + '''Get location string for problem, assuming hardcoded course_id''' return "i4x://edX/graded/problem/{0}".format(problem_url_name) def reset_question_answer(self, problem_url_name): + '''resets specified problem for current user''' problem_location = self.problem_location(problem_url_name) modx_url = reverse('modx_dispatch', - kwargs={ - 'course_id': self.graded_course.id, - 'location': problem_location, - 'dispatch': 'problem_reset', }) + kwargs={'course_id': self.graded_course.id, + 'location': problem_location, + 'dispatch': 'problem_reset', }) resp = self.client.post(modx_url) return resp @@ -855,6 +862,7 @@ class TestCourseGrader(PageLoader): return [s.earned for s in self.get_homework_scores()] def score_for_hw(hw_url_name): + """returns list of scores for a given url""" hw_section = [section for section in self.get_progress_summary()[0]['sections'] if section.get('url_name') == hw_url_name][0] diff --git a/lms/djangoapps/django_comment_client/forum/views.py b/lms/djangoapps/django_comment_client/forum/views.py index 3eee0948da..3a517af26e 100644 --- a/lms/djangoapps/django_comment_client/forum/views.py +++ b/lms/djangoapps/django_comment_client/forum/views.py @@ -1,28 +1,22 @@ import json import logging +import xml.sax.saxutils as saxutils from django.contrib.auth.decorators import login_required -from django.views.decorators.http import require_POST -from django.http import HttpResponse, Http404 -from django.utils import simplejson +from django.http import Http404 from django.core.context_processors import csrf -from django.core.urlresolvers import reverse from django.contrib.auth.models import User from mitxmako.shortcuts import render_to_response, render_to_string from courseware.courses import get_course_with_access -from course_groups.cohorts import is_course_cohorted, get_cohort_id, is_commentable_cohorted, get_cohorted_commentables, get_cohort, get_course_cohorts, get_cohort_by_id +from course_groups.cohorts import (is_course_cohorted, get_cohort_id, is_commentable_cohorted, + get_cohorted_commentables, get_course_cohorts, get_cohort_by_id) from courseware.access import has_access -from urllib import urlencode -from operator import methodcaller -from django_comment_client.permissions import check_permissions_by_view, cached_has_permission -from django_comment_client.utils import (merge_dict, extract, strip_none, - strip_blank, get_courseware_context) - +from django_comment_client.permissions import cached_has_permission +from django_comment_client.utils import (merge_dict, extract, strip_none, get_courseware_context) import django_comment_client.utils as utils import comment_client as cc -import xml.sax.saxutils as saxutils THREADS_PER_PAGE = 20 INLINE_THREADS_PER_PAGE = 20 @@ -31,6 +25,7 @@ escapedict = {'"': '"'} log = logging.getLogger("edx.discussions") +@login_required def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAGE): """ This may raise cc.utils.CommentClientError or @@ -60,7 +55,6 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG cc_user.default_sort_key = request.GET.get('sort_key') cc_user.save() - #there are 2 dimensions to consider when executing a search with respect to group id #is user a moderator #did the user request a group @@ -91,18 +85,17 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG #now add the group name if the thread has a group id for thread in threads: - + if thread.get('group_id'): thread['group_name'] = get_cohort_by_id(course_id, thread.get('group_id')).name thread['group_string'] = "This post visible only to Group %s." % (thread['group_name']) else: thread['group_name'] = "" thread['group_string'] = "This post visible to everyone." - + #patch for backward compatibility to comments service if not 'pinned' in thread: thread['pinned'] = False - query_params['page'] = page query_params['num_pages'] = num_pages @@ -110,6 +103,7 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG return threads, query_params +@login_required def inline_discussion(request, course_id, discussion_id): """ Renders JSON for DiscussionModules @@ -142,14 +136,14 @@ def inline_discussion(request, course_id, discussion_id): cohorts_list = list() if is_cohorted: - cohorts_list.append({'name':'All Groups','id':None}) + cohorts_list.append({'name': 'All Groups', 'id': None}) #if you're a mod, send all cohorts and let you pick if is_moderator: cohorts = get_course_cohorts(course_id) for c in cohorts: - cohorts_list.append({'name':c.name, 'id':c.id}) + cohorts_list.append({'name': c.name, 'id': c.id}) else: #students don't get to choose @@ -216,9 +210,6 @@ def forum_form_discussion(request, course_id): user_cohort_id = get_cohort_id(request.user, course_id) - - - context = { 'csrf': csrf(request)['csrf_token'], 'course': course, @@ -242,6 +233,7 @@ def forum_form_discussion(request, course_id): return render_to_response('discussion/index.html', context) + @login_required def single_thread(request, course_id, discussion_id, thread_id): course = get_course_with_access(request.user, course_id, 'load') @@ -250,11 +242,11 @@ def single_thread(request, course_id, discussion_id, thread_id): try: thread = cc.Thread.find(thread_id).retrieve(recursive=True, user_id=request.user.id) - + #patch for backward compatibility with comments service if not 'pinned' in thread.attributes: thread['pinned'] = False - + except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err: log.error("Error loading single thread.") raise Http404 @@ -352,7 +344,7 @@ def user_profile(request, course_id, user_id): query_params = { 'page': request.GET.get('page', 1), 'per_page': THREADS_PER_PAGE, # more than threads_per_page to show more activities - } + } threads, page, num_pages = profiled_user.active_threads(query_params) query_params['page'] = page @@ -369,8 +361,6 @@ def user_profile(request, course_id, user_id): 'annotated_content_info': saxutils.escape(json.dumps(annotated_content_info), escapedict), }) else: - - context = { 'course': course, 'user': request.user, @@ -426,5 +416,5 @@ def followed_threads(request, course_id, user_id): } return render_to_response('discussion/user_profile.html', context) - except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err: + except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError): raise Http404 diff --git a/lms/djangoapps/django_comment_client/tests.py b/lms/djangoapps/django_comment_client/tests.py index 1d925cdb8e..a35df54cd9 100644 --- a/lms/djangoapps/django_comment_client/tests.py +++ b/lms/djangoapps/django_comment_client/tests.py @@ -1,73 +1,12 @@ -from django.contrib.auth.models import User, Group -from django.core.urlresolvers import reverse -from django.test import TestCase -from django.test.client import RequestFactory -from django.conf import settings - -from mock import Mock - -from django.test.utils import override_settings - -import xmodule.modulestore.django - -from student.models import CourseEnrollment - -from django.db.models.signals import m2m_changed, pre_delete, pre_save, post_delete, post_save -from django.dispatch.dispatcher import _make_id import string import random -from .permissions import has_permission -from .models import Role, Permission -from xmodule.modulestore.django import modulestore -from xmodule.modulestore import Location -from xmodule.modulestore.xml_importer import import_from_xml -from xmodule.modulestore.xml import XMLModuleStore - -import comment_client - -from courseware.tests.tests import PageLoader, TEST_DATA_XML_MODULESTORE - -#@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) -#class TestCohorting(PageLoader): -# """Check that cohorting works properly""" -# -# def setUp(self): -# xmodule.modulestore.django._MODULESTORES = {} -# -# # Assume courses are there -# self.toy = modulestore().get_course("edX/toy/2012_Fall") -# -# # Create two accounts -# self.student = 'view@test.com' -# self.student2 = 'view2@test.com' -# self.password = 'foo' -# self.create_account('u1', self.student, self.password) -# self.create_account('u2', self.student2, self.password) -# self.activate_user(self.student) -# self.activate_user(self.student2) -# -# def test_create_thread(self): -# my_save = Mock() -# comment_client.perform_request = my_save -# -# resp = self.client.post( -# reverse('django_comment_client.base.views.create_thread', -# kwargs={'course_id': 'edX/toy/2012_Fall', -# 'commentable_id': 'General'}), -# {'some': "some", -# 'data': 'data'}) -# self.assertTrue(my_save.called) -# -# #self.assertEqual(resp.status_code, 200) -# #self.assertEqual(my_save.something, "expected", "complaint if not true") -# -# self.toy.cohort_config = {"cohorted": True} -# -# # call the view again ... -# -# # assert that different things happened +from django.contrib.auth.models import User +from django.test import TestCase +from student.models import CourseEnrollment +from django_comment_client.permissions import has_permission +from django_comment_client.models import Role class PermissionsTestCase(TestCase): diff --git a/lms/djangoapps/instructor/tests.py b/lms/djangoapps/instructor/tests.py index b775aa158a..512e81e302 100644 --- a/lms/djangoapps/instructor/tests.py +++ b/lms/djangoapps/instructor/tests.py @@ -8,13 +8,6 @@ Notes for running by hand: django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/instructor """ -import courseware.tests.tests as ct - -import json - -from nose import SkipTest -from mock import patch, Mock - from django.test.utils import override_settings # Need access to internal func to put users in the right group @@ -26,13 +19,13 @@ from django_comment_client.models import Role, FORUM_ROLE_ADMINISTRATOR, \ from django_comment_client.utils import has_forum_access from courseware.access import _course_staff_group_name -import courseware.tests.tests as ct +from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user from xmodule.modulestore.django import modulestore import xmodule.modulestore.django -@override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE) -class TestInstructorDashboardGradeDownloadCSV(ct.PageLoader): +@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) +class TestInstructorDashboardGradeDownloadCSV(LoginEnrollmentTestCase): ''' Check for download of csv ''' @@ -55,7 +48,7 @@ class TestInstructorDashboardGradeDownloadCSV(ct.PageLoader): def make_instructor(course): group_name = _course_staff_group_name(course.location) g = Group.objects.create(name=group_name) - g.user_set.add(ct.user(self.instructor)) + g.user_set.add(get_user(self.instructor)) make_instructor(self.toy) @@ -63,7 +56,6 @@ class TestInstructorDashboardGradeDownloadCSV(ct.PageLoader): self.login(self.instructor, self.password) self.enroll(self.toy) - def test_download_grades_csv(self): course = self.toy url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) @@ -101,9 +93,8 @@ def action_name(operation, rolename): return '{0} forum {1}'.format(operation, FORUM_ADMIN_ACTION_SUFFIX[rolename]) - -@override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE) -class TestInstructorDashboardForumAdmin(ct.PageLoader): +@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) +class TestInstructorDashboardForumAdmin(LoginEnrollmentTestCase): ''' Check for change in forum admin role memberships ''' @@ -112,7 +103,6 @@ class TestInstructorDashboardForumAdmin(ct.PageLoader): xmodule.modulestore.django._MODULESTORES = {} courses = modulestore().get_courses() - self.course_id = "edX/toy/2012_Fall" self.toy = modulestore().get_course(self.course_id) @@ -127,14 +117,12 @@ class TestInstructorDashboardForumAdmin(ct.PageLoader): group_name = _course_staff_group_name(self.toy.location) g = Group.objects.create(name=group_name) - g.user_set.add(ct.user(self.instructor)) + g.user_set.add(get_user(self.instructor)) self.logout() self.login(self.instructor, self.password) self.enroll(self.toy) - - def initialize_roles(self, course_id): self.admin_role = Role.objects.get_or_create(name=FORUM_ROLE_ADMINISTRATOR, course_id=course_id)[0] self.moderator_role = Role.objects.get_or_create(name=FORUM_ROLE_MODERATOR, course_id=course_id)[0] diff --git a/lms/djangoapps/licenses/tests.py b/lms/djangoapps/licenses/tests.py index 232c853b62..5289c31bc6 100644 --- a/lms/djangoapps/licenses/tests.py +++ b/lms/djangoapps/licenses/tests.py @@ -1,22 +1,138 @@ +"""Tests for License package""" import logging +import json + from uuid import uuid4 from random import shuffle from tempfile import NamedTemporaryFile +from factory import Factory, SubFactory from django.test import TestCase from django.core.management import call_command - -from .models import CourseSoftware, UserLicense +from django.core.urlresolvers import reverse +from licenses.models import CourseSoftware, UserLicense +from courseware.tests.tests import LoginEnrollmentTestCase, get_user COURSE_1 = 'edX/toy/2012_Fall' SOFTWARE_1 = 'matlab' SOFTWARE_2 = 'stata' +SERIAL_1 = '123456abcde' + log = logging.getLogger(__name__) +class CourseSoftwareFactory(Factory): + '''Factory for generating CourseSoftware objects in database''' + FACTORY_FOR = CourseSoftware + + name = SOFTWARE_1 + full_name = SOFTWARE_1 + url = SOFTWARE_1 + course_id = COURSE_1 + + +class UserLicenseFactory(Factory): + ''' + Factory for generating UserLicense objects in database + + By default, the user assigned is null, indicating that the + serial number has not yet been assigned. + ''' + FACTORY_FOR = UserLicense + + software = SubFactory(CourseSoftwareFactory) + serial = SERIAL_1 + + +class LicenseTestCase(LoginEnrollmentTestCase): + '''Tests for licenses.views''' + def setUp(self): + '''creates a user and logs in''' + self.setup_viewtest_user() + self.software = CourseSoftwareFactory() + + def test_get_license(self): + UserLicenseFactory(user=get_user(self.viewtest_email), software=self.software) + response = self.client.post(reverse('user_software_license'), + {'software': SOFTWARE_1, 'generate': 'false'}, + HTTP_X_REQUESTED_WITH='XMLHttpRequest', + HTTP_REFERER='/courses/{0}/some_page'.format(COURSE_1)) + self.assertEqual(200, response.status_code) + json_returned = json.loads(response.content) + self.assertFalse('error' in json_returned) + self.assertTrue('serial' in json_returned) + self.assertEquals(json_returned['serial'], SERIAL_1) + + def test_get_nonexistent_license(self): + response = self.client.post(reverse('user_software_license'), + {'software': SOFTWARE_1, 'generate': 'false'}, + HTTP_X_REQUESTED_WITH='XMLHttpRequest', + HTTP_REFERER='/courses/{0}/some_page'.format(COURSE_1)) + self.assertEqual(200, response.status_code) + json_returned = json.loads(response.content) + self.assertFalse('serial' in json_returned) + self.assertTrue('error' in json_returned) + + def test_create_nonexistent_license(self): + '''Should not assign a license to an unlicensed user when none are available''' + response = self.client.post(reverse('user_software_license'), + {'software': SOFTWARE_1, 'generate': 'true'}, + HTTP_X_REQUESTED_WITH='XMLHttpRequest', + HTTP_REFERER='/courses/{0}/some_page'.format(COURSE_1)) + self.assertEqual(200, response.status_code) + json_returned = json.loads(response.content) + self.assertFalse('serial' in json_returned) + self.assertTrue('error' in json_returned) + + def test_create_license(self): + '''Should assign a license to an unlicensed user if one is unassigned''' + # create an unassigned license + UserLicenseFactory(software=self.software) + response = self.client.post(reverse('user_software_license'), + {'software': SOFTWARE_1, 'generate': 'true'}, + HTTP_X_REQUESTED_WITH='XMLHttpRequest', + HTTP_REFERER='/courses/{0}/some_page'.format(COURSE_1)) + self.assertEqual(200, response.status_code) + json_returned = json.loads(response.content) + self.assertFalse('error' in json_returned) + self.assertTrue('serial' in json_returned) + self.assertEquals(json_returned['serial'], SERIAL_1) + + def test_get_license_from_wrong_course(self): + response = self.client.post(reverse('user_software_license'), + {'software': SOFTWARE_1, 'generate': 'false'}, + HTTP_X_REQUESTED_WITH='XMLHttpRequest', + HTTP_REFERER='/courses/{0}/some_page'.format('some/other/course')) + self.assertEqual(404, response.status_code) + + def test_get_license_from_non_ajax(self): + response = self.client.post(reverse('user_software_license'), + {'software': SOFTWARE_1, 'generate': 'false'}, + HTTP_REFERER='/courses/{0}/some_page'.format(COURSE_1)) + self.assertEqual(404, response.status_code) + + def test_get_license_without_software(self): + response = self.client.post(reverse('user_software_license'), + {'generate': 'false'}, + HTTP_X_REQUESTED_WITH='XMLHttpRequest', + HTTP_REFERER='/courses/{0}/some_page'.format(COURSE_1)) + self.assertEqual(404, response.status_code) + + def test_get_license_without_login(self): + self.logout() + response = self.client.post(reverse('user_software_license'), + {'software': SOFTWARE_1, 'generate': 'false'}, + HTTP_X_REQUESTED_WITH='XMLHttpRequest', + HTTP_REFERER='/courses/{0}/some_page'.format(COURSE_1)) + # if we're not logged in, we should be referred to the login page + self.assertEqual(302, response.status_code) + + class CommandTest(TestCase): + '''Test management command for importing serial numbers''' + def test_import_serial_numbers(self): size = 20 @@ -51,31 +167,33 @@ class CommandTest(TestCase): licenses_count = UserLicense.objects.all().count() self.assertEqual(3 * size, licenses_count) - cs = CourseSoftware.objects.get(pk=1) + software = CourseSoftware.objects.get(pk=1) - lics = UserLicense.objects.filter(software=cs)[:size] + lics = UserLicense.objects.filter(software=software)[:size] known_serials = list(l.serial for l in lics) known_serials.extend(generate_serials(10)) shuffle(known_serials) log.debug('Adding some new and old serials to {0}'.format(SOFTWARE_1)) - with NamedTemporaryFile() as f: - f.write('\n'.join(known_serials)) - f.flush() - args = [COURSE_1, SOFTWARE_1, f.name] + with NamedTemporaryFile() as tmpfile: + tmpfile.write('\n'.join(known_serials)) + tmpfile.flush() + args = [COURSE_1, SOFTWARE_1, tmpfile.name] call_command('import_serial_numbers', *args) log.debug('Check if we added only the new ones') - licenses_count = UserLicense.objects.filter(software=cs).count() + licenses_count = UserLicense.objects.filter(software=software).count() self.assertEqual((2 * size) + 10, licenses_count) def generate_serials(size=20): + '''generate a list of serial numbers''' return [str(uuid4()) for _ in range(size)] def generate_serials_file(size=20): + '''output list of generated serial numbers to a temp file''' serials = generate_serials(size) temp_file = NamedTemporaryFile() diff --git a/lms/djangoapps/licenses/views.py b/lms/djangoapps/licenses/views.py index 20966427ba..1c1a80ed31 100644 --- a/lms/djangoapps/licenses/views.py +++ b/lms/djangoapps/licenses/views.py @@ -7,12 +7,13 @@ from collections import namedtuple, defaultdict from mitxmako.shortcuts import render_to_string +from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User from django.http import HttpResponse, Http404 -from django.views.decorators.csrf import requires_csrf_token, csrf_protect +from django.views.decorators.csrf import requires_csrf_token -from .models import CourseSoftware -from .models import get_courses_licenses, get_or_create_license, get_license +from licenses.models import CourseSoftware +from licenses.models import get_courses_licenses, get_or_create_license, get_license log = logging.getLogger("mitx.licenses") @@ -44,6 +45,7 @@ def get_licenses_by_course(user, courses): return data_by_course +@login_required @requires_csrf_token def user_software_license(request): if request.method != 'POST' or not request.is_ajax(): @@ -65,19 +67,21 @@ def user_software_license(request): try: software = CourseSoftware.objects.get(name=software_name, course_id=course_id) - print software except CourseSoftware.DoesNotExist: raise Http404 - user = User.objects.get(id=user_id) + try: + user = User.objects.get(id=user_id) + except User.DoesNotExist: + raise Http404 if generate: - license = get_or_create_license(user, software) + software_license = get_or_create_license(user, software) else: - license = get_license(user, software) + software_license = get_license(user, software) - if license: - response = {'serial': license.serial} + if software_license: + response = {'serial': software_license.serial} else: response = {'error': 'No serial number found'} diff --git a/lms/djangoapps/open_ended_grading/tests.py b/lms/djangoapps/open_ended_grading/tests.py index 1fd871d0cd..e554fdf0e1 100644 --- a/lms/djangoapps/open_ended_grading/tests.py +++ b/lms/djangoapps/open_ended_grading/tests.py @@ -4,22 +4,22 @@ Tests for open ended grading interfaces django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/open_ended_grading """ -from django.test import TestCase -from open_ended_grading import staff_grading_service -from xmodule.open_ended_grading_classes import peer_grading_service -from xmodule import peer_grading_module +import json +from mock import MagicMock + from django.core.urlresolvers import reverse from django.contrib.auth.models import Group +from mitxmako.shortcuts import render_to_string -from courseware.access import _course_staff_group_name -import courseware.tests.tests as ct +from xmodule.open_ended_grading_classes import peer_grading_service +from xmodule import peer_grading_module from xmodule.modulestore.django import modulestore import xmodule.modulestore.django -from nose import SkipTest -from mock import patch, Mock, MagicMock -import json from xmodule.x_module import ModuleSystem -from mitxmako.shortcuts import render_to_string + +from open_ended_grading import staff_grading_service +from courseware.access import _course_staff_group_name +from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user import logging @@ -30,8 +30,8 @@ from django.http import QueryDict from xmodule.tests import test_util_open_ended -@override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE) -class TestStaffGradingService(ct.PageLoader): +@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) +class TestStaffGradingService(LoginEnrollmentTestCase): ''' Check that staff grading service proxy works. Basically just checking the access control and error handling logic -- all the actual work is on the @@ -56,7 +56,7 @@ class TestStaffGradingService(ct.PageLoader): def make_instructor(course): group_name = _course_staff_group_name(course.location) g = Group.objects.create(name=group_name) - g.user_set.add(ct.user(self.instructor)) + g.user_set.add(get_user(self.instructor)) make_instructor(self.toy) @@ -126,8 +126,8 @@ class TestStaffGradingService(ct.PageLoader): self.assertIsNotNone(d['problem_list']) -@override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE) -class TestPeerGradingService(ct.PageLoader): +@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) +class TestPeerGradingService(LoginEnrollmentTestCase): ''' Check that staff grading service proxy works. Basically just checking the access control and error handling logic -- all the actual work is on the From c639799a07730eb2244904838e6591eb760ecc6f Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Wed, 27 Mar 2013 18:12:48 -0400 Subject: [PATCH 235/241] fix missing import --- lms/djangoapps/courseware/tests/test_module_render.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lms/djangoapps/courseware/tests/test_module_render.py b/lms/djangoapps/courseware/tests/test_module_render.py index fdba969ab5..85a65b7772 100644 --- a/lms/djangoapps/courseware/tests/test_module_render.py +++ b/lms/djangoapps/courseware/tests/test_module_render.py @@ -2,6 +2,7 @@ from mock import MagicMock import json from django.http import Http404, HttpResponse +from django.core.urlresolvers import reverse from django.conf import settings from django.test import TestCase from django.test.client import RequestFactory From 03f9bb5d38a3855ee56087b9132f8ebbe13be747 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 27 Mar 2013 22:37:07 -0400 Subject: [PATCH 236/241] use a request-scoped cache to keep the metadata inheritence tree around for the whole request. This means we should only do one trip to Memcached/Mongo per course per request. This is expected to keep memory utilization down --- common/djangoapps/request_cache/__init__.py | 0 common/djangoapps/request_cache/middleware.py | 20 +++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 common/djangoapps/request_cache/__init__.py create mode 100644 common/djangoapps/request_cache/middleware.py diff --git a/common/djangoapps/request_cache/__init__.py b/common/djangoapps/request_cache/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/djangoapps/request_cache/middleware.py b/common/djangoapps/request_cache/middleware.py new file mode 100644 index 0000000000..9d3dffdf27 --- /dev/null +++ b/common/djangoapps/request_cache/middleware.py @@ -0,0 +1,20 @@ +import threading + +_request_cache_threadlocal = threading.local() +_request_cache_threadlocal.data = {} + +class RequestCache(object): + @classmethod + def get_request_cache(cls): + return _request_cache_threadlocal + + def clear_request_cache(self): + _request_cache_threadlocal.data = {} + + def process_request(self, request): + self.clear_request_cache() + return None + + def process_response(self, request, response): + self.clear_request_cache() + return response \ No newline at end of file From b609a96902e6be8c1be4fd423db2c9d2dbe018ca Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 27 Mar 2013 22:51:52 -0400 Subject: [PATCH 237/241] ummm. forgot to commit stuff --- cms/envs/common.py | 1 + cms/one_time_startup.py | 4 +- .../lib/xmodule/xmodule/modulestore/mongo.py | 105 ++++++++++-------- .../xmodule/modulestore/tests/test_mongo.py | 55 --------- lms/envs/common.py | 1 + lms/one_time_startup.py | 4 +- 6 files changed, 64 insertions(+), 106 deletions(-) diff --git a/cms/envs/common.py b/cms/envs/common.py index a83f61d8f9..12fa09947a 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -113,6 +113,7 @@ TEMPLATE_LOADERS = ( MIDDLEWARE_CLASSES = ( 'contentserver.middleware.StaticContentServer', + 'request_cache.middleware.RequestCache', 'django.middleware.cache.UpdateCacheMiddleware', 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', diff --git a/cms/one_time_startup.py b/cms/one_time_startup.py index 38a2fef847..6e88fed439 100644 --- a/cms/one_time_startup.py +++ b/cms/one_time_startup.py @@ -1,13 +1,15 @@ from dogapi import dog_http_api, dog_stats_api from django.conf import settings from xmodule.modulestore.django import modulestore +from request_cache.middleware import RequestCache from django.core.cache import get_cache, InvalidCacheBackendError cache = get_cache('mongo_metadata_inheritance') for store_name in settings.MODULESTORE: store = modulestore(store_name) - store.metadata_inheritance_cache = cache + store.metadata_inheritance_cache_subsystem = cache + store.request_cache = RequestCache.get_request_cache() if hasattr(settings, 'DATADOG_API'): dog_http_api.api_key = settings.DATADOG_API diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py index 38b15ab76e..b93a95c965 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -27,6 +27,9 @@ from .inheritance import own_metadata, INHERITABLE_METADATA, inherit_metadata log = logging.getLogger(__name__) +import threading +_mongo_metadata_request_cache_threadlocal = threading.local() + # TODO (cpennington): This code currently operates under the assumption that # there is only one revision for each item. Once we start versioning inside the CMS, # that assumption will have to change @@ -109,7 +112,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem): references to metadata_inheritance_tree """ def __init__(self, modulestore, module_data, default_class, resources_fs, - error_tracker, render_template, metadata_cache=None): + error_tracker, render_template, cached_metadata=None): """ modulestore: the module store that can be used to retrieve additional modules @@ -134,7 +137,8 @@ class CachingDescriptorSystem(MakoDescriptorSystem): # cdodge: other Systems have a course_id attribute defined. To keep things consistent, let's # define an attribute here as well, even though it's None self.course_id = None - self.metadata_cache = metadata_cache + self.cached_metadata = cached_metadata + def load_item(self, location): """ @@ -170,8 +174,8 @@ class CachingDescriptorSystem(MakoDescriptorSystem): model_data = DbModel(kvs, class_, None, MongoUsage(self.course_id, location)) module = class_(self, location, model_data) - if self.metadata_cache is not None: - metadata_to_inherit = self.metadata_cache.get(metadata_cache_key(location), {}).get('parent_metadata', {}).get(location.url(), {}) + if self.cached_metadata is not None: + metadata_to_inherit = self.cached_metadata.get(location.url(), {}) inherit_metadata(module, metadata_to_inherit) return module except: @@ -223,7 +227,8 @@ class MongoModuleStore(ModuleStoreBase): def __init__(self, host, db, collection, fs_root, render_template, port=27017, default_class=None, error_tracker=null_error_tracker, - user=None, password=None, **kwargs): + user=None, password=None, request_cache=None, + metadata_inheritance_cache_subsystem=None, **kwargs): ModuleStoreBase.__init__(self) @@ -254,8 +259,10 @@ class MongoModuleStore(ModuleStoreBase): self.error_tracker = error_tracker self.render_template = render_template self.ignore_write_events_on_courses = [] + self.request_cache = request_cache + self.metadata_inheritance_cache_subsystem = metadata_inheritance_cache_subsystem - def get_metadata_inheritance_tree(self, location): + def compute_metadata_inheritance_tree(self, location): ''' TODO (cdodge) This method can be deleted when the 'split module store' work has been completed ''' @@ -323,32 +330,47 @@ class MongoModuleStore(ModuleStoreBase): if root is not None: _compute_inherited_metadata(root) - return {'parent_metadata': metadata_to_inherit, - 'timestamp': datetime.now()} + return metadata_to_inherit - def get_cached_metadata_inheritance_trees(self, locations, force_refresh=False): + def get_cached_metadata_inheritance_tree(self, location, force_refresh=False): ''' TODO (cdodge) This method can be deleted when the 'split module store' work has been completed ''' + key = metadata_cache_key(location) + tree = {} + + if not force_refresh: + # see if we are first in the request cache (if present) + if self.request_cache is not None and key in self.request_cache.data.get('metadata_inheritance', {}): + logging.debug('***** HIT IN REQUEST CACHE') + return self.request_cache.data['metadata_inheritance'][key] - trees = {} - if locations and self.metadata_inheritance_cache is not None and not force_refresh: - trees = self.metadata_inheritance_cache.get_many(list(set([metadata_cache_key(loc) for loc in locations]))) - else: - # This is to help guard against an accident prod runtime without a cache - logging.warning('Running MongoModuleStore without metadata_inheritance_cache. ' - 'This should not happen in production!') + # then look in any caching subsystem (e.g. memcached) + if self.metadata_inheritance_cache_subsystem is not None: + tree = self.metadata_inheritance_cache_subsystem.get(key, {}) + if tree: + logging.debug('***** HIT IN MEMCACHED') + else: + logging.warning('Running MongoModuleStore without a metadata_inheritance_cache_subsystem. This is OK in localdev and testing environment. Not OK in production.') - to_cache = {} - for loc in locations: - cache_key = metadata_cache_key(loc) - if cache_key not in trees: - to_cache[cache_key] = trees[cache_key] = self.get_metadata_inheritance_tree(loc) + if not tree: + # if not in subsystem, or we are on force refresh, then we have to compute + logging.debug('***** COMPUTING METADATA') + tree = self.compute_metadata_inheritance_tree(location) - if to_cache and self.metadata_inheritance_cache is not None: - self.metadata_inheritance_cache.set_many(to_cache) + # now populate a request_cache, if available + if self.request_cache is not None: + # we can't assume the 'metadatat_inheritance' part of the request cache dict has been + # defined + if 'metadata_inheritance' not in self.request_cache.data: + self.request_cache.data['metadata_inheritance'] = {} + self.request_cache.data['metadata_inheritance'][key] = tree - return trees + # now write to caching subsystem (e.g. memcached), if available + if self.metadata_inheritance_cache_subsystem is not None: + self.metadata_inheritance_cache_subsystem.set(key, tree) + + return tree def refresh_cached_metadata_inheritance_tree(self, location): """ @@ -357,15 +379,7 @@ class MongoModuleStore(ModuleStoreBase): """ pseudo_course_id = '/'.join([location.org, location.course]) if pseudo_course_id not in self.ignore_write_events_on_courses: - self.get_cached_metadata_inheritance_trees([location], force_refresh=True) - - def clear_cached_metadata_inheritance_tree(self, location): - """ - Delete the cached metadata inheritance tree for the org/course combination - for location - """ - if self.metadata_inheritance_cache is not None: - self.metadata_inheritance_cache.delete(metadata_cache_key(location)) + self.get_cached_metadata_inheritance_tree(location, force_refresh=True) def _clean_item_data(self, item): """ @@ -411,18 +425,7 @@ class MongoModuleStore(ModuleStoreBase): return data - def _cache_metadata_inheritance(self, items, depth, force_refresh=False): - """ - Retrieves all course metadata inheritance trees needed to load items - """ - - locations = [ - Location(item['location']) for item in items - if not (item['location']['category'] == 'course' and depth == 0) - ] - return self.get_cached_metadata_inheritance_trees(locations, force_refresh=force_refresh) - - def _load_item(self, item, data_cache, metadata_cache): + def _load_item(self, item, data_cache, apply_cached_metadata=True): """ Load an XModuleDescriptor from item, using the children stored in data_cache """ @@ -434,6 +437,10 @@ class MongoModuleStore(ModuleStoreBase): resource_fs = OSFS(root) + cached_metadata = {} + if apply_cached_metadata: + cached_metadata = 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 system = CachingDescriptorSystem( @@ -443,7 +450,7 @@ class MongoModuleStore(ModuleStoreBase): resource_fs, self.error_tracker, self.render_template, - metadata_cache, + cached_metadata, ) return system.load_item(item['location']) @@ -453,11 +460,11 @@ class MongoModuleStore(ModuleStoreBase): to specified depth """ data_cache = self._cache_children(items, depth) - inheritance_cache = self._cache_metadata_inheritance(items, depth) # 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, inheritance_cache) for item in items] + # bother with the metadata inheritance + return [self._load_item(item, data_cache, + apply_cached_metadata=(item['location']['category']!='course' or depth !=0)) for item in items] def get_courses(self): ''' diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py index 3e29c07ea4..061d70d09f 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py @@ -103,58 +103,3 @@ class TestMongoModuleStore(object): def test_path_to_location(self): '''Make sure that path_to_location works''' check_path_to_location(self.store) - - def test_metadata_inheritance_query_count(self): - ''' - When retrieving items from mongo, we should only query the cache a number of times - equal to the number of courses being retrieved from. - - We should also not query - ''' - self.store.metadata_inheritance_cache = Mock() - get_many = self.store.metadata_inheritance_cache.get_many - set_many = self.store.metadata_inheritance_cache.set_many - get_many.return_value = {('edX', 'toy'): {}} - - self.store.get_item(Location("i4x://edX/toy/course/2012_Fall"), depth=0) - assert_false(get_many.called) - assert_false(set_many.called) - get_many.reset_mock() - - self.store.get_item(Location("i4x://edX/toy/course/2012_Fall"), depth=3) - get_many.assert_called_with([('edX', 'toy')]) - assert_equals(0, set_many.call_count) - get_many.reset_mock() - - self.store.get_items(Location('i4x', 'edX', None, 'course', None), depth=0) - assert_false(get_many.called) - assert_false(set_many.called) - get_many.reset_mock() - - self.store.get_items(Location('i4x', 'edX', None, 'course', None), depth=3) - assert_equals(1, get_many.call_count) - assert_equals([('edX', 'simple'), ('edX', 'toy')], sorted(get_many.call_args[0][0])) - assert_equals(1, set_many.call_count) - assert_equals([('edX', 'simple')], sorted(set_many.call_args[0][0].keys())) - get_many.reset_mock() - - self.store.get_items(Location('i4x', 'edX', None, None, None), depth=0) - assert_equals(1, get_many.call_count) - assert_equals([('edX', 'simple'), ('edX', 'toy')], sorted(get_many.call_args[0][0])) - assert_equals(1, set_many.call_count) - assert_equals([('edX', 'simple')], sorted(set_many.call_args[0][0].keys())) - get_many.reset_mock() - - def test_metadata_inheritance_query_count_forced_refresh(self): - self.store.metadata_inheritance_cache = Mock() - get_many = self.store.metadata_inheritance_cache.get_many - set_many = self.store.metadata_inheritance_cache.set_many - get_many.return_value = {('edX', 'toy'): {}} - - self.store.get_cached_metadata_inheritance_trees( - [Location("i4x://edX/toy/course/2012_Fall"), Location("i4x://edX/simple/course/2012_Fall")], - True - ) - assert_false(get_many.called) - assert_equals(1, set_many.call_count) - assert_equals([('edX', 'simple'), ('edX', 'toy')], sorted(set_many.call_args[0][0].keys())) diff --git a/lms/envs/common.py b/lms/envs/common.py index cfd6fc34de..8654b5ebf5 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -364,6 +364,7 @@ TEMPLATE_LOADERS = ( MIDDLEWARE_CLASSES = ( 'contentserver.middleware.StaticContentServer', + 'request_cache.middleware.RequestCache', 'django_comment_client.middleware.AjaxExceptionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', diff --git a/lms/one_time_startup.py b/lms/one_time_startup.py index 6b3c45d60f..e1b1f79444 100644 --- a/lms/one_time_startup.py +++ b/lms/one_time_startup.py @@ -2,13 +2,15 @@ import logging from dogapi import dog_http_api, dog_stats_api from django.conf import settings from xmodule.modulestore.django import modulestore +from request_cache.middleware import RequestCache from django.core.cache import get_cache, InvalidCacheBackendError cache = get_cache('mongo_metadata_inheritance') for store_name in settings.MODULESTORE: store = modulestore(store_name) - store.metadata_inheritance_cache = cache + store.metadata_inheritance_cache_subsystem = cache + store.request_cache = RequestCache.get_request_cache() if hasattr(settings, 'DATADOG_API'): dog_http_api.api_key = settings.DATADOG_API From 446397b23bbcf625a82374c46773720e5e059e28 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 27 Mar 2013 23:12:00 -0400 Subject: [PATCH 238/241] remove unused thread.local() --- common/lib/xmodule/xmodule/modulestore/mongo.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py index b93a95c965..b388f81f7c 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -27,9 +27,6 @@ from .inheritance import own_metadata, INHERITABLE_METADATA, inherit_metadata log = logging.getLogger(__name__) -import threading -_mongo_metadata_request_cache_threadlocal = threading.local() - # TODO (cpennington): This code currently operates under the assumption that # there is only one revision for each item. Once we start versioning inside the CMS, # that assumption will have to change From d448aa1365b7aca300bfc67ba3684e162b09e4aa Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 27 Mar 2013 23:13:33 -0400 Subject: [PATCH 239/241] remove debug log messages --- common/lib/xmodule/xmodule/modulestore/mongo.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py index b388f81f7c..7bd61924fa 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -339,20 +339,16 @@ class MongoModuleStore(ModuleStoreBase): if not force_refresh: # see if we are first in the request cache (if present) if self.request_cache is not None and key in self.request_cache.data.get('metadata_inheritance', {}): - logging.debug('***** HIT IN REQUEST CACHE') return self.request_cache.data['metadata_inheritance'][key] # then look in any caching subsystem (e.g. memcached) if self.metadata_inheritance_cache_subsystem is not None: tree = self.metadata_inheritance_cache_subsystem.get(key, {}) - if tree: - logging.debug('***** HIT IN MEMCACHED') else: logging.warning('Running MongoModuleStore without a metadata_inheritance_cache_subsystem. This is OK in localdev and testing environment. Not OK in production.') if not tree: # if not in subsystem, or we are on force refresh, then we have to compute - logging.debug('***** COMPUTING METADATA') tree = self.compute_metadata_inheritance_tree(location) # now populate a request_cache, if available From 3f52261b5b44a7d76dd6cc2c71e7ab95ca10e3e5 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 27 Mar 2013 23:18:38 -0400 Subject: [PATCH 240/241] hmmm. actually, we should only write out to memcache if we've recomputed. Otherwise, a memcache hit will end up writing back to memcache... --- 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 7bd61924fa..47e35cda93 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -350,8 +350,14 @@ class MongoModuleStore(ModuleStoreBase): if not tree: # if not in subsystem, or we are on force refresh, then we have to compute tree = self.compute_metadata_inheritance_tree(location) + + # now write out computed tree to caching subsystem (e.g. memcached), if available + if self.metadata_inheritance_cache_subsystem is not None: + self.metadata_inheritance_cache_subsystem.set(key, tree) - # now populate a request_cache, if available + # now populate a request_cache, if available. NOTE, we are outside of the + # scope of the above if: statement so that after a memcache hit, it'll get + # put into the request_cache if self.request_cache is not None: # we can't assume the 'metadatat_inheritance' part of the request cache dict has been # defined @@ -359,10 +365,6 @@ class MongoModuleStore(ModuleStoreBase): self.request_cache.data['metadata_inheritance'] = {} self.request_cache.data['metadata_inheritance'][key] = tree - # now write to caching subsystem (e.g. memcached), if available - if self.metadata_inheritance_cache_subsystem is not None: - self.metadata_inheritance_cache_subsystem.set(key, tree) - return tree def refresh_cached_metadata_inheritance_tree(self, location): From 7279f9c4601f7ee769ce689b1bcab4c63b94e377 Mon Sep 17 00:00:00 2001 From: cahrens Date: Thu, 28 Mar 2013 10:54:10 -0400 Subject: [PATCH 241/241] Bug fix for grading type not showing on course outline (#258). --- .../contentstore/features/subsection.feature | 8 ++++++++ cms/djangoapps/contentstore/features/subsection.py | 13 ++++++++++++- cms/templates/overview.html | 2 +- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/cms/djangoapps/contentstore/features/subsection.feature b/cms/djangoapps/contentstore/features/subsection.feature index 1be5f4aeb9..e913c6a4bf 100644 --- a/cms/djangoapps/contentstore/features/subsection.feature +++ b/cms/djangoapps/contentstore/features/subsection.feature @@ -17,6 +17,14 @@ Feature: Create Subsection And I click to edit the subsection name Then I see the complete subsection name with a quote in the editor + Scenario: Assign grading type to a subsection and verify it is still shown after refresh (bug #258) + Given I have opened a new course section in Studio + And I have added a new subsection + And I mark it as Homework + Then I see it marked as Homework + And I reload the page + Then I see it marked as Homework + @skip-phantom Scenario: Delete a subsection Given I have opened a new course section in Studio diff --git a/cms/djangoapps/contentstore/features/subsection.py b/cms/djangoapps/contentstore/features/subsection.py index 54f49f2fa6..4ab27fcb49 100644 --- a/cms/djangoapps/contentstore/features/subsection.py +++ b/cms/djangoapps/contentstore/features/subsection.py @@ -40,7 +40,7 @@ def i_click_to_edit_subsection_name(step): def i_see_complete_subsection_name_with_quote_in_editor(step): css = '.subsection-display-name-input' assert world.is_css_present(css) - assert_equal(world.browser.find_by_css(css).value, 'Subsection With "Quote"') + assert_equal(world.css_find(css).value, 'Subsection With "Quote"') @step('I have added a new subsection$') @@ -48,6 +48,17 @@ def i_have_added_a_new_subsection(step): add_subsection() +@step('I mark it as Homework$') +def i_mark_it_as_homework(step): + world.css_click('a.menu-toggle') + world.browser.click_link_by_text('Homework') + + +@step('I see it marked as Homework$') +def i_see_it_marked__as_homework(step): + assert_equal(world.css_find(".status-label").value, 'Homework') + + ############ ASSERTIONS ################### diff --git a/cms/templates/overview.html b/cms/templates/overview.html index 904f654717..d45a90093e 100644 --- a/cms/templates/overview.html +++ b/cms/templates/overview.html @@ -200,7 +200,7 @@
    -
    +