From b5fda6d26df1559b10d10a9dca4fadc78a43d601 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 24 Jul 2013 10:47:43 -0400 Subject: [PATCH 001/112] edx.org: adds in variables/mixins to support edx.org visual changes --- lms/static/sass/base/_mixins.scss | 22 +++++++++++---- lms/static/sass/base/_variables.scss | 40 +++++++++++++++++++--------- 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/lms/static/sass/base/_mixins.scss b/lms/static/sass/base/_mixins.scss index 4060c36f5f..e2074f1976 100644 --- a/lms/static/sass/base/_mixins.scss +++ b/lms/static/sass/base/_mixins.scss @@ -1,10 +1,13 @@ -@function em($pxval, $base: 16) { - @return #{$pxval / $base}em; +// mixins - font sizing +@mixin font-size($sizeValue: 16){ + font-size: $sizeValue + px; + font-size: ($sizeValue/10) + rem; } -// Line-height -@function lh($amount: 1) { - @return $body-line-height * $amount; +// mixins - line height +@mixin line-height($fontSize: auto){ + line-height: ($fontSize*1.48) + px; + line-height: (($fontSize/10)*1.48) + rem; } // image-replacement hidden text @@ -31,6 +34,15 @@ display: block; } +@function em($pxval, $base: 16) { + @return #{$pxval / $base}em; +} + +// Line-height +@function lh($amount: 1) { + @return $body-line-height * $amount; +} + //----------------- // Theme Mixin Styles diff --git a/lms/static/sass/base/_variables.scss b/lms/static/sass/base/_variables.scss index c0bbcfb9ee..b7dd620f8a 100644 --- a/lms/static/sass/base/_variables.scss +++ b/lms/static/sass/base/_variables.scss @@ -39,19 +39,33 @@ $outer-border-color: rgb(170, 170, 170); $light-gray: #ddd; $dark-gray: #333; -// edx.org-related -$m-gray-l1: rgb(203,203,203); -$m-gray-l2: rgb(246,246,246); -$m-gray: rgb(153,153,153); -$m-gray-d1: rgb(102,102,102); -$m-gray-d2: rgb(51,51,51); -$m-gray-a1: rgb(80,80,80); -$m-blue: rgb(65, 116, 170); -// $m-blue: rgb(85, 151, 221); (used in marketing redesign) -$m-blue-l1: rgb(85, 151, 221); -$m-blue-d1: shade($m-blue,15%); -$m-blue-s1: saturate($m-blue,15%); -$m-pink: rgb(204,51,102); +// edx.org marketing site variables +$m-gray: #8A8C8F; +$m-gray-l1: #97999B; +$m-gray-l2: #A4A6A8; +$m-gray-l3: #B1B2B4; +$m-gray-l4: #F5F5F5; +$m-gray-d1: #7D7F83; +$m-gray-d2: #707276; +$m-gray-d3: #646668; +$m-gray-d4: #050505; + +$m-blue: #1AA1DE; +$m-blue-l1: #2BACE6; +$m-blue-l2: #42B5E9; +$m-blue-l3: #59BEEC; +$m-blue-d1: #1790C7; +$m-blue-d2: #1580B0; +$m-blue-d3: #126F9A; +$m-blue-d4: #0A4A67; + +$m-pink: #B52A67; +$m-pink-l1: #CA2F73; +$m-pink-l2: #D33F80; +$m-pink-l3: #D7548E; +$m-pink-d1: #A0255B; +$m-pink-d2: #8C204F; +$m-pink-d3: #771C44; $m-base-font-size: em(15); From 5f15e4227e00c66e24167a5752f1941f0623e202 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 24 Jul 2013 10:48:22 -0400 Subject: [PATCH 002/112] edx.org: applies edx.org visual changes (with overrides/new mixins) --- lms/static/sass/_shame.scss | 200 +++++++++++++++------- lms/static/sass/multicourse/_account.scss | 9 +- lms/static/sass/shared/_header.scss | 22 +-- 3 files changed, 143 insertions(+), 88 deletions(-) diff --git a/lms/static/sass/_shame.scss b/lms/static/sass/_shame.scss index ac6ac24b41..041c42a66f 100644 --- a/lms/static/sass/_shame.scss +++ b/lms/static/sass/_shame.scss @@ -2,70 +2,134 @@ // shame file - used for any bad-form/orphaned scss that knowingly violate edX FED architecture/standards (see - http://csswizardry.com/2013/04/shame-css/) // ==================== -// marketing site - registration iframe band-aid (poor form enough to isolate out) +// edx.org marketing site - 7/2013 visual button revamp + +// extends btn +.m-btn { + @include box-sizing(border-box); + @include transition(color 0.25s ease-in-out, background 0.25s ease-in-out, box-shadow 0.25s ease-in-out); + display: inline-block; + cursor: pointer; + text-decoration: none; + + &:hover, &:active { + + } + + &.disabled, &[disabled] { + cursor: default; + pointer-events: none; + } +} + +.m-btn-pill { + border-radius: ($baseline/5); +} + +.m-btn-rounded { + border-radius: ($baseline/2); +} + +.m-btn-edged { + border-radius: ($baseline/10); +} + +// primary button +.m-btn-primary { + @extend .m-btn; + @extend .m-btn-edged; + border: none; + padding:($baseline/2) ($baseline); + text-align: center; + text-shadow: none; + font-weight: 500; + + &.disabled, &[disabled] { + background: $m-gray-d3; + } +} + +// blue primary button +.m-btn-primary-blue { + @extend .m-btn-primary; + box-shadow: 0 2px 1px 0 $m-blue-d4; + background: $m-blue-d3; + color: $white; + + &:hover, &:active { + background: $m-blue-d1; + } + + &.current, &.active { + box-shadow: inset 0 2px 1px 1px $m-blue-d2; + background: $m-blue; + color: $m-blue-d2; + + &:hover, &:active { + box-shadow: inset 0 2px 1px 1px $m-blue-d3; + color: $m-blue-d3; + } + } + + &.disabled, &[disabled] { + box-shadow: none; + } +} + +// pink primary button +.m-btn-primary-pink { + @extend .m-btn-primary; + box-shadow: 0 2px 1px 0 $m-pink-d2; + background: $m-pink; + color: $white; + + &:hover, &:active { + background: $m-pink-l3; + } + + &.current, &.active { + box-shadow: inset 0 2px 1px 1px $m-pink-d1; + background: $m-pink-l2; + color: $m-pink-d1; + + &:hover, &:active { + box-shadow: inset 0 2px 1px 1px $m-pink-d2; + color: $m-pink-d3; + } + } + + &.disabled, &[disabled] { + box-shadow: none; + } +} + + +// ==================== + +// edx.org marketing site - needed, but bad overrides with importants +.view-register, .view-login, .view-passwordreset { + + .form-actions button[type="submit"] { + text-transform: none; + vertical-align: middle; + font-weight: 600 !important; + letter-spacing: 0 !important; + } +} + + + + +// ==================== + +// edx.org marketing site - registration iframe band-aid (poor form enough to isolate out) .view-partial-mktgregister { - background: transparent; // dimensions needed for course about page on marketing site .wrapper-view { overflow: hidden; } - // button elements - not a better place to put these, sadly - .btn { - @include box-sizing('border-box'); - display: block; - padding: $baseline/2; - text-transform: lowercase; - color: $white; - letter-spacing: 0.1rem; - cursor: pointer; - text-align: center; - border: none !important; - text-decoration: none; - text-shadow: none; - letter-spacing: 0.1rem; - font-size: 17px; - font-weight: 300; - box-shadow: 0 !important; - - strong { - font-weight: 400; - text-transform: none; - } - } - - .btn-primary { - @extend .btn; - @include linear-gradient($m-blue-s1 5%, $m-blue-d1 95%); - - // no hover state conventions to follow from marketing :/ - &:hover, &:active { - - } - } - - .btn-secondary { - @extend .btn; - @include linear-gradient($m-gray 5%, $m-gray-d1 95%); - - // no hover state conventions to follow from marketing :/ - &:hover, &:active { - - } - } - - .btn-tertiary { - @extend .btn; - background: $m-blue-l1; - color: $m-blue; - - // no hover state conventions to follow from marketing :/ - &:hover, &:active { - - } - } - // nav list .list-actions { list-style: none; @@ -78,31 +142,37 @@ } .action { + @include font-size(16); + font-weight: 500; // register or access courseware &.action-register, &.access-courseware { - @extend .btn-primary; + @extend .m-btn-primary-blue; + display: block; } // already registered but course not started or registration is closed &.is-registered, &.registration-closed { - @extend .btn-secondary; + @extend .m-btn-primary-pink; pointer-events: none !important; + display: block; } // coming soon &.coming-soon { - @extend .btn-tertiary; + @extend .m-btn-primary-pink; pointer-events: none !important; outline: none; + display: block; } } } -//-------------------------------------- -// The Following is to enable themes to -// display H1s on login and register pages -//-------------------------------------- + +// ==================== + + +// The Following is to enable themes to display H1s on login and register pages .view-login .introduction header h1, .view-register .introduction header h1 { @include login_register_h1_style; @@ -110,4 +180,4 @@ footer .references { @include footer_references_style; -} \ No newline at end of file +} diff --git a/lms/static/sass/multicourse/_account.scss b/lms/static/sass/multicourse/_account.scss index 9de0a1fca8..eeda917d46 100644 --- a/lms/static/sass/multicourse/_account.scss +++ b/lms/static/sass/multicourse/_account.scss @@ -37,7 +37,7 @@ font-weight: 600; text-transform: uppercase; letter-spacing: 0 !important; - color: saturate($link-color-d1,15%); + color: $m-gray-d2; } .heading-5 { @@ -390,7 +390,11 @@ @include clearfix(); button[type="submit"] { - @extend .button-primary; + @extend .m-btn-primary-blue; + // needed, but bad overrides + text-transform: none; + vertical-align: middle; + font-weight: 600; &:disabled, &.is-disabled { opacity: 0.3; @@ -431,7 +435,6 @@ margin: 0 0 ($baseline/4) 0; font-size: em(14); font-weight: 600; - color: $m-gray-d2 !important; } .message-copy { diff --git a/lms/static/sass/shared/_header.scss b/lms/static/sass/shared/_header.scss index 7da89ccc1c..90aa7c6fa6 100644 --- a/lms/static/sass/shared/_header.scss +++ b/lms/static/sass/shared/_header.scss @@ -278,26 +278,8 @@ header.global { li { display: inline-block; - a { - border-radius: 0; - @include linear-gradient(saturate($link-color-d1,15%) 5%, shade($link-color-d1,15%) 95%); - display: inline-block; - padding: $baseline/2 $baseline*2.5; - text-transform: lowercase; - color: $very-light-text; - letter-spacing: 0.1rem; - font-weight: 300; - cursor: pointer; - text-align: center; - border: none !important; - text-shadow: none; - letter-spacing: 0.1rem; - font-size: 14px; - box-shadow: none !important; - - &:hover { - text-decoration: none; - } + .cta { + @extend .m-btn-primary-blue; } } From cc95d004b5df0b06db35b5a4727744b1e3729f34 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 24 Jul 2013 11:39:40 -0400 Subject: [PATCH 003/112] edx.org: handles letter-spacing on buttons --- lms/static/sass/multicourse/_account.scss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lms/static/sass/multicourse/_account.scss b/lms/static/sass/multicourse/_account.scss index eeda917d46..0daaf8c0ca 100644 --- a/lms/static/sass/multicourse/_account.scss +++ b/lms/static/sass/multicourse/_account.scss @@ -391,10 +391,6 @@ button[type="submit"] { @extend .m-btn-primary-blue; - // needed, but bad overrides - text-transform: none; - vertical-align: middle; - font-weight: 600; &:disabled, &.is-disabled { opacity: 0.3; From 3094471efc039b3a17a3ed52fe63a53cf6244656 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 24 Jul 2013 11:40:37 -0400 Subject: [PATCH 004/112] edx.org: revises iframe-based register/status buttons to match revised styles --- lms/static/sass/_shame.scss | 11 +++++++++-- lms/templates/courseware/mktg_course_about.html | 2 +- lms/templates/mktg_iframe.html | 9 +-------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lms/static/sass/_shame.scss b/lms/static/sass/_shame.scss index 041c42a66f..f67683afe4 100644 --- a/lms/static/sass/_shame.scss +++ b/lms/static/sass/_shame.scss @@ -43,9 +43,14 @@ text-align: center; text-shadow: none; font-weight: 500; + letter-spacing: 0; - &.disabled, &[disabled] { + &.disabled, &[disabled], &.is-disabled { background: $m-gray-d3; + + &:hover { + background: $m-gray-d3 !important; // needed for IE currently + } } } @@ -73,6 +78,7 @@ &.disabled, &[disabled] { box-shadow: none; + background: $m-gray-d3; // needed for IE currently } } @@ -100,6 +106,7 @@ &.disabled, &[disabled] { box-shadow: none; + background: $m-gray-d3; // needed for IE currently } } @@ -142,7 +149,7 @@ } .action { - @include font-size(16); + font-size: 16px; font-weight: 500; // register or access courseware diff --git a/lms/templates/courseware/mktg_course_about.html b/lms/templates/courseware/mktg_course_about.html index dc667c850c..8dedc70337 100644 --- a/lms/templates/courseware/mktg_course_about.html +++ b/lms/templates/courseware/mktg_course_about.html @@ -53,7 +53,7 @@ %elif allow_registration: Register for ${course.number} %else: -
Registration Is Closed
+
Registration Is Closed
%endif diff --git a/lms/templates/mktg_iframe.html b/lms/templates/mktg_iframe.html index 6d02f3fcc5..abaf466785 100644 --- a/lms/templates/mktg_iframe.html +++ b/lms/templates/mktg_iframe.html @@ -27,14 +27,7 @@ From 95478a3499c8f95d14f887f7f4007a7b326dbe31 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 24 Jul 2013 11:52:25 -0400 Subject: [PATCH 005/112] edx.org: revising base link color variable to use darker value for accessibility --- lms/static/sass/base/_variables.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/static/sass/base/_variables.scss b/lms/static/sass/base/_variables.scss index b7dd620f8a..7c209705e8 100644 --- a/lms/static/sass/base/_variables.scss +++ b/lms/static/sass/base/_variables.scss @@ -118,7 +118,7 @@ $border-color-3: rgb(100,100,100); $border-color-4: rgb(252,252,252); $link-color: $blue; -$link-color-d1: $m-blue; +$link-color-d1: $m-blue-d2; $link-hover: $pink; $site-status-color: $pink; From d2eca92f93083d79d80c0c92bb2e1ccd8d4d39b4 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 24 Jul 2013 14:56:27 -0400 Subject: [PATCH 006/112] edx.org: abstracting new edx.org button extends and colors with other themes in mind --- lms/static/sass/_shame.scss | 64 +++++++++++------------ lms/static/sass/base/_variables.scss | 48 ++++++++++++++++- lms/static/sass/multicourse/_account.scss | 2 +- lms/static/sass/shared/_header.scss | 2 +- lms/templates/mktg_iframe.html | 3 +- 5 files changed, 82 insertions(+), 37 deletions(-) diff --git a/lms/static/sass/_shame.scss b/lms/static/sass/_shame.scss index f67683afe4..8348a774d4 100644 --- a/lms/static/sass/_shame.scss +++ b/lms/static/sass/_shame.scss @@ -35,7 +35,7 @@ } // primary button -.m-btn-primary { +.m-btn-base { @extend .m-btn; @extend .m-btn-edged; border: none; @@ -46,67 +46,67 @@ letter-spacing: 0; &.disabled, &[disabled], &.is-disabled { - background: $m-gray-d3; + background: $action-primary-disabled-bg; &:hover { - background: $m-gray-d3 !important; // needed for IE currently + background: $action-primary-disabled-bg !important; // needed for IE currently } } } -// blue primary button -.m-btn-primary-blue { - @extend .m-btn-primary; - box-shadow: 0 2px 1px 0 $m-blue-d4; - background: $m-blue-d3; - color: $white; +// primary button +.m-btn-primary { + @extend .m-btn-base; + box-shadow: 0 2px 1px 0 $action-primary-shadow; + background: $action-primary-bg; + color: $action-primary-fg; &:hover, &:active { - background: $m-blue-d1; + background: $action-primary-focused-bg; } &.current, &.active { - box-shadow: inset 0 2px 1px 1px $m-blue-d2; - background: $m-blue; - color: $m-blue-d2; + box-shadow: inset 0 2px 1px 1px $action-primary-active-shadow; + background: $action-primary-active-bg; + color: $action-primary-active-fg; &:hover, &:active { - box-shadow: inset 0 2px 1px 1px $m-blue-d3; - color: $m-blue-d3; + box-shadow: inset 0 2px 1px 1px $action-primary-active-focused-shadow; + color: $action-primary-active-focused-fg; } } &.disabled, &[disabled] { box-shadow: none; - background: $m-gray-d3; // needed for IE currently + background: $action-primary-disabled-bg; // needed for IE currently } } -// pink primary button -.m-btn-primary-pink { - @extend .m-btn-primary; - box-shadow: 0 2px 1px 0 $m-pink-d2; - background: $m-pink; - color: $white; +// secondary button +.m-btn-secondary { + @extend .m-btn-base; + box-shadow: 0 2px 1px 0 $action-secondary-shadow; + background: $action-secondary-bg; + color: $action-secondary-fg; &:hover, &:active { - background: $m-pink-l3; + background: $action-secondary-focused-bg; } &.current, &.active { - box-shadow: inset 0 2px 1px 1px $m-pink-d1; - background: $m-pink-l2; - color: $m-pink-d1; + box-shadow: inset 0 2px 1px 1px $action-secondary-active-shadow; + background: $action-secondary-active-bg; + color: $action-secondary-active-fg; &:hover, &:active { - box-shadow: inset 0 2px 1px 1px $m-pink-d2; - color: $m-pink-d3; + box-shadow: inset 0 2px 1px 1px $action-secondary-active-focused-shadow; + color: $action-secondary-active-focused-fg; } } &.disabled, &[disabled] { box-shadow: none; - background: $m-gray-d3; // needed for IE currently + background: $action-secondary-disabled-bg; // needed for IE currently } } @@ -154,20 +154,20 @@ // register or access courseware &.action-register, &.access-courseware { - @extend .m-btn-primary-blue; + @extend .m-btn-primary; display: block; } // already registered but course not started or registration is closed &.is-registered, &.registration-closed { - @extend .m-btn-primary-pink; + @extend .m-btn-secondary; pointer-events: none !important; display: block; } // coming soon &.coming-soon { - @extend .m-btn-primary-pink; + @extend .m-btn-secondary; pointer-events: none !important; outline: none; display: block; diff --git a/lms/static/sass/base/_variables.scss b/lms/static/sass/base/_variables.scss index 7c209705e8..48c62bbb4c 100644 --- a/lms/static/sass/base/_variables.scss +++ b/lms/static/sass/base/_variables.scss @@ -69,7 +69,6 @@ $m-pink-d3: #771C44; $m-base-font-size: em(15); - $base-font-color: rgb(60,60,60); $baseFontColor: rgb(60,60,60); $lighter-base-font-color: rgb(100,100,100); @@ -88,10 +87,57 @@ $courseware-footer-border: none; $courseware-footer-shadow: none; $courseware-footer-margin: 0px; + +// actions $button-bg-image: linear-gradient(#fff 0%, rgb(250,250,250) 50%, rgb(237,237,237) 50%, rgb(220,220,220) 100%); $button-bg-color: transparent; $button-bg-hover-color: #fff; +// actions - primary +$action-primary-bg: $m-blue-d3; +$action-primary-fg: $white; +$action-primary-shadow: $m-blue-d4; + +// focused - hover/active pseudo states +$action-primary-focused-bg: $m-blue-d1; +$action-primary-focused-fg: $white; + +// current or active navigation item +$action-primary-active-bg: $m-blue; +$action-primary-active-fg: $m-blue-d3; +$action-primary-active-shadow: $m-blue-d2; +$action-primary-active-focused-fg: $m-blue-d4; +$action-primary-active-focused-shadow: $m-blue-d3; + +// disabled +$action-primary-disabled-bg: $m-gray-d3; +$action-prmary-disabled-fg: $white; + + + +// actions - secondary +$action-secondary-bg: $m-pink; +$action-secondary-fg: $white; +$action-secondary-shadow: $m-pink-d2; + +// focused - hover/active pseudo states +$action-secondary-focused-bg: $m-pink-l3; +$action-secondary-focused-fg: $white; + +// current or active navigation item +$action-secondary-active-bg: $m-pink-l2; +$action-secondary-active-fg: $m-pink-d1; +$action-secondary-active-shadow: $m-pink-d1; +$action-secondary-active-focused-fg: $m-pink-d3; +$action-secondary-active-focused-shadow: $m-pink-d2; + +// disabled +$action-secondary-disabled-bg: $m-gray-d3; +$action-secondary-disabled-fg: $white; + + + + $faded-hr-image-1: linear-gradient(180deg, rgba(200,200,200, 0) 0%, rgba(200,200,200, 1) 50%, rgba(200,200,200, 0)); $faded-hr-image-2: linear-gradient(180deg, rgba(200,200,200, 0) 0%, rgba(200,200,200, 1)); $faded-hr-image-3: linear-gradient(180deg, rgba(200,200,200, 1) 0%, rgba(200,200,200, 0)); diff --git a/lms/static/sass/multicourse/_account.scss b/lms/static/sass/multicourse/_account.scss index 0daaf8c0ca..2ec9f50dba 100644 --- a/lms/static/sass/multicourse/_account.scss +++ b/lms/static/sass/multicourse/_account.scss @@ -390,7 +390,7 @@ @include clearfix(); button[type="submit"] { - @extend .m-btn-primary-blue; + @extend .m-btn-primary; &:disabled, &.is-disabled { opacity: 0.3; diff --git a/lms/static/sass/shared/_header.scss b/lms/static/sass/shared/_header.scss index 90aa7c6fa6..3f2daccf52 100644 --- a/lms/static/sass/shared/_header.scss +++ b/lms/static/sass/shared/_header.scss @@ -279,7 +279,7 @@ header.global { display: inline-block; .cta { - @extend .m-btn-primary-blue; + @extend .m-btn-primary; } } diff --git a/lms/templates/mktg_iframe.html b/lms/templates/mktg_iframe.html index abaf466785..97a23d0e5f 100644 --- a/lms/templates/mktg_iframe.html +++ b/lms/templates/mktg_iframe.html @@ -27,8 +27,7 @@ From 57bc010a0a86655591c215ebb684e865e741865f Mon Sep 17 00:00:00 2001 From: Vasyl Nakvasiuk Date: Thu, 25 Jul 2013 16:37:25 +0300 Subject: [PATCH 007/112] gst integration into studio --- .../contentstore/views/component.py | 7 +- .../lib/xmodule/xmodule/css/gst/display.scss | 44 ++++++ common/lib/xmodule/xmodule/gst_module.py | 131 ++++++++++-------- .../lib/xmodule/xmodule/tests/test_import.py | 2 +- common/templates/courseware_vendor_js.html | 1 + 5 files changed, 123 insertions(+), 62 deletions(-) create mode 100644 common/lib/xmodule/xmodule/css/gst/display.scss diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index 1be6ac2822..8b8c289da3 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -46,7 +46,12 @@ COMPONENT_TYPES = ['discussion', 'html', 'problem', 'video'] OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"] NOTE_COMPONENT_TYPES = ['notes'] -ADVANCED_COMPONENT_TYPES = ['annotatable', 'word_cloud', 'videoalpha'] + OPEN_ENDED_COMPONENT_TYPES + NOTE_COMPONENT_TYPES +ADVANCED_COMPONENT_TYPES = [ + 'annotatable', + 'word_cloud', + 'videoalpha', + 'graphical_slider_tool' +] + OPEN_ENDED_COMPONENT_TYPES + NOTE_COMPONENT_TYPES ADVANCED_COMPONENT_CATEGORY = 'advanced' ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules' diff --git a/common/lib/xmodule/xmodule/css/gst/display.scss b/common/lib/xmodule/xmodule/css/gst/display.scss new file mode 100644 index 0000000000..83ca4650f0 --- /dev/null +++ b/common/lib/xmodule/xmodule/css/gst/display.scss @@ -0,0 +1,44 @@ +// In the LMS sliders use built-in styles from jquery-ui-1.8.22.custom.css. +// CMS uses its own sliders styles. +// These styles we use only to sure, that slider in GST module +// will be render correctly (just like a duplication some from jquery-ui-1.8.22.custom.css). +// Cause, for example, CMS overwrites many jquery-ui-1.8.22.custom.css styles, +// and we must overwrite them again. + +.ui-widget-content { + border: 1px solid #dddddd; + color: #333333; +} + +.ui-widget { + font-family: Trebuchet MS, Tahoma, Verdana, Arial, sans-serif; + font-size: 1.1em; +} + +.ui-corner-all, .ui-corner-top, .ui-corner-left, .ui-corner-tl { + -moz-border-radius-topleft: 4px; + -webkit-border-top-left-radius: 4px; + -khtml-border-top-left-radius: 4px; + border-top-left-radius: 4px; +} + +.ui-corner-all, .ui-corner-top, .ui-corner-right, .ui-corner-tr { + -moz-border-radius-topright: 4px; + -webkit-border-top-right-radius: 4px; + -khtml-border-top-right-radius: 4px; + border-top-right-radius: 4px; +} + +.ui-corner-all, .ui-corner-bottom, .ui-corner-left, .ui-corner-bl { + -moz-border-radius-bottomleft: 4px; + -webkit-border-bottom-left-radius: 4px; + -khtml-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; +} + +.ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-br { + -moz-border-radius-bottomright: 4px; + -webkit-border-bottom-right-radius: 4px; + -khtml-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; +} \ No newline at end of file diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py index 5c902f48c2..80fafe4dc2 100644 --- a/common/lib/xmodule/xmodule/gst_module.py +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -9,17 +9,16 @@ from lxml import etree from lxml import html import xmltodict -from xmodule.mako_module import MakoModuleDescriptor +from xmodule.editing_module import XMLEditingDescriptor from xmodule.xml_module import XmlDescriptor from xmodule.x_module import XModule from xmodule.stringify import stringify_children from pkg_resources import resource_string from xblock.core import String, Scope - log = logging.getLogger(__name__) -DEFAULT_RENDER=""" +DEFAULT_RENDER = """

Graphic slider tool: Dynamic range and implicit functions.

You can make the range of the x axis (but not ticks of x axis) of @@ -33,13 +32,19 @@ DEFAULT_RENDER=""" """ -DEFAULT_CONFIGURATION=""" + +DEFAULT_CONFIGURATION = """ Math.sqrt(r * r - x * x) -Math.sqrt(r * r - x * x) + Math.sqrt(r * r / 20 - Math.pow(x-r/2.5, 2)) + r/8 + -Math.sqrt(r * r / 20 - Math.pow(x-r/2.5, 2)) + r/5.5 + Math.sqrt(r * r / 20 - Math.pow(x+r/2.5, 2)) + r/8 + -Math.sqrt(r * r / 20 - Math.pow(x+r/2.5, 2)) + r/5.5 + -Math.sqrt(r * r / 5 - x * x) - r/5.5 @@ -54,10 +59,13 @@ DEFAULT_CONFIGURATION=""" """ - class GraphicalSliderToolFields(object): - render = String(scope=Scope.content, default=DEFAULT_RENDER) - configuration = String(scope=Scope.content, default=DEFAULT_CONFIGURATION) + data = String( + help="Html contents to display for this module", + default='{}{}'.format( + DEFAULT_RENDER, DEFAULT_CONFIGURATION), + scope=Scope.content + ) class GraphicalSliderToolModule(GraphicalSliderToolFields, XModule): @@ -65,23 +73,23 @@ class GraphicalSliderToolModule(GraphicalSliderToolFields, XModule): ''' js = { - 'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee')], - 'js': [ - # 3rd party libraries used by graphic slider tool. - # TODO - where to store them - outside xmodule? - resource_string(__name__, 'js/src/graphical_slider_tool/gst_main.js'), - resource_string(__name__, 'js/src/graphical_slider_tool/state.js'), - resource_string(__name__, 'js/src/graphical_slider_tool/logme.js'), - resource_string(__name__, 'js/src/graphical_slider_tool/general_methods.js'), - resource_string(__name__, 'js/src/graphical_slider_tool/sliders.js'), - resource_string(__name__, 'js/src/graphical_slider_tool/inputs.js'), - resource_string(__name__, 'js/src/graphical_slider_tool/graph.js'), - resource_string(__name__, 'js/src/graphical_slider_tool/el_output.js'), - resource_string(__name__, 'js/src/graphical_slider_tool/g_label_el_output.js'), - resource_string(__name__, 'js/src/graphical_slider_tool/gst.js') - - ] + 'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee')], + 'js': [ + # 3rd party libraries used by graphic slider tool. + # TODO - where to store them - outside xmodule? + resource_string(__name__, 'js/src/graphical_slider_tool/gst_main.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/state.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/logme.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/general_methods.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/sliders.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/inputs.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/graph.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/el_output.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/g_label_el_output.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/gst.js') + ] } + css = {'scss': [resource_string(__name__, 'css/gst/display.scss')]} js_module_name = "GraphicalSliderTool" def get_html(self): @@ -90,15 +98,23 @@ class GraphicalSliderToolModule(GraphicalSliderToolFields, XModule): # these 3 will be used in class methods self.html_id = self.location.html_id() self.html_class = self.location.category + + self.configuration = html.fromstring(self.data).xpath('configuration')[0] + self.configuration = stringify_children(self.configuration) + + self.render = html.fromstring(self.data).xpath('render')[0] + self.render = stringify_children(self.render) + self.configuration_json = self.build_configuration_json() params = { - 'gst_html': self.substitute_controls(self.render), - 'element_id': self.html_id, - 'element_class': self.html_class, - 'configuration_json': self.configuration_json - } + 'gst_html': self.substitute_controls(self.render), + 'element_id': self.html_id, + 'element_class': self.html_class, + 'configuration_json': self.configuration_json + } content = self.system.render_template( - 'graphical_slider_tool.html', params) + 'graphical_slider_tool.html', params + ) return content def substitute_controls(self, html_string): @@ -126,9 +142,10 @@ class GraphicalSliderToolModule(GraphicalSliderToolFields, XModule): if plot_el: plot_el = plot_el[0] plot_el.getparent().replace(plot_el, html.fromstring( - plot_div.format(element_class=self.html_class, - element_id=self.html_id, - style=plot_el.get('style', "")))) + plot_div.format( + element_class=self.html_class, + element_id=self.html_id, + style=plot_el.get('style', "")))) # substitute sliders slider_div = '

added for interface compatibility with xmltodict.parse # class added for javascript's part purposes - return json.dumps(xmltodict.parse('' + self.configuration + '')) + root = '{}'.format( + self.html_class, + self.configuration) + return json.dumps(xmltodict.parse(root)) -class GraphicalSliderToolDescriptor(GraphicalSliderToolFields, MakoModuleDescriptor, XmlDescriptor): +class GraphicalSliderToolDescriptor(GraphicalSliderToolFields, XMLEditingDescriptor, XmlDescriptor): module_class = GraphicalSliderToolModule @classmethod @@ -202,24 +223,14 @@ class GraphicalSliderToolDescriptor(GraphicalSliderToolFields, MakoModuleDescrip exactly one '{0}' tag".format(child)) # finished - def parse(k): - """Assumes that xml_object has child k""" - return stringify_children(xml_object.xpath(k)[0]) return { - 'render': parse('render'), - 'configuration': parse('configuration') - }, [] + 'data': stringify_children(xml_object) + }, [] def definition_to_xml(self, resource_fs): '''Return an xml element representing this definition.''' - xml_object = etree.Element('graphical_slider_tool') - - def add_child(k): - child_str = '<{tag}>{body}'.format(tag=k, body=getattr(self, k)) - child_node = etree.fromstring(child_str) - xml_object.append(child_node) - - for child in ['render', 'configuration']: - add_child(child) - + data = '<{tag}>{body}'.format( + tag='graphical_slider_tool', + body=self.data) + xml_object = etree.fromstring(data) return xml_object diff --git a/common/lib/xmodule/xmodule/tests/test_import.py b/common/lib/xmodule/xmodule/tests/test_import.py index 2fe9d70627..fb1bed2d3a 100644 --- a/common/lib/xmodule/xmodule/tests/test_import.py +++ b/common/lib/xmodule/xmodule/tests/test_import.py @@ -445,7 +445,7 @@ class ImportTestCase(BaseCourseTestCase): render_string_from_sample_gst_xml = """ \ """.strip() - self.assertEqual(gst_sample.render, render_string_from_sample_gst_xml) + self.assertIn(render_string_from_sample_gst_xml, gst_sample.data) def test_word_cloud_import(self): modulestore = XMLModuleStore(DATA_DIR, course_dirs=['word_cloud']) diff --git a/common/templates/courseware_vendor_js.html b/common/templates/courseware_vendor_js.html index 84e682ddac..20466210b8 100644 --- a/common/templates/courseware_vendor_js.html +++ b/common/templates/courseware_vendor_js.html @@ -8,6 +8,7 @@ + ## codemirror From c240f6597d32b01093236cd0b49ee48060bb81dc Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 22 Jul 2013 13:26:08 -0400 Subject: [PATCH 008/112] Reformat JS --- cms/templates/manage_users.html | 89 ++++++++++++++++----------------- 1 file changed, 43 insertions(+), 46 deletions(-) diff --git a/cms/templates/manage_users.html b/cms/templates/manage_users.html index 22d57be41d..32d6522686 100644 --- a/cms/templates/manage_users.html +++ b/cms/templates/manage_users.html @@ -67,57 +67,51 @@ <%block name="jsextra"> From 21a32370df88195a07a360d84c12995459cffcad Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 22 Jul 2013 15:05:09 -0400 Subject: [PATCH 009/112] Reorganize URLs and views around course team Match other views better, saner URLs, more RESTful style, extensible for other roles --- cms/djangoapps/contentstore/views/user.py | 126 ++++++++++------------ cms/templates/manage_users.html | 32 ++++-- cms/templates/widgets/header.html | 2 +- cms/urls.py | 12 +-- 4 files changed, 83 insertions(+), 89 deletions(-) diff --git a/cms/djangoapps/contentstore/views/user.py b/cms/djangoapps/contentstore/views/user.py index 3a18448118..47dc35fcb4 100644 --- a/cms/djangoapps/contentstore/views/user.py +++ b/cms/djangoapps/contentstore/views/user.py @@ -1,7 +1,9 @@ +import json from django.conf import settings from django.core.exceptions import PermissionDenied -from django.core.urlresolvers import reverse +from django.contrib.auth.models import User from django.contrib.auth.decorators import login_required +from django.views.decorators.http import require_http_methods from django.utils.translation import ugettext as _ from django.views.decorators.http import require_POST from django_future.csrf import ensure_csrf_cookie @@ -10,9 +12,9 @@ from django.core.context_processors import csrf from xmodule.modulestore.django import modulestore from contentstore.utils import get_url_reverse, get_lms_link_for_item -from util.json_request import expect_json, JsonResponse +from util.json_request import JsonResponse from auth.authz import STAFF_ROLE_NAME, INSTRUCTOR_ROLE_NAME, get_users_in_course_group_by_role -from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group +from auth.authz import add_user_to_course_group, remove_user_from_course_group from course_creators.views import get_course_creator_status, add_user_with_status_unrequested, user_requested_access from .access import has_access @@ -60,10 +62,11 @@ def request_course_creator(request): @login_required @ensure_csrf_cookie -def manage_users(request, location): +def manage_users(request, org, course, name): ''' This view will return all CMS users who are editors for the specified course ''' + location = Location('i4x', org, course, 'course', name) # check that logged in user has permissions to this item if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME) and not has_access(request.user, location, role=STAFF_ROLE_NAME): raise PermissionDenied() @@ -73,91 +76,72 @@ def manage_users(request, location): return render_to_response('manage_users.html', { 'context_course': course_module, 'staff': get_users_in_course_group_by_role(location, STAFF_ROLE_NAME), - 'add_user_postback_url': reverse('add_user', args=[location]).rstrip('/'), - 'remove_user_postback_url': reverse('remove_user', args=[location]).rstrip('/'), 'allow_actions': has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME), - 'request_user_id': request.user.id }) -@expect_json @login_required @ensure_csrf_cookie -def add_user(request, location): - ''' - This POST-back view will add a user - specified by email - to the list of editors for - the specified course - ''' - email = request.POST.get("email") - - if not email: - msg = { - 'Status': 'Failed', - 'ErrMsg': _('Please specify an email address.'), - } - return JsonResponse(msg, 400) - - # remove leading/trailing whitespace if necessary - email = email.strip() - - # check that logged in user has admin permissions to this course - if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME): +@require_http_methods(("GET", "POST", "PUT", "DELETE")) +def course_team_user(request, org, course, name, email): + location = Location('i4x', org, course, 'course', name) + # check that logged in user has permissions to this item + if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME) and not has_access(request.user, location, role=STAFF_ROLE_NAME): raise PermissionDenied() - user = get_user_by_email(email) - - # user doesn't exist?!? Return error. - if user is None: + try: + user = User.objects.get(email=email) + except: msg = { - 'Status': 'Failed', - 'ErrMsg': _("Could not find user by email address '{email}'.").format(email=email), + "error": _("Could not find user by email address '{email}'.").format(email=email), } return JsonResponse(msg, 404) - # user exists, but hasn't activated account?!? + if request.method == "GET": + # just return info about the user + roles = set() + for group in user.groups.all(): + if not "_" in group.name: + continue + role, coursename = group.name.split("_", 1) + if coursename in (location.course, location.course_id): + roles.add(role) + msg = { + "email": user.email, + "active": user.is_active, + "roles": list(roles), + } + return JsonResponse(msg) + + # can't modify an inactive user if not user.is_active: msg = { - 'Status': 'Failed', - 'ErrMsg': _('User {email} has registered but has not yet activated his/her account.').format(email=email), + "error": _('User {email} has registered but has not yet activated his/her account.').format(email=email), } return JsonResponse(msg, 400) - # ok, we're cool to add to the course group - add_user_to_course_group(request.user, user, location, STAFF_ROLE_NAME) + # all other operations require the requesting user to specify a role -- + # or if no role is specified, default to "staff" + if "role" in request.POST: + role = request.POST["role"] + elif request.body: + try: + payload = json.loads(request.body) + except: + return JsonResponse({"error": _("malformed JSON")}, 400) + try: + role = payload["role"] + except KeyError: + return JsonResponse({"error": "`role` is required"}, 400) + else: + role = STAFF_ROLE_NAME - return JsonResponse({"Status": "OK"}) - - -@expect_json -@login_required -@ensure_csrf_cookie -def remove_user(request, location): - ''' - This POST-back view will remove a user - specified by email - from the list of editors for - the specified course - ''' - - email = request.POST["email"] - - # check that logged in user has admin permissions on this course - if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME): - raise PermissionDenied() - - user = get_user_by_email(email) - if user is None: - msg = { - 'Status': 'Failed', - 'ErrMsg': _("Could not find user by email address '{email}'.").format(email=email), - } - return JsonResponse(msg, 404) - - # make sure we're not removing ourselves - if user.id == request.user.id: - raise PermissionDenied() - - remove_user_from_course_group(request.user, user, location, STAFF_ROLE_NAME) - - return JsonResponse({"Status": "OK"}) + if request.method in ("POST", "PUT"): + add_user_to_course_group(request.user, user, location, role) + return JsonResponse() + elif request.method == "DELETE": + remove_user_from_course_group(request.user, user, location, role) + return JsonResponse() def _get_course_creator_status(user): diff --git a/cms/templates/manage_users.html b/cms/templates/manage_users.html index 32d6522686..45b4651983 100644 --- a/cms/templates/manage_users.html +++ b/cms/templates/manage_users.html @@ -1,4 +1,5 @@ <%! from django.utils.translation import ugettext as _ %> +<%! from django.core.urlresolvers import reverse %> <%inherit file="base.html" /> <%block name="title">${_("Course Team Settings")} <%block name="bodyclass">is-signedin course users settings team @@ -46,12 +47,17 @@
    % for user in staff: -
  1. +
  2. ${user.username} ${user.email} %if allow_actions :
    - %if request_user_id != user.id: + %if request.user.id != user.id: %endif
    @@ -67,20 +73,25 @@ <%block name="jsextra"> From 6a9074e1851fea87b0a5edd0b077659575c5574a Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Tue, 23 Jul 2013 10:17:16 -0400 Subject: [PATCH 011/112] Removed `get_url_reverse` function It was causing unit tests to fail, and it's a needless bit of abstraction that never should have existed in the first place. --- .../contentstore/tests/test_checklists.py | 8 +++- .../contentstore/tests/test_utils.py | 44 ------------------- cms/djangoapps/contentstore/utils.py | 32 -------------- cms/djangoapps/contentstore/views/assets.py | 7 ++- .../contentstore/views/checklist.py | 17 ++++++- cms/djangoapps/contentstore/views/user.py | 22 +++++++--- 6 files changed, 43 insertions(+), 87 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_checklists.py b/cms/djangoapps/contentstore/tests/test_checklists.py index 02999f6567..6f8f102df8 100644 --- a/cms/djangoapps/contentstore/tests/test_checklists.py +++ b/cms/djangoapps/contentstore/tests/test_checklists.py @@ -1,5 +1,5 @@ """ Unit tests for checklist methods in views.py. """ -from contentstore.utils import get_modulestore, get_url_reverse +from contentstore.utils import get_modulestore from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.tests.factories import CourseFactory from django.core.urlresolvers import reverse @@ -38,7 +38,11 @@ class ChecklistTestCase(CourseTestCase): def test_get_checklists(self): """ Tests the get checklists method. """ - checklists_url = get_url_reverse('Checklists', self.course) + checklists_url = reverse("checklists", kwargs={ + 'org': self.course.location.org, + 'course': self.course.location.course, + 'name': self.course.location.name, + }) response = self.client.get(checklists_url) self.assertContains(response, "Getting Started With Studio") payload = response.content diff --git a/cms/djangoapps/contentstore/tests/test_utils.py b/cms/djangoapps/contentstore/tests/test_utils.py index fec82db1bb..26c49843b5 100644 --- a/cms/djangoapps/contentstore/tests/test_utils.py +++ b/cms/djangoapps/contentstore/tests/test_utils.py @@ -72,50 +72,6 @@ class LMSLinksTestCase(TestCase): ) -class UrlReverseTestCase(ModuleStoreTestCase): - """ Tests for get_url_reverse """ - def test_course_page_names(self): - """ Test the defined course pages. """ - course = CourseFactory.create(org='mitX', number='666', display_name='URL Reverse Course') - - self.assertEquals( - '/manage_users/i4x://mitX/666/course/URL_Reverse_Course', - utils.get_url_reverse('ManageUsers', course) - ) - - self.assertEquals( - '/mitX/666/settings-details/URL_Reverse_Course', - utils.get_url_reverse('SettingsDetails', course) - ) - - self.assertEquals( - '/mitX/666/settings-grading/URL_Reverse_Course', - utils.get_url_reverse('SettingsGrading', course) - ) - - self.assertEquals( - '/mitX/666/course/URL_Reverse_Course', - utils.get_url_reverse('CourseOutline', course) - ) - - self.assertEquals( - '/mitX/666/checklists/URL_Reverse_Course', - utils.get_url_reverse('Checklists', course) - ) - - def test_unknown_passes_through(self): - """ Test that unknown values pass through. """ - course = CourseFactory.create(org='mitX', number='666', display_name='URL Reverse Course') - self.assertEquals( - 'foobar', - utils.get_url_reverse('foobar', course) - ) - self.assertEquals( - 'https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about', - utils.get_url_reverse('https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about', course) - ) - - class ExtraPanelTabTestCase(TestCase): """ Tests adding and removing extra course tabs. """ diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 4973bddaca..a2e927ef46 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -188,38 +188,6 @@ def update_item(location, value): get_modulestore(location).update_item(location, value) -def get_url_reverse(course_page_name, course_module): - """ - Returns the course URL link to the specified location. This value is suitable to use as an href link. - - course_page_name should correspond to an attribute in CoursePageNames (for example, 'ManageUsers' - or 'SettingsDetails'), or else it will simply be returned. This method passes back unknown values of - course_page_names so that it can also be used for absolute (known) URLs. - - course_module is used to obtain the location, org, course, and name properties for a course, if - course_page_name corresponds to an attribute in CoursePageNames. - """ - url_name = getattr(CoursePageNames, course_page_name, None) - ctx_loc = course_module.location - - if CoursePageNames.ManageUsers == url_name: - return reverse(url_name, kwargs={"location": ctx_loc}) - elif url_name in [CoursePageNames.SettingsDetails, CoursePageNames.SettingsGrading, - CoursePageNames.CourseOutline, CoursePageNames.Checklists]: - return reverse(url_name, kwargs={'org': ctx_loc.org, 'course': ctx_loc.course, 'name': ctx_loc.name}) - else: - return course_page_name - - -class CoursePageNames: - """ Constants for pages that are recognized by get_url_reverse method. """ - ManageUsers = "manage_users" - SettingsDetails = "settings_details" - SettingsGrading = "settings_grading" - CourseOutline = "course_index" - Checklists = "checklists" - - def add_extra_panel_tab(tab_type, course): """ Used to add the panel tab to a course if it does not exist. diff --git a/cms/djangoapps/contentstore/views/assets.py b/cms/djangoapps/contentstore/views/assets.py index 0bb9551ac9..6d371bef18 100644 --- a/cms/djangoapps/contentstore/views/assets.py +++ b/cms/djangoapps/contentstore/views/assets.py @@ -29,7 +29,6 @@ from xmodule.util.date_utils import get_default_time_display from xmodule.modulestore import InvalidLocationError from xmodule.exceptions import NotFoundError -from ..utils import get_url_reverse from .access import get_location_and_verify_access from util.json_request import JsonResponse @@ -320,7 +319,11 @@ def import_course(request, org, course, name): return render_to_response('import.html', { 'context_course': course_module, - 'successful_import_redirect_url': get_url_reverse('CourseOutline', course_module) + 'successful_import_redirect_url': reverse('course_index', kwargs={ + 'org': location.org, + 'course': location.course, + 'name': location.name, + }) }) diff --git a/cms/djangoapps/contentstore/views/checklist.py b/cms/djangoapps/contentstore/views/checklist.py index bcf4a1a5b9..17f0a55565 100644 --- a/cms/djangoapps/contentstore/views/checklist.py +++ b/cms/djangoapps/contentstore/views/checklist.py @@ -4,12 +4,13 @@ from util.json_request import JsonResponse from django.http import HttpResponseBadRequest from django.contrib.auth.decorators import login_required from django.views.decorators.http import require_http_methods +from django.core.urlresolvers import reverse from django_future.csrf import ensure_csrf_cookie from mitxmako.shortcuts import render_to_response from xmodule.modulestore.inheritance import own_metadata -from ..utils import get_modulestore, get_url_reverse +from ..utils import get_modulestore from .access import get_location_and_verify_access from xmodule.course_module import CourseDescriptor @@ -96,10 +97,22 @@ def expand_checklist_action_urls(course_module): """ checklists = course_module.checklists modified = False + urlconf_map = { + "ManageUsers": "manage_users", + "SettingsDetails": "settings_details", + "SettingsGrading": "settings_grading", + "CourseOutline": "course_index", + "Checklists": "checklists", + } for checklist in checklists: if not checklist.get('action_urls_expanded', False): for item in checklist.get('items'): - item['action_url'] = get_url_reverse(item.get('action_url'), course_module) + urlconf_name = urlconf_map.get(item.get('action_url')) + item['action_url'] = reverse(urlconf_name, kwargs={ + 'org': course_module.location.org, + 'course': course_module.location.course, + 'name': course_module.location.name, + }) checklist['action_urls_expanded'] = True modified = True diff --git a/cms/djangoapps/contentstore/views/user.py b/cms/djangoapps/contentstore/views/user.py index ee4e4e435a..c5d642e207 100644 --- a/cms/djangoapps/contentstore/views/user.py +++ b/cms/djangoapps/contentstore/views/user.py @@ -1,6 +1,7 @@ import json from django.conf import settings from django.core.exceptions import PermissionDenied +from django.core.urlresolvers import reverse from django.contrib.auth.models import User, Group from django.contrib.auth.decorators import login_required from django.views.decorators.http import require_http_methods @@ -11,7 +12,7 @@ from mitxmako.shortcuts import render_to_response from django.core.context_processors import csrf from xmodule.modulestore.django import modulestore -from contentstore.utils import get_url_reverse, get_lms_link_for_item +from contentstore.utils import get_lms_link_for_item from util.json_request import JsonResponse from auth.authz import ( STAFF_ROLE_NAME, INSTRUCTOR_ROLE_NAME, get_users_in_course_group_by_role, @@ -40,11 +41,22 @@ def index(request): and course.location.name != '') courses = filter(course_filter, courses) + def format_course_for_view(course): + return ( + course.display_name, + reverse("course_index", kwargs={ + 'org': course.location.org, + 'course': course.location.course, + 'name': course.location.name, + }), + get_lms_link_for_item( + course.location, + course_id=course.location.course_id, + ), + ) + return render_to_response('index.html', { - 'courses': [(course.display_name, - get_url_reverse('CourseOutline', course), - get_lms_link_for_item(course.location, course_id=course.location.course_id)) - for course in courses], + 'courses': [format_course_for_view(c) for c in courses], 'user': request.user, 'request_course_creator_url': reverse('request_course_creator'), 'course_creator_status': _get_course_creator_status(request.user), From b835f7c3a3ef0aa452935db3ee9bbdf40be33e78 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Tue, 23 Jul 2013 10:38:45 -0400 Subject: [PATCH 012/112] Update a manage_user reverse call --- cms/templates/settings.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/templates/settings.html b/cms/templates/settings.html index 1dde9b6c0d..1f5d89b2b9 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -262,7 +262,7 @@ from contentstore import utils From 724ef2e1e5a8cff9656960f46b6e05dcb3317560 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Tue, 23 Jul 2013 11:10:47 -0400 Subject: [PATCH 013/112] Fixing test failures --- .../contentstore/tests/test_contentstore.py | 4 +- .../contentstore/tests/test_users.py | 195 ------------------ .../contentstore/views/checklist.py | 5 +- cms/templates/settings_advanced.html | 2 +- cms/templates/settings_graders.html | 2 +- 5 files changed, 9 insertions(+), 199 deletions(-) delete mode 100644 cms/djangoapps/contentstore/tests/test_users.py diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index a51110163d..0ba4c49874 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -1167,7 +1167,9 @@ class ContentStoreTest(ModuleStoreTestCase): # manage users resp = self.client.get(reverse('manage_users', - kwargs={'location': loc.url()})) + kwargs={'org': loc.org, + 'course': loc.course, + 'name': loc.name})) self.assertEqual(200, resp.status_code) # course info diff --git a/cms/djangoapps/contentstore/tests/test_users.py b/cms/djangoapps/contentstore/tests/test_users.py deleted file mode 100644 index 8fea4004dd..0000000000 --- a/cms/djangoapps/contentstore/tests/test_users.py +++ /dev/null @@ -1,195 +0,0 @@ -""" -Tests for user.py. -""" -import json -import mock -from .utils import CourseTestCase -from django.core.urlresolvers import reverse -from contentstore.views.user import _get_course_creator_status -from course_creators.views import add_user_with_status_granted -from course_creators.admin import CourseCreatorAdmin -from course_creators.models import CourseCreator - -from django.http import HttpRequest -from django.contrib.auth.models import User -from django.contrib.admin.sites import AdminSite - - -class UsersTestCase(CourseTestCase): - def setUp(self): - super(UsersTestCase, self).setUp() - self.url = reverse("add_user", kwargs={"location": ""}) - - def test_empty(self): - resp = self.client.post(self.url) - self.assertEqual(resp.status_code, 400) - content = json.loads(resp.content) - self.assertEqual(content["Status"], "Failed") - - -class IndexCourseCreatorTests(CourseTestCase): - """ - Tests the various permutations of course creator status. - """ - def setUp(self): - super(IndexCourseCreatorTests, self).setUp() - - self.index_url = reverse("index") - self.request_access_url = reverse("request_course_creator") - - # Disable course creation takes precedence over enable creator group. I have enabled the - # latter to make this clear. - self.disable_course_creation = { - "DISABLE_COURSE_CREATION": True, - "ENABLE_CREATOR_GROUP": True, - 'STUDIO_REQUEST_EMAIL': 'mark@marky.mark', - } - - self.enable_creator_group = {"ENABLE_CREATOR_GROUP": True} - - self.admin = User.objects.create_user('Mark', 'mark+courses@edx.org', 'foo') - self.admin.is_staff = True - - def test_get_course_creator_status_disable_creation(self): - # DISABLE_COURSE_CREATION is True (this is the case on edx, where we have a marketing site). - # Only edx staff can create courses. - with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.disable_course_creation): - self.assertTrue(self.user.is_staff) - self.assertEquals('granted', _get_course_creator_status(self.user)) - self._set_user_non_staff() - self.assertFalse(self.user.is_staff) - self.assertEquals('disallowed_for_this_site', _get_course_creator_status(self.user)) - - def test_get_course_creator_status_default_cause(self): - # Neither ENABLE_CREATOR_GROUP nor DISABLE_COURSE_CREATION are enabled. Anyone can create a course. - self.assertEquals('granted', _get_course_creator_status(self.user)) - self._set_user_non_staff() - self.assertEquals('granted', _get_course_creator_status(self.user)) - - def test_get_course_creator_status_creator_group(self): - # ENABLE_CREATOR_GROUP is True. This is the case on edge. - # Only staff members and users who have been granted access can create courses. - with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.enable_creator_group): - # Staff members can always create courses. - self.assertEquals('granted', _get_course_creator_status(self.user)) - # Non-staff must request access. - self._set_user_non_staff() - self.assertEquals('unrequested', _get_course_creator_status(self.user)) - # Staff user requests access. - self.client.post(self.request_access_url) - self.assertEquals('pending', _get_course_creator_status(self.user)) - - def test_get_course_creator_status_creator_group_granted(self): - # ENABLE_CREATOR_GROUP is True. This is the case on edge. - # Check return value for a non-staff user who has been granted access. - with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.enable_creator_group): - self._set_user_non_staff() - add_user_with_status_granted(self.admin, self.user) - self.assertEquals('granted', _get_course_creator_status(self.user)) - - def test_get_course_creator_status_creator_group_denied(self): - # ENABLE_CREATOR_GROUP is True. This is the case on edge. - # Check return value for a non-staff user who has been denied access. - with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.enable_creator_group): - self._set_user_non_staff() - self._set_user_denied() - self.assertEquals('denied', _get_course_creator_status(self.user)) - - def test_disable_course_creation_enabled_non_staff(self): - # Test index page content when DISABLE_COURSE_CREATION is True, non-staff member. - with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.disable_course_creation): - self._set_user_non_staff() - self._assert_cannot_create() - - def test_disable_course_creation_enabled_staff(self): - # Test index page content when DISABLE_COURSE_CREATION is True, staff member. - with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.disable_course_creation): - resp = self._assert_can_create() - self.assertFalse('Email staff to create course' in resp.content) - - def test_can_create_by_default(self): - # Test index page content with neither ENABLE_CREATOR_GROUP nor DISABLE_COURSE_CREATION enabled. - # Anyone can create a course. - self._assert_can_create() - self._set_user_non_staff() - self._assert_can_create() - - def test_course_creator_group_enabled(self): - # Test index page content with ENABLE_CREATOR_GROUP True. - # Staff can always create a course, others must request access. - with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.enable_creator_group): - # Staff members can always create courses. - self._assert_can_create() - - # Non-staff case. - self._set_user_non_staff() - resp = self._assert_cannot_create() - self.assertTrue(self.request_access_url in resp.content) - - # Now request access. - self.client.post(self.request_access_url) - - # Still cannot create a course, but the "request access button" is no longer there. - resp = self._assert_cannot_create() - self.assertFalse(self.request_access_url in resp.content) - self.assertTrue('has-status is-pending' in resp.content) - - def test_course_creator_group_granted(self): - # Test index page content with ENABLE_CREATOR_GROUP True, non-staff member with access granted. - with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.enable_creator_group): - self._set_user_non_staff() - add_user_with_status_granted(self.admin, self.user) - self._assert_can_create() - - def test_course_creator_group_denied(self): - # Test index page content with ENABLE_CREATOR_GROUP True, non-staff member with access denied. - with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.enable_creator_group): - self._set_user_non_staff() - self._set_user_denied() - resp = self._assert_cannot_create() - self.assertFalse(self.request_access_url in resp.content) - self.assertTrue('has-status is-denied' in resp.content) - - def _assert_can_create(self): - """ - Helper method that posts to the index page and checks that the user can create a course. - - Returns the response from the post. - """ - resp = self.client.post(self.index_url) - self.assertTrue('new-course-button' in resp.content) - self.assertFalse(self.request_access_url in resp.content) - self.assertFalse('Email staff to create course' in resp.content) - return resp - - def _assert_cannot_create(self): - """ - Helper method that posts to the index page and checks that the user cannot create a course. - - Returns the response from the post. - """ - resp = self.client.post(self.index_url) - self.assertFalse('new-course-button' in resp.content) - return resp - - def _set_user_non_staff(self): - """ - Sets user as non-staff. - """ - self.user.is_staff = False - self.user.save() - - def _set_user_denied(self): - """ - Sets course creator status to denied in admin table. - """ - self.table_entry = CourseCreator(user=self.user) - self.table_entry.save() - - self.deny_request = HttpRequest() - self.deny_request.user = self.admin - - self.creator_admin = CourseCreatorAdmin(self.table_entry, AdminSite()) - - self.table_entry.state = CourseCreator.DENIED - self.creator_admin.save_model(self.deny_request, self.table_entry, None, True) diff --git a/cms/djangoapps/contentstore/views/checklist.py b/cms/djangoapps/contentstore/views/checklist.py index 17f0a55565..74f0a33769 100644 --- a/cms/djangoapps/contentstore/views/checklist.py +++ b/cms/djangoapps/contentstore/views/checklist.py @@ -107,7 +107,10 @@ def expand_checklist_action_urls(course_module): for checklist in checklists: if not checklist.get('action_urls_expanded', False): for item in checklist.get('items'): - urlconf_name = urlconf_map.get(item.get('action_url')) + action_url = item.get('action_url') + if action_url not in urlconf_map: + continue + urlconf_name = urlconf_map[action_url] item['action_url'] = reverse(urlconf_name, kwargs={ 'org': course_module.location.org, 'course': course_module.location.course, diff --git a/cms/templates/settings_advanced.html b/cms/templates/settings_advanced.html index e1b1913c87..32f22712a8 100644 --- a/cms/templates/settings_advanced.html +++ b/cms/templates/settings_advanced.html @@ -96,7 +96,7 @@ editor.render(); % endif diff --git a/cms/templates/settings_graders.html b/cms/templates/settings_graders.html index d9040009cc..f3a4584a26 100644 --- a/cms/templates/settings_graders.html +++ b/cms/templates/settings_graders.html @@ -140,7 +140,7 @@ from contentstore import utils From 97a02d415f2789d317f5686a08020458514bda7d Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Tue, 23 Jul 2013 12:55:58 -0400 Subject: [PATCH 014/112] Make assertion failure message more understandable --- .../features/component_settings_editor_helpers.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py index 2b206e4466..8a8f6deb04 100644 --- a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py +++ b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py @@ -16,7 +16,11 @@ def create_component_instance(step, component_button_css, category, if has_multiple_templates: click_component_from_menu(category, boilerplate, expected_css) - assert_equal(1, len(world.css_find(expected_css))) + assert_equal( + 1, + len(world.css_find(expected_css)), + "Component instance with css {css} was not created successfully".format(css=expected_css)) + @world.absorb def click_new_component_button(step, component_button_css): From f438552b3b9dd13ad92ffec34971a7963915a91a Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Tue, 23 Jul 2013 15:27:33 -0400 Subject: [PATCH 015/112] Added unit tests for new course team API --- .../contentstore/tests/test_users.py | 175 ++++++++++++++++++ cms/djangoapps/contentstore/views/user.py | 52 ++++-- cms/templates/manage_users.html | 11 +- 3 files changed, 211 insertions(+), 27 deletions(-) create mode 100644 cms/djangoapps/contentstore/tests/test_users.py diff --git a/cms/djangoapps/contentstore/tests/test_users.py b/cms/djangoapps/contentstore/tests/test_users.py new file mode 100644 index 0000000000..82f511d6ab --- /dev/null +++ b/cms/djangoapps/contentstore/tests/test_users.py @@ -0,0 +1,175 @@ +import json +from .utils import CourseTestCase +from django.contrib.auth.models import User, Group +from django.core.urlresolvers import reverse +from auth.authz import get_course_groupname_for_role + + +class UsersTestCase(CourseTestCase): + def setUp(self): + super(UsersTestCase, self).setUp() + self.ext_user = User.objects.create_user( + "joe", "joe@comedycentral.com", "haha") + self.ext_user.is_active = True + self.ext_user.is_staff = False + self.ext_user.save() + self.inactive_user = User.objects.create_user( + "carl", "carl@comedycentral.com", "haha") + self.inactive_user.is_active = False + self.inactive_user.is_staff = False + self.inactive_user.save() + + self.index_url = reverse("manage_users", kwargs={ + "org": self.course.location.org, + "course": self.course.location.course, + "name": self.course.location.name, + }) + self.detail_url = reverse("course_team_user", kwargs={ + "org": self.course.location.org, + "course": self.course.location.course, + "name": self.course.location.name, + "email": self.ext_user.email, + }) + self.inactive_detail_url = reverse("course_team_user", kwargs={ + "org": self.course.location.org, + "course": self.course.location.course, + "name": self.course.location.name, + "email": self.inactive_user.email, + }) + self.invalid_detail_url = reverse("course_team_user", kwargs={ + "org": self.course.location.org, + "course": self.course.location.course, + "name": self.course.location.name, + "email": "nonexistent@user.com", + }) + self.staff_groupname = get_course_groupname_for_role(self.course.location, "staff") + self.inst_groupname = get_course_groupname_for_role(self.course.location, "instructor") + + def test_index(self): + resp = self.client.get(self.index_url) + self.assertNotContains(resp, self.ext_user.email) + + def test_detail(self): + resp = self.client.get(self.detail_url) + self.assertEqual(resp.status_code, 200) + result = json.loads(resp.content) + self.assertEqual(result["role"], None) + self.assertTrue(result["active"]) + + def test_detail_inactive(self): + resp = self.client.get(self.inactive_detail_url) + self.assert2XX(resp.status_code) + result = json.loads(resp.content) + self.assertFalse(result["active"]) + + def test_detail_invalid(self): + resp = self.client.get(self.invalid_detail_url) + self.assert4XX(resp.status_code) + result = json.loads(resp.content) + self.assertIn("error", result) + + def test_detail_post(self): + resp = self.client.post( + self.detail_url, + data={"role": None}, + ) + self.assert2XX(resp.status_code) + # reload user from DB + ext_user = User.objects.get(email=self.ext_user.email) + groups = [g.name for g in ext_user.groups.all()] + # no content: should not be in any roles + self.assertNotIn(self.staff_groupname, groups) + self.assertNotIn(self.inst_groupname, groups) + + def test_detail_post_staff(self): + resp = self.client.post( + self.detail_url, + data=json.dumps({"role": "staff"}), + content_type="application/json", + HTTP_ACCEPT="application/json", + ) + self.assert2XX(resp.status_code) + # reload user from DB + ext_user = User.objects.get(email=self.ext_user.email) + groups = [g.name for g in ext_user.groups.all()] + self.assertIn(self.staff_groupname, groups) + self.assertNotIn(self.inst_groupname, groups) + + def test_detail_post_instructor(self): + resp = self.client.post( + self.detail_url, + data=json.dumps({"role": "instructor"}), + content_type="application/json", + HTTP_ACCEPT="application/json", + ) + self.assert2XX(resp.status_code) + # reload user from DB + ext_user = User.objects.get(email=self.ext_user.email) + groups = [g.name for g in ext_user.groups.all()] + self.assertNotIn(self.staff_groupname, groups) + self.assertIn(self.inst_groupname, groups) + + def test_detail_post_missing_role(self): + resp = self.client.post( + self.detail_url, + data=json.dumps({"toys": "fun"}), + content_type="application/json", + HTTP_ACCEPT="application/json", + ) + self.assert4XX(resp.status_code) + result = json.loads(resp.content) + self.assertIn("error", result) + + def test_detail_post_bad_json(self): + resp = self.client.post( + self.detail_url, + data="{foo}", + content_type="application/json", + HTTP_ACCEPT="application/json", + ) + self.assert4XX(resp.status_code) + result = json.loads(resp.content) + self.assertIn("error", result) + + def test_detail_post_no_json(self): + resp = self.client.post( + self.detail_url, + data={"role": "staff"}, + HTTP_ACCEPT="application/json", + ) + self.assert2XX(resp.status_code) + # reload user from DB + ext_user = User.objects.get(email=self.ext_user.email) + groups = [g.name for g in ext_user.groups.all()] + self.assertIn(self.staff_groupname, groups) + self.assertNotIn(self.inst_groupname, groups) + + def test_detail_delete_staff(self): + group, _ = Group.objects.get_or_create(name=self.staff_groupname) + self.ext_user.groups.add(group) + self.ext_user.save() + + resp = self.client.delete( + self.detail_url, + HTTP_ACCEPT="application/json", + ) + self.assert2XX(resp.status_code) + # reload user from DB + ext_user = User.objects.get(email=self.ext_user.email) + groups = [g.name for g in ext_user.groups.all()] + self.assertNotIn(self.staff_groupname, groups) + + def test_detail_delete_instructor(self): + group, _ = Group.objects.get_or_create(name=self.inst_groupname) + self.ext_user.groups.add(group) + self.ext_user.save() + + resp = self.client.delete( + self.detail_url, + HTTP_ACCEPT="application/json", + ) + self.assert2XX(resp.status_code) + # reload user from DB + ext_user = User.objects.get(email=self.ext_user.email) + groups = [g.name for g in ext_user.groups.all()] + self.assertNotIn(self.inst_groupname, groups) diff --git a/cms/djangoapps/contentstore/views/user.py b/cms/djangoapps/contentstore/views/user.py index c5d642e207..2b2f170617 100644 --- a/cms/djangoapps/contentstore/views/user.py +++ b/cms/djangoapps/contentstore/views/user.py @@ -111,20 +111,23 @@ def course_team_user(request, org, course, name, email): } return JsonResponse(msg, 404) + # role hierarchy: "instructor" has more permissions than "staff" (in a course) + roles = ["instructor", "staff"] + if request.method == "GET": # just return info about the user - roles = set() - for group in user.groups.all(): - if not "_" in group.name: - continue - role, coursename = group.name.split("_", 1) - if coursename in (location.course, location.course_id): - roles.add(role) msg = { "email": user.email, "active": user.is_active, - "roles": list(roles), + "role": None, } + # what's the highest role that this user has? + groupnames = set(g.name for g in user.groups.all()) + for role in roles: + role_groupname = get_course_groupname_for_role(location, role) + if role_groupname in groupnames: + msg["role"] = role + break return JsonResponse(msg) # can't modify an inactive user @@ -134,11 +137,14 @@ def course_team_user(request, org, course, name, email): } return JsonResponse(msg, 400) - # all other operations require the requesting user to specify a role -- - # or if no role is specified, default to "staff" - if not request.body: - role = STAFF_ROLE_NAME - else: + if request.method == "DELETE": + # remove all roles in this course from this user + for role in roles: + remove_user_from_course_group(request.user, user, location, role) + return JsonResponse() + + # all other operations require the requesting user to specify a role + if request.META.get("CONTENT_TYPE", "") == "application/json" and request.body: try: payload = json.loads(request.body) except: @@ -147,15 +153,21 @@ def course_team_user(request, org, course, name, email): role = payload["role"] except KeyError: return JsonResponse({"error": "`role` is required"}, 400) - groupname = get_course_groupname_for_role(location, role) - group = Group.objects.get_or_create(name=groupname) + else: + if not "role" in request.POST: + return JsonResponse({"error": "`role` is required"}, 400) + role = request.POST["role"] - if request.method in ("POST", "PUT"): + # make sure that the role group exists + groupname = get_course_groupname_for_role(location, role) + Group.objects.get_or_create(name=groupname) + + if role == "instructor": add_user_to_course_group(request.user, user, location, role) - return JsonResponse() - elif request.method == "DELETE": - remove_user_from_course_group(request.user, user, location, role) - return JsonResponse() + elif role == "staff": + add_user_to_course_group(request.user, user, location, role) + remove_user_from_course_group(request.user, user, location, "instructor") + return JsonResponse() def _get_course_creator_status(user): diff --git a/cms/templates/manage_users.html b/cms/templates/manage_users.html index 69430cbbea..9a468664b5 100644 --- a/cms/templates/manage_users.html +++ b/cms/templates/manage_users.html @@ -140,9 +140,6 @@ type: 'DELETE', dataType: 'json', contentType: 'application/json', - data: JSON.stringify({ - role: 'staff', - }), complete: function() { location.reload(); } @@ -153,18 +150,18 @@ e.preventDefault() var type; if($(this).hasClass("add-admin")) { - type = 'POST'; + role = 'instructor'; } else { - type = 'DELETE'; + role = 'staff'; } var url = $(this).closest("li").data("url"); $.ajax({ url: url, - type: type, + type: 'POST', dataType: 'json', contentType: 'application/json', data: JSON.stringify({ - role: 'instructor', + role: role }), complete: function() { location.reload(); From 0682157477bbd1996c99261ce0b730a2ba41953f Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Tue, 23 Jul 2013 15:50:44 -0400 Subject: [PATCH 016/112] Test manage_users view for user that is a member of the course team --- cms/djangoapps/contentstore/tests/test_users.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cms/djangoapps/contentstore/tests/test_users.py b/cms/djangoapps/contentstore/tests/test_users.py index 82f511d6ab..327bbfcf64 100644 --- a/cms/djangoapps/contentstore/tests/test_users.py +++ b/cms/djangoapps/contentstore/tests/test_users.py @@ -47,8 +47,18 @@ class UsersTestCase(CourseTestCase): def test_index(self): resp = self.client.get(self.index_url) + # ext_user is not currently a member of the course team, and so should + # not show up on the page. self.assertNotContains(resp, self.ext_user.email) + def test_index_member(self): + group, _ = Group.objects.get_or_create(name=self.staff_groupname) + self.ext_user.groups.add(group) + self.ext_user.save() + + resp = self.client.get(self.index_url) + self.assertContains(resp, self.ext_user.email) + def test_detail(self): resp = self.client.get(self.detail_url) self.assertEqual(resp.status_code, 200) From c70bd5c908249de3557cc96b0307918c91c5ed32 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Tue, 23 Jul 2013 16:07:43 -0400 Subject: [PATCH 017/112] Remove whitespace from email addresses on the course team page --- cms/templates/manage_users.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/templates/manage_users.html b/cms/templates/manage_users.html index 9a468664b5..1fa6a4d64a 100644 --- a/cms/templates/manage_users.html +++ b/cms/templates/manage_users.html @@ -93,7 +93,7 @@ var $newUserForm = $('.new-user-form'); $newUserForm.bind('submit', function(e) { e.preventDefault(); - var url = tplUserURL.replace("@@EMAIL@@", $('#email').val()) + var url = tplUserURL.replace("@@EMAIL@@", $('#email').val().trim()) $.ajax({ url: url, type: 'POST', From b6c69547de0432ca1d55d44b35d32ecfd461061f Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Tue, 23 Jul 2013 16:43:19 -0400 Subject: [PATCH 018/112] Check for instructor role before removing it --- cms/djangoapps/contentstore/views/user.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/views/user.py b/cms/djangoapps/contentstore/views/user.py index 2b2f170617..6945d75da4 100644 --- a/cms/djangoapps/contentstore/views/user.py +++ b/cms/djangoapps/contentstore/views/user.py @@ -166,7 +166,10 @@ def course_team_user(request, org, course, name, email): add_user_to_course_group(request.user, user, location, role) elif role == "staff": add_user_to_course_group(request.user, user, location, role) - remove_user_from_course_group(request.user, user, location, "instructor") + # should *not* be an instructor + inst_groupname = get_course_groupname_for_role(location, "instructor") + if any(g.name == inst_groupname for g in user.groups.all()): + remove_user_from_course_group(request.user, user, location, "instructor") return JsonResponse() From 79c554ba5b82d0ba18d4777d7235f634783da483 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Wed, 24 Jul 2013 09:52:59 -0400 Subject: [PATCH 019/112] course admin team: handle is_staff users A user with `is_staff=True` is treated as being in all groups. This is problematic when we care about the user's staff/instructor role for a course: you can't remove the instructor role. This commit changes the `is_user_in_course_group_role` function to allow the caller to specify that it should not check the `is_staff` attribute on the user. --- cms/djangoapps/auth/authz.py | 6 ++++-- cms/templates/manage_users.html | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/cms/djangoapps/auth/authz.py b/cms/djangoapps/auth/authz.py index 0f2e60dd6e..4923851445 100644 --- a/cms/djangoapps/auth/authz.py +++ b/cms/djangoapps/auth/authz.py @@ -178,10 +178,12 @@ def _remove_user_from_group(user, group_name): user.save() -def is_user_in_course_group_role(user, location, role): +def is_user_in_course_group_role(user, location, role, check_staff=True): if user.is_active and user.is_authenticated: # all "is_staff" flagged accounts belong to all groups - return user.is_staff or user.groups.filter(name=get_course_groupname_for_role(location, role)).count() > 0 + if check_staff and user.is_staff: + return True + return user.groups.filter(name=get_course_groupname_for_role(location, role)).count() > 0 return False diff --git a/cms/templates/manage_users.html b/cms/templates/manage_users.html index 1fa6a4d64a..8baa9854c9 100644 --- a/cms/templates/manage_users.html +++ b/cms/templates/manage_users.html @@ -59,7 +59,7 @@ % if allow_actions:
    % if request.user.id != user.id: - % if is_user_in_course_group_role(user, context_course.location, 'instructor'): + % if is_user_in_course_group_role(user, context_course.location, 'instructor', check_staff=False): <% admin_class = "remove-admin" %> <% admin_text = "Remove Admin" %> % else: From 42331464eda1887d9bb43499c67fc1ea45f83e43 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Wed, 24 Jul 2013 10:39:27 -0400 Subject: [PATCH 020/112] Can't remove last instructor of a course --- .../contentstore/tests/test_users.py | 60 +++++++++++++++++++ cms/djangoapps/contentstore/views/user.py | 46 ++++++++++---- cms/templates/manage_users.html | 26 ++++---- 3 files changed, 106 insertions(+), 26 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_users.py b/cms/djangoapps/contentstore/tests/test_users.py index 327bbfcf64..2fe88490ee 100644 --- a/cms/djangoapps/contentstore/tests/test_users.py +++ b/cms/djangoapps/contentstore/tests/test_users.py @@ -105,6 +105,29 @@ class UsersTestCase(CourseTestCase): self.assertIn(self.staff_groupname, groups) self.assertNotIn(self.inst_groupname, groups) + def test_detail_post_staff_other_inst(self): + inst_group, _ = Group.objects.get_or_create(name=self.inst_groupname) + self.user.groups.add(inst_group) + self.user.save() + + resp = self.client.post( + self.detail_url, + data=json.dumps({"role": "staff"}), + content_type="application/json", + HTTP_ACCEPT="application/json", + ) + self.assert2XX(resp.status_code) + # reload user from DB + ext_user = User.objects.get(email=self.ext_user.email) + groups = [g.name for g in ext_user.groups.all()] + self.assertIn(self.staff_groupname, groups) + self.assertNotIn(self.inst_groupname, groups) + # check that other user is unchanged + user = User.objects.get(email=self.user.email) + groups = [g.name for g in user.groups.all()] + self.assertNotIn(self.staff_groupname, groups) + self.assertIn(self.inst_groupname, groups) + def test_detail_post_instructor(self): resp = self.client.post( self.detail_url, @@ -171,7 +194,9 @@ class UsersTestCase(CourseTestCase): def test_detail_delete_instructor(self): group, _ = Group.objects.get_or_create(name=self.inst_groupname) + self.user.groups.add(group) self.ext_user.groups.add(group) + self.user.save() self.ext_user.save() resp = self.client.delete( @@ -183,3 +208,38 @@ class UsersTestCase(CourseTestCase): ext_user = User.objects.get(email=self.ext_user.email) groups = [g.name for g in ext_user.groups.all()] self.assertNotIn(self.inst_groupname, groups) + + def test_delete_last_instructor(self): + group, _ = Group.objects.get_or_create(name=self.inst_groupname) + self.ext_user.groups.add(group) + self.ext_user.save() + + resp = self.client.delete( + self.detail_url, + HTTP_ACCEPT="application/json", + ) + self.assertEqual(resp.status_code, 400) + result = json.loads(resp.content) + self.assertIn("error", result) + # reload user from DB + ext_user = User.objects.get(email=self.ext_user.email) + groups = [g.name for g in ext_user.groups.all()] + self.assertIn(self.inst_groupname, groups) + + def test_post_last_instructor(self): + group, _ = Group.objects.get_or_create(name=self.inst_groupname) + self.ext_user.groups.add(group) + self.ext_user.save() + + resp = self.client.post( + self.detail_url, + data={"role": "staff"}, + HTTP_ACCEPT="application/json", + ) + self.assertEqual(resp.status_code, 400) + result = json.loads(resp.content) + self.assertIn("error", result) + # reload user from DB + ext_user = User.objects.get(email=self.ext_user.email) + groups = [g.name for g in ext_user.groups.all()] + self.assertIn(self.inst_groupname, groups) diff --git a/cms/djangoapps/contentstore/views/user.py b/cms/djangoapps/contentstore/views/user.py index 6945d75da4..b7fe313226 100644 --- a/cms/djangoapps/contentstore/views/user.py +++ b/cms/djangoapps/contentstore/views/user.py @@ -87,9 +87,15 @@ def manage_users(request, org, course, name): course_module = modulestore().get_item(location) + staff_groupname = get_course_groupname_for_role(location, "staff") + staff_group, __ = Group.objects.get_or_create(name=staff_groupname) + inst_groupname = get_course_groupname_for_role(location, "instructor") + inst_group, __ = Group.objects.get_or_create(name=inst_groupname) + return render_to_response('manage_users.html', { 'context_course': course_module, - 'staff': get_users_in_course_group_by_role(location, STAFF_ROLE_NAME), + 'staff': staff_group.user_set.all(), + 'instructors': inst_group.user_set.all(), 'allow_actions': has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME), }) @@ -137,8 +143,22 @@ def course_team_user(request, org, course, name, email): } return JsonResponse(msg, 400) + # make sure that the role groups exist + staff_groupname = get_course_groupname_for_role(location, "staff") + staff_group, __ = Group.objects.get_or_create(name=staff_groupname) + inst_groupname = get_course_groupname_for_role(location, "instructor") + inst_group, __ = Group.objects.get_or_create(name=inst_groupname) + if request.method == "DELETE": - # remove all roles in this course from this user + # remove all roles in this course from this user: but fail if the user + # is the last instructor in the course team + instructors = set(inst_group.user_set.all()) + if user in instructors and len(instructors) == 1: + msg = { + "error": _("You may not remove the last instructor from a course") + } + return JsonResponse(msg, 400) + for role in roles: remove_user_from_course_group(request.user, user, location, role) return JsonResponse() @@ -152,24 +172,26 @@ def course_team_user(request, org, course, name, email): try: role = payload["role"] except KeyError: - return JsonResponse({"error": "`role` is required"}, 400) + return JsonResponse({"error": _("`role` is required")}, 400) else: if not "role" in request.POST: - return JsonResponse({"error": "`role` is required"}, 400) + return JsonResponse({"error": _("`role` is required")}, 400) role = request.POST["role"] - # make sure that the role group exists - groupname = get_course_groupname_for_role(location, role) - Group.objects.get_or_create(name=groupname) - if role == "instructor": add_user_to_course_group(request.user, user, location, role) elif role == "staff": - add_user_to_course_group(request.user, user, location, role) - # should *not* be an instructor - inst_groupname = get_course_groupname_for_role(location, "instructor") - if any(g.name == inst_groupname for g in user.groups.all()): + # if we're trying to downgrade a user from "instructor" to "staff", + # make sure we have at least one other instructor in the course team. + instructors = set(inst_group.user_set.all()) + if user in instructors: + if len(instructors) == 1: + msg = { + "error": _("You may not remove the last instructor from a course") + } + return JsonResponse(msg, 400) remove_user_from_course_group(request.user, user, location, "instructor") + add_user_to_course_group(request.user, user, location, role) return JsonResponse() diff --git a/cms/templates/manage_users.html b/cms/templates/manage_users.html index 8baa9854c9..41738b1e6d 100644 --- a/cms/templates/manage_users.html +++ b/cms/templates/manage_users.html @@ -39,9 +39,9 @@
    - - - + + +
    %endif @@ -58,15 +58,13 @@ ${user.email} % if allow_actions:
    - % if request.user.id != user.id: - % if is_user_in_course_group_role(user, context_course.location, 'instructor', check_staff=False): - <% admin_class = "remove-admin" %> - <% admin_text = "Remove Admin" %> - % else: - <% admin_class = "add-admin" %> - <% admin_text = "Add Admin" %> - % endif - ${admin_text} + <% is_instuctor = is_user_in_course_group_role(user, context_course.location, 'instructor', check_staff=False) %> + % if is_instuctor and len(instructors) == 1: + Admin + % else: + ${_("Remove Admin") if is_instuctor else _("Add Admin")} + % endif + % if request.user.id != user.id: ## can't remove yourself % endif
    @@ -146,10 +144,10 @@ }); }); - $(".toggle-admin").click(function(e) { + $(".toggle-admin-role").click(function(e) { e.preventDefault() var type; - if($(this).hasClass("add-admin")) { + if($(this).hasClass("add-admin-role")) { role = 'instructor'; } else { role = 'staff'; From a1b44afda343780f932b244804c47ea5ed3ae9a2 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Wed, 24 Jul 2013 10:54:34 -0400 Subject: [PATCH 021/112] Only instructors may make other instructors on a course --- .../contentstore/tests/test_users.py | 74 +++++++++++++++++++ cms/djangoapps/contentstore/views/user.py | 26 ++++++- 2 files changed, 96 insertions(+), 4 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_users.py b/cms/djangoapps/contentstore/tests/test_users.py index 2fe88490ee..4b9dcf487f 100644 --- a/cms/djangoapps/contentstore/tests/test_users.py +++ b/cms/djangoapps/contentstore/tests/test_users.py @@ -243,3 +243,77 @@ class UsersTestCase(CourseTestCase): ext_user = User.objects.get(email=self.ext_user.email) groups = [g.name for g in ext_user.groups.all()] self.assertIn(self.inst_groupname, groups) + + def test_permission_denied_self(self): + group, _ = Group.objects.get_or_create(name=self.staff_groupname) + self.user.groups.add(group) + self.user.is_staff = False + self.user.save() + + self_url = reverse("course_team_user", kwargs={ + "org": self.course.location.org, + "course": self.course.location.course, + "name": self.course.location.name, + "email": self.user.email, + }) + + resp = self.client.post( + self_url, + data={"role": "instructor"}, + HTTP_ACCEPT="application/json", + ) + self.assert4XX(resp.status_code) + result = json.loads(resp.content) + self.assertIn("error", result) + + def test_permission_denied_other(self): + group, _ = Group.objects.get_or_create(name=self.staff_groupname) + self.user.groups.add(group) + self.user.is_staff = False + self.user.save() + + resp = self.client.post( + self.detail_url, + data={"role": "instructor"}, + HTTP_ACCEPT="application/json", + ) + self.assert4XX(resp.status_code) + result = json.loads(resp.content) + self.assertIn("error", result) + + def test_staff_can_delete_self(self): + group, _ = Group.objects.get_or_create(name=self.staff_groupname) + self.user.groups.add(group) + self.user.is_staff = False + self.user.save() + + self_url = reverse("course_team_user", kwargs={ + "org": self.course.location.org, + "course": self.course.location.course, + "name": self.course.location.name, + "email": self.user.email, + }) + + resp = self.client.delete(self_url) + self.assert2XX(resp.status_code) + # reload user from DB + user = User.objects.get(email=self.user.email) + groups = [g.name for g in user.groups.all()] + self.assertNotIn(self.staff_groupname, groups) + + def test_staff_cannot_delete_other(self): + group, _ = Group.objects.get_or_create(name=self.staff_groupname) + self.user.groups.add(group) + self.user.is_staff = False + self.user.save() + self.ext_user.groups.add(group) + self.ext_user.save() + + resp = self.client.delete(self.detail_url) + self.assert4XX(resp.status_code) + result = json.loads(resp.content) + self.assertIn("error", result) + # reload user from DB + ext_user = User.objects.get(email=self.ext_user.email) + groups = [g.name for g in ext_user.groups.all()] + self.assertIn(self.staff_groupname, groups) diff --git a/cms/djangoapps/contentstore/views/user.py b/cms/djangoapps/contentstore/views/user.py index b7fe313226..7e1cca2370 100644 --- a/cms/djangoapps/contentstore/views/user.py +++ b/cms/djangoapps/contentstore/views/user.py @@ -106,8 +106,17 @@ def manage_users(request, org, course, name): def course_team_user(request, org, course, name, email): location = Location('i4x', org, course, 'course', name) # check that logged in user has permissions to this item - if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME) and not has_access(request.user, location, role=STAFF_ROLE_NAME): - raise PermissionDenied() + if has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME): + # instructors have full permissions + pass + elif has_access(request.user, location, role=STAFF_ROLE_NAME) and email == request.user.email: + # staff can only affect themselves + pass + else: + msg = { + "error": _("Insufficient permissions") + } + return JsonResponse(msg, 400) try: user = User.objects.get(email=email) @@ -153,14 +162,18 @@ def course_team_user(request, org, course, name, email): # remove all roles in this course from this user: but fail if the user # is the last instructor in the course team instructors = set(inst_group.user_set.all()) + staff = set(staff_group.user_set.all()) if user in instructors and len(instructors) == 1: msg = { "error": _("You may not remove the last instructor from a course") } return JsonResponse(msg, 400) - for role in roles: - remove_user_from_course_group(request.user, user, location, role) + if user in instructors: + user.groups.remove(inst_group) + if user in staff: + user.groups.remove(staff_group) + user.save() return JsonResponse() # all other operations require the requesting user to specify a role @@ -179,6 +192,11 @@ def course_team_user(request, org, course, name, email): role = request.POST["role"] if role == "instructor": + if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME): + msg = { + "error": _("Only instructors may create other instructors") + } + return JsonResponse(msg, 400) add_user_to_course_group(request.user, user, location, role) elif role == "staff": # if we're trying to downgrade a user from "instructor" to "staff", From 8a10695d7eed4a8bea2980d0cdb7cdbd0eae5582 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Wed, 24 Jul 2013 11:20:59 -0400 Subject: [PATCH 022/112] Extend runone script to accept pdb flags --- scripts/runone.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/runone.py b/scripts/runone.py index b403b09ff9..8baf6790b8 100755 --- a/scripts/runone.py +++ b/scripts/runone.py @@ -19,6 +19,8 @@ def find_full_path(path_to_file): def main(argv): parser = argparse.ArgumentParser(description="Run just one test") parser.add_argument('--nocapture', '-s', action='store_true', help="Don't capture stdout (any stdout output will be printed immediately)") + parser.add_argument('--pdb', action='store_true', help="Use pdb for test errors") + parser.add_argument('--pdb-fail', action='store_true', help="Use pdb for test failures") parser.add_argument('words', metavar="WORDS", nargs='+', help="The description of a test failure, like 'ERROR: test_set_missing_field (courseware.tests.test_model_data.TestStudentModuleStorage)'") args = parser.parse_args(argv) @@ -54,6 +56,10 @@ def main(argv): django_args = ["./manage.py", system, "--settings", "test", "test"] if args.nocapture: django_args.append("-s") + if args.pdb: + django_args.append("--pdb") + if args.pdb_fail: + django_args.append("--pdb-fail") django_args.append(test_spec) print " ".join(django_args) From 4a2b5519ba2043cd3962739a661c9e87696ffe9f Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Thu, 25 Jul 2013 12:59:03 -0400 Subject: [PATCH 023/112] Studio: styles new user role controls and revamps course team UI --- cms/static/sass/elements/_forms.scss | 165 ++++++++++++++++++++++ cms/static/sass/elements/_icons.scss | 41 +++++- cms/static/sass/views/_textbooks.scss | 2 +- cms/static/sass/views/_users.scss | 195 ++++++++++++++++++++++---- cms/templates/manage_users.html | 164 +++++++++++++++------- 5 files changed, 492 insertions(+), 75 deletions(-) diff --git a/cms/static/sass/elements/_forms.scss b/cms/static/sass/elements/_forms.scss index 9907b05995..3d11db02c3 100644 --- a/cms/static/sass/elements/_forms.scss +++ b/cms/static/sass/elements/_forms.scss @@ -95,6 +95,171 @@ form { // ==================== +// forms - archetype - add a new X form +.new-form { + @extend .ui-window; + @include box-sizing(border-box); + position: relative; + width: 100%; + margin-bottom: ($baseline*2); + border-radius: 2px; + background: $white; + + .wrapper-form { + padding: $baseline ($baseline*1.5); + } + + .title { + @extend .t-title4; + font-weight: 700; + margin-bottom: $baseline; + border-bottom: 1px solid $gray-l4; + padding-bottom: ($baseline/2); + } + + fieldset { + margin-bottom: $baseline; + } + + // form elements - need to make this more universal + .form-fields { + @extend .cont-no-list; + + .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 { + @extend .t-copy-sub1; + @include transition(color, 0.15s, ease-in-out); + margin: 0 0 ($baseline/4) 0; + + &.is-focused { + color: $blue; + } + } + + //this section is borrowed from _account.scss - we should clean up and unify later + input, textarea { + @extend .t-copy-base; + 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 { + @extend .t-copy-sub2; + @include transition(color, 0.15s, ease-in-out); + display: block; + margin-top: ($baseline/4); + color: $gray-l3; + } + + &.error { + + label { + color: $red; + } + + input { + border-color: $red; + } + } + } + } + + .actions { + border-top: 1px solid $gray-l1; + padding: ($baseline*0.75) $baseline; + box-shadow: inset 0 1px 2px $shadow; + background: $gray-l6; + + .action-primary { + @include blue-button; + @extend .t-action2; + @include transition(all $tmg-f2 linear 0); + display: inline-block; + padding: ($baseline/5) $baseline; + font-weight: 600; + } + + .action-secondary { + @include grey-button; + @extend .t-action2; + @include transition(all $tmg-f2 linear 0); + display: inline-block; + padding: ($baseline/5) $baseline; + font-weight: 600; + } + } +} + +// ==================== + // forms - grandfathered input.search { padding: 6px 15px 8px 30px; diff --git a/cms/static/sass/elements/_icons.scss b/cms/static/sass/elements/_icons.scss index a75c97ea76..5b5dc0ddfd 100644 --- a/cms/static/sass/elements/_icons.scss +++ b/cms/static/sass/elements/_icons.scss @@ -1,4 +1,4 @@ -// studio - elements - icons +// studio - elements - icons & badges // ==================== .icon { @@ -14,3 +14,42 @@ vertical-align: middle; margin-right: ($baseline/4); } + +// ui - badges +.wrapper-ui-badge { + position: absolute; + top: -1px; + left: ($baseline*1.5); +} + +.ui-badge { + @extend .t-title9; + position: relative; + border-bottom-right-radius: ($baseline/10); + border-bottom-left-radius: ($baseline/10); + padding: ($baseline/4) ($baseline/2) ($baseline/4) ($baseline/2); + font-weight: 600; + text-transform: uppercase; + + * [class^="icon-"] { + margin-right: ($baseline/5); + } + + // OPTION: add this class for a visual hanging display + &.is-hanging { + top: -($baseline/4); + + &:after { + position: absolute; + top: 0; + right: -($baseline/4); + display: block; + height: 0; + width: 0; + border-bottom: ($baseline/4) solid $black-t3; + border-right: ($baseline/4) solid transparent; + content: ""; + opacity: 0.5; + } + } +} diff --git a/cms/static/sass/views/_textbooks.scss b/cms/static/sass/views/_textbooks.scss index 8d2b2d9489..8058673b2b 100644 --- a/cms/static/sass/views/_textbooks.scss +++ b/cms/static/sass/views/_textbooks.scss @@ -30,7 +30,7 @@ body.course.textbooks { } .textbook { - @extend .window; + @extend .ui-window; position: relative; .view-textbook { diff --git a/cms/static/sass/views/_users.scss b/cms/static/sass/views/_users.scss index ecaa319707..514536ccca 100644 --- a/cms/static/sass/views/_users.scss +++ b/cms/static/sass/views/_users.scss @@ -3,20 +3,56 @@ body.course.users { + // page layout + .content-primary, .content-supplementary { + @include box-sizing(border-box); + float: left; + } + + .content-primary { + width: flex-grid(9, 12); + margin-right: flex-gutter(); + } + + .content-supplementary { + width: flex-grid(3, 12); + } + + // content + .content { + + .introduction { + @extend .t-copy-sub1; + margin: 0 0 ($baseline*2) 0; + } + } + + + // new user form + .add-user { + @extend .new-form; + display: none; + + &.is-shown { + display: block; + } + } + + // new user form (old) .new-user-form { display: none; - padding: 15px 20px; + padding: ($baseline*0.75) $baseline; background-color: $lightBluishGrey2; #result { display: none; float: left; - margin-bottom: 15px; - padding: 3px 15px; + margin-bottom: ($baseline*0.75); + padding: 3px ($baseline*0.75); border-radius: 3px; - background: $error-red; + background: $red; font-size: 14px; - color: #fff; + color: $white; } .form-elements { @@ -25,58 +61,165 @@ body.course.users { label { display: inline-block; - margin-right: 10px; + margin-right: ($baseline/2); } .email-input { width: 350px; padding: 8px 8px 10px; - border-color: $darkGrey; + border-color: $gray-d1; } .add-button { @include blue-button; - padding: 5px 20px 9px; + padding: ($baseline/4) $baseline 9px; } .cancel-button { @include white-button; - padding: 5px 20px 9px; + padding: ($baseline/4) $baseline 9px; } } + // listing of users + .user-list, .user-item, .item-metadata, .item-actions { + @include box-sizing(border-box); + } + .user-list { - border: 1px solid $mediumGrey; - background: #fff; - li { + .user-item { + @extend .ui-window; + @include clearfix(); position: relative; - padding: 20px; - border-bottom: 1px solid $mediumGrey; + width: flex-grid(9, 9); + margin: 0 0 ($baseline/2) 0; + padding: $baseline ($baseline*1.5) $baseline ($baseline*1.5); &:last-child { - border-bottom: none; + margin-bottom: 0; } - span { + .item-metadata, .item-actions { display: inline-block; + vertical-align: middle; } - .user-name { - margin-right: 10px; - font-size: 24px; - font-weight: 300; + // item - flag + .flag-role { + @extend .ui-badge; + color: $white; + + .msg-you { + margin-left: ($baseline/5); + opacity: 0.65; + text-transform: none; + font-weight: 500; + } + + &:after { + border-bottom-color: $black-t1; + } + + &.flag-role-staff { + background: $gray-l2; + } + + &.flag-role-admin { + background: $gray-d1; + } } - .user-email { - font-size: 14px; - font-style: italic; - color: $mediumGrey; + // item - metadata + .item-metadata { + width: flex-grid(5, 9); + margin-right: flex-gutter(); + + .user-username, .user-email { + display: inline-block; + vertical-align: middle; + } + + .user-username { + @extend .t-title4; + @include transition(color $tmg-f2 ease-in-out 0s); + margin: 0 ($baseline/2) ($baseline/10) 0; + color: $gray-d4; + font-weight: 600; + } + + .user-email { + @extend .t-title6; + } } + // item - actions .item-actions { - top: 24px; + width: flex-grid(4, 9); + position: static; // nasty reset needed due to base.scss + text-align: right; + + .action { + display: inline-block; + vertical-align: middle; + } + + .action-role { + margin-right: ($baseline/2); + } + + .action-delete { + + } + + .delete { + @extend .ui-btn-non; + } + + // nasty reset needed due to base.scss + .delete-button { + margin-right: 0; + float: none; + color: inherit; + } + + // admin role controls + .toggle-admin-role { + + &.add-admin-role { + @include blue-button; + @extend .t-action2; + @include transition(all .15s); + display: inline-block; + padding: ($baseline/5) $baseline; + font-weight: 600; + } + + &.remove-admin-role { + @include grey-button; + @extend .t-action2; + @include transition(all .15s); + display: inline-block; + padding: ($baseline/5) $baseline; + font-weight: 600; + } + } + } + + // STATE: hover + &:hover { + + .user-username { + } + + .user-email { + + } + + .item-actions { + + } } } } -} \ No newline at end of file +} diff --git a/cms/templates/manage_users.html b/cms/templates/manage_users.html index 41738b1e6d..fd2236a966 100644 --- a/cms/templates/manage_users.html +++ b/cms/templates/manage_users.html @@ -3,7 +3,7 @@ <%! from auth.authz import is_user_in_course_group_role %> <%inherit file="base.html" /> <%block name="title">${_("Course Team Settings")} -<%block name="bodyclass">is-signedin course users settings team +<%block name="bodyclass">is-signedin course users team <%block name="content"> @@ -27,54 +27,122 @@
    -
    -
    - -
    -

    ${_("The following list of users have been designated as course staff. This means that these users will have permissions to modify course content. You may add additional course staff below, if you are the course instructor. Please note that they must have already registered and verified their account.")}

    +
    +
    +
    +

    Managing Your Course Team

    +
    +

    ${_("[Introduction Message - Mark to Provide Copy.] Maecenas faucibus mollis interdum. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla vitae elit libero, a pharetra augue.")}

    +
    -
    +
    %if allow_actions: -
    -
    -
    - - - + +
    +

    ${_("Add a User to Your Course's Team")}

    + +
    + +
    + +
    + ${_("Textbook information")} +
    + + + ${_("Please provide the email address of the course staff member you'd like to add")} +
    +
    +
    +
    + +
    %endif -
    -
      - % for user in staff: -
    1. - ${user.username} - ${user.email} - % if allow_actions: -
      - <% is_instuctor = is_user_in_course_group_role(user, context_course.location, 'instructor', check_staff=False) %> + +
        + % for user in staff: +
      1. + + <% is_instuctor = is_user_in_course_group_role(user, context_course.location, 'instructor', check_staff=False) %> + % if is_instuctor and len(instructors) == 1: + + + ${_("Current Role:")} + + ${_("Admin")} + % if request.user.id == user.id: + ${_("You!")} + % endif + + + + % else: + + + ${_("Current Role:")} + + ${_("Staff")} + % if request.user.id == user.id: + ${_("You!")} + % endif + + + + % endif + + + + % if allow_actions: +
      +
    2. + % if request.user.id != user.id: ## can't remove yourself +
    3. + ${_("Delete the user,")} ${user.username} +
    4. % endif - - % endfor -
    -
    + + % endif + +
  3. + % endfor +
-
+ + +
@@ -88,10 +156,10 @@ ))}" $(document).ready(function() { - var $newUserForm = $('.new-user-form'); + var $newUserForm = $('#add-user-form'); $newUserForm.bind('submit', function(e) { e.preventDefault(); - var url = tplUserURL.replace("@@EMAIL@@", $('#email').val().trim()) + var url = tplUserURL.replace("@@EMAIL@@", $('#user-email-input').val().trim()) $.ajax({ url: url, type: 'POST', @@ -106,23 +174,25 @@ notifyOnError: false, error: function(jqXHR, textStatus, errorThrown) { data = JSON.parse(jqXHR.responseText); - $('#result').show().empty().append(data.error); + $('#add-user-error').toggleClass('is-shown').empty().append(data.error); } }); }); - var $cancelButton = $newUserForm.find('.cancel-button'); + var $cancelButton = $newUserForm.find('.action-cancel'); $cancelButton.bind('click', function(e) { e.preventDefault(); - $newUserForm.slideUp(150); - $('#result').hide(); - $('#email').val(''); + $('.new-user-button').removeClass('is-disabled'); + $newUserForm.toggleClass('is-shown'); + $('#add-user-error').removeClass('is-shown'); + $('#user-email-input').val(''); }); $('.new-user-button').bind('click', function(e) { e.preventDefault(); - $newUserForm.slideDown(150); - $newUserForm.find('.email-input').focus(); + $(this).addClass('is-disabled'); + $newUserForm.toggleClass('is-shown'); + $newUserForm.find('#user-email-input').focus(); }); $('body').bind('keyup', function(e) { From 91f192f6b5a4c18c5c317b788b0d95e0cff58461 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 25 Jul 2013 13:29:33 -0400 Subject: [PATCH 024/112] Added error prompts for the course team page --- cms/templates/manage_users.html | 75 +++++++++++++++++++++++++++++---- 1 file changed, 66 insertions(+), 9 deletions(-) diff --git a/cms/templates/manage_users.html b/cms/templates/manage_users.html index fd2236a966..76d0fa79d9 100644 --- a/cms/templates/manage_users.html +++ b/cms/templates/manage_users.html @@ -42,10 +42,6 @@

${_("Add a User to Your Course's Team")}

-
- -
-
${_("Textbook information")}
@@ -173,8 +169,26 @@ }, notifyOnError: false, error: function(jqXHR, textStatus, errorThrown) { - data = JSON.parse(jqXHR.responseText); - $('#add-user-error').toggleClass('is-shown').empty().append(data.error); + var message; + try { + message = JSON.parse(jqXHR.responseText).error || "Unknown"; + } catch (e) { + message = "Unknown"; + } + var prompt = new CMS.Views.Prompt.Error({ + title: gettext("Error adding user"), + message: message, + actions: { + primary: { + text: gettext("OK"), + click: function(view) { + view.hide(); + $("#user-email-input").focus() + } + } + } + }) + prompt.show(); } }); }); @@ -184,7 +198,6 @@ e.preventDefault(); $('.new-user-button').removeClass('is-disabled'); $newUserForm.toggleClass('is-shown'); - $('#add-user-error').removeClass('is-shown'); $('#user-email-input').val(''); }); @@ -208,8 +221,30 @@ type: 'DELETE', dataType: 'json', contentType: 'application/json', - complete: function() { + success: function(data) { location.reload(); + }, + notifyOnError: false, + error: function(jqXHR, textStatus, errorThrown) { + var message; + try { + message = JSON.parse(jqXHR.responseText).error || "Unknown"; + } catch (e) { + message = "Unknown"; + } + var prompt = new CMS.Views.Prompt.Error({ + title: gettext("Error removing user"), + message: message, + actions: { + primary: { + text: gettext("OK"), + click: function(view) { + view.hide(); + } + } + } + }) + prompt.show(); } }); }); @@ -231,8 +266,30 @@ data: JSON.stringify({ role: role }), - complete: function() { + success: function(data) { location.reload(); + }, + notifyOnError: false, + error: function(jqXHR, textStatus, errorThrown) { + var message; + try { + message = JSON.parse(jqXHR.responseText).error || "Unknown"; + } catch (e) { + message = "Unknown"; + } + var prompt = new CMS.Views.Prompt.Error({ + title: gettext("Error changing user"), + message: message, + actions: { + primary: { + text: gettext("OK"), + click: function(view) { + view.hide(); + } + } + } + }) + prompt.show(); } }) }) From 5738e4f79d5567fc0131f45a3bb4662de029b15a Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 25 Jul 2013 14:47:23 -0400 Subject: [PATCH 025/112] Pull correct URL when changing user permissions for course team --- cms/templates/manage_users.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/templates/manage_users.html b/cms/templates/manage_users.html index 76d0fa79d9..4cbfff2f51 100644 --- a/cms/templates/manage_users.html +++ b/cms/templates/manage_users.html @@ -257,7 +257,7 @@ } else { role = 'staff'; } - var url = $(this).closest("li").data("url"); + var url = $(this).closest("li[data-url]").data("url"); $.ajax({ url: url, type: 'POST', From ecf855eba7c377db14975e661dd192052cc62f36 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 25 Jul 2013 14:50:28 -0400 Subject: [PATCH 026/112] Fixup translations --- cms/templates/manage_users.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cms/templates/manage_users.html b/cms/templates/manage_users.html index 4cbfff2f51..6dc1561c9f 100644 --- a/cms/templates/manage_users.html +++ b/cms/templates/manage_users.html @@ -98,7 +98,7 @@

${user.username} - ${user.email} + ${user.email}

@@ -114,7 +114,7 @@ % if request.user.id != user.id: ## can't remove yourself
  • - ${_("Delete the user,")} ${user.username} + ${_("Delete the user, {username}").format(username=user.username)}
  • % endif From 41832744d78b323bd8b6d9cc5928ca2a4242a8b6 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 25 Jul 2013 14:50:55 -0400 Subject: [PATCH 027/112] Correct course team admin badging logic --- cms/templates/manage_users.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/templates/manage_users.html b/cms/templates/manage_users.html index 6dc1561c9f..e43ceaea59 100644 --- a/cms/templates/manage_users.html +++ b/cms/templates/manage_users.html @@ -68,7 +68,7 @@ ))}"> <% is_instuctor = is_user_in_course_group_role(user, context_course.location, 'instructor', check_staff=False) %> - % if is_instuctor and len(instructors) == 1: + % if is_instuctor: ${_("Current Role:")} From e5ef5ef1a0f54743e0a3d1019b014d95cfbb783d Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 25 Jul 2013 14:54:21 -0400 Subject: [PATCH 028/112] Show disabled trash icon instead of not showing it at all --- cms/templates/manage_users.html | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cms/templates/manage_users.html b/cms/templates/manage_users.html index e43ceaea59..7ab8e912f7 100644 --- a/cms/templates/manage_users.html +++ b/cms/templates/manage_users.html @@ -112,11 +112,9 @@ ${_("Remove Admin Access") if is_instuctor else _("Add Admin Access")} % endif - % if request.user.id != user.id: ## can't remove yourself -
  • +
  • ${_("Delete the user, {username}").format(username=user.username)}
  • - % endif % endif From deced24b3284499ce099deadf8decf0a3b52f91c Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Thu, 25 Jul 2013 16:41:53 -0400 Subject: [PATCH 029/112] Studio: refactored form-based Sass and revised markup/copy for course team admin mgmt --- cms/static/sass/elements/_forms.scss | 147 +++++++++++++++++++-------- cms/static/sass/views/_users.scss | 80 +++++---------- cms/templates/manage_users.html | 59 ++++++----- 3 files changed, 160 insertions(+), 126 deletions(-) diff --git a/cms/static/sass/elements/_forms.scss b/cms/static/sass/elements/_forms.scss index 3d11db02c3..55048b49dc 100644 --- a/cms/static/sass/elements/_forms.scss +++ b/cms/static/sass/elements/_forms.scss @@ -93,36 +93,47 @@ form { } } -// ==================== -// forms - archetype - add a new X form -.new-form { - @extend .ui-window; - @include box-sizing(border-box); - position: relative; - width: 100%; - margin-bottom: ($baseline*2); - border-radius: 2px; - background: $white; +// ELEM: form wrapper +.wrapper-create-element { + height: 0; + margin-bottom: $baseline; + opacity: 0.0; + pointer-events: none; + overflow: hidden; - .wrapper-form { - padding: $baseline ($baseline*1.5); + &.animate { + @include transition(opacity $tmg-f1 ease-in-out 0s, height $tmg-f1 ease-in-out 0s); } + &.is-shown { + height: auto; // define a specific height for the animating version of this UI to work properly + opacity: 1.0; + pointer-events: auto; + } +} + +// ELEM: form +// form styling for creating a new content item (course, user, textbook) +form[class^="create-"] { + @extend .ui-window; + @include box-sizing(border-box); + border-radius: 2px; + width: 100%; + background: $white; + .title { @extend .t-title4; - font-weight: 700; - margin-bottom: $baseline; - border-bottom: 1px solid $gray-l4; - padding-bottom: ($baseline/2); + font-weight: 600; + padding: $baseline ($baseline*1.5) 0 ($baseline*1.5); } fieldset { - margin-bottom: $baseline; + padding: $baseline ($baseline*1.5); } - // form elements - need to make this more universal - .form-fields { + + .list-input { @extend .cont-no-list; .field { @@ -150,7 +161,7 @@ form { label { @extend .t-copy-sub1; - @include transition(color, 0.15s, ease-in-out); + @include transition(color $tmg-f3 ease-in-out 0s); margin: 0 0 ($baseline/4) 0; &.is-focused { @@ -158,8 +169,9 @@ form { } } - //this section is borrowed from _account.scss - we should clean up and unify later + input, textarea { + @include transition(all $tmg-f2 ease-in-out 0s); @extend .t-copy-base; height: 100%; width: 100%; @@ -173,23 +185,8 @@ form { 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; } @@ -219,45 +216,111 @@ form { color: $gray-l3; } - &.error { + .tip-error { + display: none; + float: none; + } + &.error { label { color: $red; } + .tip-error { + @extend .anim-fadeIn; + display: block; + color: $red; + } + input { border-color: $red; } } } + + .field-inline { + + input, textarea, select { + width: 62%; + display: inline-block; + } + + .tip-stacked { + display: inline-block; + float: right; + width: 35%; + margin-top: 0; + } + + &.error { + .tip-error { + } + } + + } + + .field-group { + @include clearfix(); + margin: 0 0 ($baseline/2) 0; + + .field { + display: block; + width: 47%; + border-bottom: none; + margin: 0 ($baseline*0.75) 0 0; + padding: ($baseline/4) 0 0 0; + float: left; + position: relative; + + &:nth-child(odd) { + float: left; + } + + &:nth-child(even) { + float: right; + margin-right: 0; + } + + input, textarea { + width: 100%; + } + } + } } .actions { - border-top: 1px solid $gray-l1; - padding: ($baseline*0.75) $baseline; box-shadow: inset 0 1px 2px $shadow; + margin-top: ($baseline*0.75); + border-top: 1px solid $gray-l1; + padding: ($baseline*0.75) ($baseline*1.5); background: $gray-l6; .action-primary { @include blue-button; @extend .t-action2; - @include transition(all $tmg-f2 linear 0); + @include transition(all .15s); display: inline-block; padding: ($baseline/5) $baseline; font-weight: 600; + text-transform: uppercase; } .action-secondary { @include grey-button; @extend .t-action2; - @include transition(all $tmg-f2 linear 0); + @include transition(all .15s); display: inline-block; padding: ($baseline/5) $baseline; font-weight: 600; + text-transform: uppercase; } } } + + + + // ==================== // forms - grandfathered diff --git a/cms/static/sass/views/_users.scss b/cms/static/sass/views/_users.scss index 514536ccca..633385309b 100644 --- a/cms/static/sass/views/_users.scss +++ b/cms/static/sass/views/_users.scss @@ -3,7 +3,7 @@ body.course.users { - // page layout + // LAYOUT: page .content-primary, .content-supplementary { @include box-sizing(border-box); float: left; @@ -18,7 +18,7 @@ body.course.users { width: flex-grid(3, 12); } - // content + // ELEM: content .content { .introduction { @@ -28,60 +28,15 @@ body.course.users { } - // new user form - .add-user { - @extend .new-form; - display: none; + // ELEM: new user form + .wrapper-create-user { &.is-shown { - display: block; + height: ($baseline*15); } } - // new user form (old) - .new-user-form { - display: none; - padding: ($baseline*0.75) $baseline; - background-color: $lightBluishGrey2; - - #result { - display: none; - float: left; - margin-bottom: ($baseline*0.75); - padding: 3px ($baseline*0.75); - border-radius: 3px; - background: $red; - font-size: 14px; - color: $white; - } - - .form-elements { - clear: both; - } - - label { - display: inline-block; - margin-right: ($baseline/2); - } - - .email-input { - width: 350px; - padding: 8px 8px 10px; - border-color: $gray-d1; - } - - .add-button { - @include blue-button; - padding: ($baseline/4) $baseline 9px; - } - - .cancel-button { - @include white-button; - padding: ($baseline/4) $baseline 9px; - } - } - - // listing of users + // ELEM: listing of users .user-list, .user-item, .item-metadata, .item-actions { @include box-sizing(border-box); } @@ -94,7 +49,7 @@ body.course.users { position: relative; width: flex-grid(9, 9); margin: 0 0 ($baseline/2) 0; - padding: $baseline ($baseline*1.5) $baseline ($baseline*1.5); + padding: ($baseline*1.25) ($baseline*1.5) $baseline ($baseline*1.5); &:last-child { margin-bottom: 0; @@ -105,7 +60,7 @@ body.course.users { vertical-align: middle; } - // item - flag + // ELEM: item - flag .flag-role { @extend .ui-badge; color: $white; @@ -130,7 +85,7 @@ body.course.users { } } - // item - metadata + // ELEM: item - metadata .item-metadata { width: flex-grid(5, 9); margin-right: flex-gutter(); @@ -153,7 +108,7 @@ body.course.users { } } - // item - actions + // ELEM: item - actions .item-actions { width: flex-grid(4, 9); position: static; // nasty reset needed due to base.scss @@ -170,20 +125,26 @@ body.course.users { .action-delete { + // STATE: disabled + &.is-disabled { + opacity: 0.0; + visibility: hidden; + pointer-events: none; + } } .delete { @extend .ui-btn-non; } - // nasty reset needed due to base.scss + // HACK: nasty reset needed due to base.scss .delete-button { margin-right: 0; float: none; color: inherit; } - // admin role controls + // ELEM: admin role controls .toggle-admin-role { &.add-admin-role { @@ -204,6 +165,11 @@ body.course.users { font-weight: 600; } } + + .notoggleforyou { + @extend .t-copy-sub2; + color: $gray-l2; + } } // STATE: hover diff --git a/cms/templates/manage_users.html b/cms/templates/manage_users.html index 7ab8e912f7..48b68a2731 100644 --- a/cms/templates/manage_users.html +++ b/cms/templates/manage_users.html @@ -19,7 +19,7 @@ @@ -38,24 +38,28 @@
    %if allow_actions: -
    -
    -

    ${_("Add a User to Your Course's Team")}

    +
    + +
    +

    ${_("Add a User to Your Course's Team")}

    -
    - ${_("Textbook information")} -
    - - - ${_("Please provide the email address of the course staff member you'd like to add")} -
    -
    -
    -
    - - -
    - +
    + ${_("Textbook information")} +
      +
    1. + + + ${_("Please provide the email address of the course staff member you'd like to add")} +
    2. +
    +
    +
    +
    + + +
    + +
    %endif
      @@ -107,7 +111,7 @@
    - % if is_instuctor and len(instructors) == 1: + <% user_is_instuctor = is_user_in_course_group_role(request.user, context_course.location, 'instructor', check_staff=False) %> + % if user_is_instuctor and len(instructors) == 1:

    ${_("Tranferring Ownership")}

    ${_("[Mark to provide copy] Maecenas faucibus mollis interdum. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.")}

    From 0bd25c05e2f9b1a828b337ffd39c653c8e26c86c Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 29 Jul 2013 12:04:58 -0400 Subject: [PATCH 031/112] Fix up lettuce tests for course team page redesign --- cms/djangoapps/contentstore/features/course-team.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/cms/djangoapps/contentstore/features/course-team.py b/cms/djangoapps/contentstore/features/course-team.py index ad5d31977c..cad07a990e 100644 --- a/cms/djangoapps/contentstore/features/course-team.py +++ b/cms/djangoapps/contentstore/features/course-team.py @@ -22,20 +22,22 @@ def create_other_user(_step, name): @step(u'I add "([^"]*)" to the course team') def add_other_user(_step, name): - new_user_css = 'a.new-user-button' + new_user_css = 'a.create-user-button' world.css_click(new_user_css) + world.wait(0.5) - email_css = 'input.email-input' + email_css = 'input#user-email-input' f = world.css_find(email_css) f._element.send_keys(name, EMAIL_EXTENSION) - confirm_css = '#add_user' + confirm_css = 'form.create-user button.action-primary' world.css_click(confirm_css) @step(u'I delete "([^"]*)" from the course team') def delete_other_user(_step, name): - to_delete_css = 'a.remove-user[data-id="{name}{extension}"]'.format(name=name, extension=EMAIL_EXTENSION) + to_delete_css = '.user-item .item-actions a.remove-user[data-id="{email}"]'.format( + email="{0}{1}".format(name, EMAIL_EXTENSION)) world.css_click(to_delete_css) @@ -63,5 +65,5 @@ def cannot_delete(_step): @step(u's?he cannot add users') def cannot_add(_step): - add_css = 'a.new-user' + add_css = 'a.create-user-button' assert world.is_css_not_present(add_css) From e4bd8622d9ab9ccb9a2eda4a9f261eda85581037 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 29 Jul 2013 12:28:13 -0400 Subject: [PATCH 032/112] Updated changelog for course team admin story --- CHANGELOG.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 51a98f2de7..66b0bc6cb1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,9 @@ the setting is not present, the API is disabled). LMS: Added endpoints for AJAX requests to enable/disable notifications (which are not yet implemented) and a one-click unsubscribe page. +Studio: Allow instructors of a course to designate other staff as instructors; +this allows instructors to hand off management of a course to someone else. + Common: Add a manage.py that knows about edx-platform specific settings and projects Common: Added *experimental* support for jsinput type. From 64566c14e67e2abc10578d029017520449c4290f Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 29 Jul 2013 12:50:46 -0400 Subject: [PATCH 033/112] Fix unit tests --- cms/djangoapps/contentstore/views/user.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/views/user.py b/cms/djangoapps/contentstore/views/user.py index 7e1cca2370..a0db8ecef8 100644 --- a/cms/djangoapps/contentstore/views/user.py +++ b/cms/djangoapps/contentstore/views/user.py @@ -12,10 +12,11 @@ from mitxmako.shortcuts import render_to_response from django.core.context_processors import csrf from xmodule.modulestore.django import modulestore +from xmodule.modulestore import Location from contentstore.utils import get_lms_link_for_item from util.json_request import JsonResponse from auth.authz import ( - STAFF_ROLE_NAME, INSTRUCTOR_ROLE_NAME, get_users_in_course_group_by_role, + STAFF_ROLE_NAME, INSTRUCTOR_ROLE_NAME, add_user_to_course_group, remove_user_from_course_group, get_course_groupname_for_role) from course_creators.views import get_course_creator_status, add_user_with_status_unrequested, user_requested_access From 20ce33dcdbb2658993e1be8c6f20c6d71f09331f Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Mon, 29 Jul 2013 13:38:27 -0400 Subject: [PATCH 034/112] Fixed broken tests from chrome versioning --- cms/djangoapps/contentstore/features/common.py | 3 +++ .../contentstore/features/component_settings_editor_helpers.py | 2 +- cms/djangoapps/contentstore/features/upload.py | 2 +- cms/djangoapps/contentstore/features/video-editor.py | 1 + lms/djangoapps/courseware/features/homepage.py | 2 +- 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index d357c8ae96..6fe63e68a5 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -242,7 +242,9 @@ def save_button_disabled(step): @step('I confirm the prompt') def confirm_the_prompt(step): prompt_css = 'a.button.action-primary' + world.wait_for(lambda _driver: world.css_visible(prompt_css)) world.css_click(prompt_css) + world.wait_for(lambda _driver: not world.css_visible(prompt_css)) @step(u'I am shown a (.*)$') @@ -252,6 +254,7 @@ def i_am_shown_a_notification(step, notification_type): def type_in_codemirror(index, text): world.css_click(".CodeMirror", index=index) + world.browser.execute_script("$('div.CodeMirror.CodeMirror-focused > div').css('overflow', '')") g = world.css_find("div.CodeMirror.CodeMirror-focused > div > textarea") if world.is_mac(): g._element.send_keys(Keys.COMMAND + 'a') diff --git a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py index 2b206e4466..225f654fea 100644 --- a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py +++ b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py @@ -39,7 +39,7 @@ def click_component_from_menu(category, boilerplate, expected_css): elem_css = "a[data-category='{}']:not([data-boilerplate])".format(category) elements = world.css_find(elem_css) assert_equal(len(elements), 1) - world.css_click(elem_css) + world.css_click(elem_css, success_condition=lambda: 1 == len(world.css_find(expected_css))) @world.absorb diff --git a/cms/djangoapps/contentstore/features/upload.py b/cms/djangoapps/contentstore/features/upload.py index 0c700956e3..df63b26b3b 100644 --- a/cms/djangoapps/contentstore/features/upload.py +++ b/cms/djangoapps/contentstore/features/upload.py @@ -58,7 +58,7 @@ def delete_file(_step, file_name): world.css_click(delete_css, index=index) prompt_confirm_css = 'li.nav-item > a.action-primary' - world.css_click(prompt_confirm_css) + world.css_click(prompt_confirm_css, success_condition=lambda: not world.css_visible(prompt_confirm_css)) @step(u'I should see only one "([^"]*)"$') diff --git a/cms/djangoapps/contentstore/features/video-editor.py b/cms/djangoapps/contentstore/features/video-editor.py index 93d638e621..6113f42c91 100644 --- a/cms/djangoapps/contentstore/features/video-editor.py +++ b/cms/djangoapps/contentstore/features/video-editor.py @@ -19,5 +19,6 @@ def i_see_the_correct_settings_and_values(step): @step('I have set "show captions" to (.*)') def set_show_captions(step, setting): world.css_click('a.edit-button') + world.wait_for(lambda _driver: world.css_visible('a.save-button')) world.browser.select('Show Captions', setting) world.css_click('a.save-button') diff --git a/lms/djangoapps/courseware/features/homepage.py b/lms/djangoapps/courseware/features/homepage.py index 585d1582d7..51c3277e69 100644 --- a/lms/djangoapps/courseware/features/homepage.py +++ b/lms/djangoapps/courseware/features/homepage.py @@ -8,7 +8,7 @@ from nose.tools import assert_in, assert_equals @step(u'I should see the following Partners in the Partners section') def i_should_see_partner(step): partners = world.browser.find_by_css(".partner .name span") - names = set(span.text for span in partners) + names = set(span.html for span in partners) for partner in step.hashes: assert_in(partner['Partner'], names) From 3bc34d71a9a99f3af2d9e98b7dc5b83749ca9a7b Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Mon, 29 Jul 2013 13:52:02 -0400 Subject: [PATCH 035/112] Changed version requirement in testing.md --- doc/testing.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/testing.md b/doc/testing.md index cfca297f2b..b060336d1d 100644 --- a/doc/testing.md +++ b/doc/testing.md @@ -186,8 +186,8 @@ uses [Selenium](http://docs.seleniumhq.org/) to control the Chrome browser. **Prerequisite**: You must have [ChromeDriver](https://code.google.com/p/selenium/wiki/ChromeDriver) installed to run the tests in Chrome. The tests are confirmed to run -with Chrome (not Chromium) version 26.0.0.1410.63 with ChromeDriver -version r195636. +with Chrome (not Chromium) version 28.0.1500.71 with ChromeDriver +version 2.1.210398. To run all the acceptance tests: From eac14615cd63a9ad24442ac5a74a1851a60cdd07 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Wed, 24 Jul 2013 16:15:30 -0400 Subject: [PATCH 036/112] Load advanced_entry template on page, rather than via AJAX --- cms/static/js/views/settings/advanced_view.js | 16 ++++------------ cms/templates/js/advanced_entry.underscore | 11 +++++++++++ cms/templates/settings_advanced.html | 14 ++++++++------ 3 files changed, 23 insertions(+), 18 deletions(-) create mode 100644 cms/templates/js/advanced_entry.underscore diff --git a/cms/static/js/views/settings/advanced_view.js b/cms/static/js/views/settings/advanced_view.js index 90e84adf2b..5ae0c19570 100644 --- a/cms/static/js/views/settings/advanced_view.js +++ b/cms/static/js/views/settings/advanced_view.js @@ -11,16 +11,9 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ // TODO enable/disable save based on validation (currently enabled whenever there are changes) }, initialize : function() { - var self = this; - // instantiates an editor template for each update in the collection - window.templateLoader.loadRemoteTemplate("advanced_entry", - "/static/client_templates/advanced_entry.html", - function (raw_template) { - self.template = _.template(raw_template); - self.render(); - } - ); + this.template = _.template($("#advanced_entry-tpl").text()); this.listenTo(this.model, 'invalid', this.handleValidationError); + this.render(); }, render: function() { // catch potential outside call before template loaded @@ -56,7 +49,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ CodeMirror.fromTextArea(textarea, { mode: "application/json", lineNumbers: false, lineWrapping: false, onChange: function(instance, changeobj) { - instance.save() + instance.save(); // this event's being called even when there's no change :-( if (instance.getValue() !== oldValue) { var message = gettext("Your changes will not take effect until you save your progress. Take care with key and value formatting, as validation is not implemented."); @@ -105,8 +98,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ // call validateKey on each to ensure proper format // check for dupes var self = this; - this.model.save({}, - { + this.model.save({}, { success : function() { self.render(); var title = gettext("Your policy changes have been saved."); diff --git a/cms/templates/js/advanced_entry.underscore b/cms/templates/js/advanced_entry.underscore new file mode 100644 index 0000000000..26b1a386f6 --- /dev/null +++ b/cms/templates/js/advanced_entry.underscore @@ -0,0 +1,11 @@ +
  • +
    + + +
    + +
    + + +
    +
  • diff --git a/cms/templates/settings_advanced.html b/cms/templates/settings_advanced.html index e1b1913c87..0409b1a9e4 100644 --- a/cms/templates/settings_advanced.html +++ b/cms/templates/settings_advanced.html @@ -1,15 +1,17 @@ -<%! from django.utils.translation import ugettext as _ %> <%inherit file="base.html" /> +<%namespace name='static' file='static_content.html'/> <%! from django.core.urlresolvers import reverse %> +<%! from django.utils.translation import ugettext as _ %> +<%! from contentstore import utils %> <%block name="title">${_("Advanced Settings")} <%block name="bodyclass">is-signedin course advanced settings -<%namespace name='static' file='static_content.html'/> -<%! -from contentstore import utils -%> - <%block name="jsextra"> +% for template_name in ["advanced_entry"]: + +% endfor From 97fb08ab0829b6d15fcbf31fcacb65e19ed1097d Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 25 Jul 2013 12:56:37 -0400 Subject: [PATCH 037/112] Make mitxmako.render_to_response not require a dictionary argument --- common/djangoapps/mitxmako/shortcuts.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/djangoapps/mitxmako/shortcuts.py b/common/djangoapps/mitxmako/shortcuts.py index 7f7c3f9ebe..3c68fa85be 100644 --- a/common/djangoapps/mitxmako/shortcuts.py +++ b/common/djangoapps/mitxmako/shortcuts.py @@ -92,9 +92,10 @@ def render_to_string(template_name, dictionary, context=None, namespace='main'): return template.render_unicode(**context_dictionary) -def render_to_response(template_name, dictionary, context_instance=None, namespace='main', **kwargs): +def render_to_response(template_name, dictionary=None, context_instance=None, namespace='main', **kwargs): """ Returns a HttpResponse whose content is filled with the result of calling lookup.get_template(args[0]).render with the passed arguments. """ + dictionary = dictionary or {} return HttpResponse(render_to_string(template_name, dictionary, context_instance, namespace), **kwargs) From 5b9e2835529278b1164d70b5b464eca9a6f2ec7c Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 25 Jul 2013 17:02:50 -0400 Subject: [PATCH 038/112] Load course info update template on page instead of via AJAX --- cms/static/js/views/course_info_edit.js | 11 +---------- cms/templates/course_info.html | 7 +++++++ .../js/course_info_update.underscore} | 0 3 files changed, 8 insertions(+), 10 deletions(-) rename cms/{static/client_templates/course_info_update.html => templates/js/course_info_update.underscore} (100%) diff --git a/cms/static/js/views/course_info_edit.js b/cms/static/js/views/course_info_edit.js index ecd9ebe78d..ae0a66d45b 100644 --- a/cms/static/js/views/course_info_edit.js +++ b/cms/static/js/views/course_info_edit.js @@ -34,16 +34,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ }, initialize: function() { - var self = this; - // instantiates an editor template for each update in the collection - window.templateLoader.loadRemoteTemplate("course_info_update", - // TODO Where should the template reside? how to use the static.url to create the path? - "/static/client_templates/course_info_update.html", - function (raw_template) { - self.template = _.template(raw_template); - self.render(); - } - ); + this.template = _.template($("#course_info_update-tpl").text()); // when the client refetches the updates as a whole, re-render them this.listenTo(this.collection, 'reset', this.render); }, diff --git a/cms/templates/course_info.html b/cms/templates/course_info.html index dcfffd1d5a..eca3628b50 100644 --- a/cms/templates/course_info.html +++ b/cms/templates/course_info.html @@ -6,6 +6,13 @@ <%block name="title">${_("Course Updates")} <%block name="bodyclass">is-signedin course course-info updates +<%block name="header_extras"> +% for template_name in ["course_info_update"]: + +% endfor + <%block name="jsextra"> diff --git a/cms/static/client_templates/course_info_update.html b/cms/templates/js/course_info_update.underscore similarity index 100% rename from cms/static/client_templates/course_info_update.html rename to cms/templates/js/course_info_update.underscore From 372992244c8f68c37964fc7b3716bbb22739167f Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 25 Jul 2013 17:26:52 -0400 Subject: [PATCH 039/112] Removed course_info_handouts AJAX load --- cms/static/js/views/course_info_edit.js | 9 ++------- cms/templates/course_info.html | 3 +-- .../js/course_info_handouts.underscore} | 0 3 files changed, 3 insertions(+), 9 deletions(-) rename cms/{static/client_templates/course_info_handouts.html => templates/js/course_info_handouts.underscore} (100%) diff --git a/cms/static/js/views/course_info_edit.js b/cms/static/js/views/course_info_edit.js index ae0a66d45b..cb91422143 100644 --- a/cms/static/js/views/course_info_edit.js +++ b/cms/static/js/views/course_info_edit.js @@ -232,16 +232,11 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({ }, initialize: function() { + this.template = _.template($("#course_info_handouts-tpl").text()); var self = this; this.model.fetch({ complete: function() { - window.templateLoader.loadRemoteTemplate("course_info_handouts", - "/static/client_templates/course_info_handouts.html", - function (raw_template) { - self.template = _.template(raw_template); - self.render(); - } - ); + self.render(); }, reset: true }); diff --git a/cms/templates/course_info.html b/cms/templates/course_info.html index eca3628b50..03f4c35d14 100644 --- a/cms/templates/course_info.html +++ b/cms/templates/course_info.html @@ -7,7 +7,7 @@ <%block name="bodyclass">is-signedin course course-info updates <%block name="header_extras"> -% for template_name in ["course_info_update"]: +% for template_name in ["course_info_update", "course_info_handouts"]: @@ -15,7 +15,6 @@ <%block name="jsextra"> - diff --git a/cms/static/client_templates/course_info_handouts.html b/cms/templates/js/course_info_handouts.underscore similarity index 100% rename from cms/static/client_templates/course_info_handouts.html rename to cms/templates/js/course_info_handouts.underscore From 419207ac3ddfba0e5efb9ed2d269c75296ee74f9 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Mon, 29 Jul 2013 14:49:50 -0400 Subject: [PATCH 040/112] Fixed issues with problem-editor.feature --- cms/djangoapps/contentstore/features/common.py | 4 +--- .../features/component_settings_editor_helpers.py | 6 ++++++ cms/djangoapps/contentstore/features/problem-editor.py | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 6fe63e68a5..712983bfe8 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -242,9 +242,7 @@ def save_button_disabled(step): @step('I confirm the prompt') def confirm_the_prompt(step): prompt_css = 'a.button.action-primary' - world.wait_for(lambda _driver: world.css_visible(prompt_css)) - world.css_click(prompt_css) - world.wait_for(lambda _driver: not world.css_visible(prompt_css)) + world.css_click(prompt_css, success_condition=lambda: not world.css_visible(prompt_css)) @step(u'I am shown a (.*)$') diff --git a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py index 225f654fea..5691081977 100644 --- a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py +++ b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py @@ -13,6 +13,10 @@ def create_component_instance(step, component_button_css, category, click_new_component_button(step, component_button_css) + def animation_done(_driver): + return world.browser.evaluate_script("$('div.new-component').css('display')") == 'none' + world.wait_for(animation_done) + if has_multiple_templates: click_component_from_menu(category, boilerplate, expected_css) @@ -39,11 +43,13 @@ def click_component_from_menu(category, boilerplate, expected_css): elem_css = "a[data-category='{}']:not([data-boilerplate])".format(category) elements = world.css_find(elem_css) assert_equal(len(elements), 1) + world.wait_for(lambda _driver: world.css_visible(elem_css)) world.css_click(elem_css, success_condition=lambda: 1 == len(world.css_find(expected_css))) @world.absorb def edit_component_and_select_settings(): + world.wait_for(lambda _driver: world.css_visible('a.edit-button')) world.css_click('a.edit-button') world.css_click('#settings-mode') diff --git a/cms/djangoapps/contentstore/features/problem-editor.py b/cms/djangoapps/contentstore/features/problem-editor.py index 565a35f802..8ffa866236 100644 --- a/cms/djangoapps/contentstore/features/problem-editor.py +++ b/cms/djangoapps/contentstore/features/problem-editor.py @@ -156,7 +156,7 @@ def cancel_does_not_save_changes(step): def create_latex_problem(step): world.click_new_component_button(step, '.large-problem-icon') # Go to advanced tab. - world.css_click('#ui-id-2') + world.css_click('#ui-id-2', success_condition=lambda: world.css_has_class('div.ui-tabs li', 'ui-state-active', index=1)) world.click_component_from_menu("problem", "latex_problem.yaml", '.xmodule_CapaModule') From ee46b3ef24d345f14b44e30d207725c4916e1196 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Mon, 29 Jul 2013 15:58:23 -0400 Subject: [PATCH 041/112] One click categories do not have animations --- .../features/component_settings_editor_helpers.py | 9 +++++---- cms/djangoapps/contentstore/features/problem-editor.py | 6 +++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py index 5691081977..8c6a1023c9 100644 --- a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py +++ b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py @@ -12,16 +12,17 @@ def create_component_instance(step, component_button_css, category, has_multiple_templates=True): click_new_component_button(step, component_button_css) - - def animation_done(_driver): - return world.browser.evaluate_script("$('div.new-component').css('display')") == 'none' - world.wait_for(animation_done) + if category == 'problem' or category == 'html': + def animation_done(_driver): + return world.browser.evaluate_script("$('div.new-component').css('display')") == 'none' + world.wait_for(animation_done) if has_multiple_templates: click_component_from_menu(category, boilerplate, expected_css) assert_equal(1, len(world.css_find(expected_css))) + @world.absorb def click_new_component_button(step, component_button_css): step.given('I have clicked the new unit button') diff --git a/cms/djangoapps/contentstore/features/problem-editor.py b/cms/djangoapps/contentstore/features/problem-editor.py index 8ffa866236..e97e2857a5 100644 --- a/cms/djangoapps/contentstore/features/problem-editor.py +++ b/cms/djangoapps/contentstore/features/problem-editor.py @@ -155,8 +155,12 @@ def cancel_does_not_save_changes(step): @step('I have created a LaTeX Problem') def create_latex_problem(step): world.click_new_component_button(step, '.large-problem-icon') + + def animation_done(_driver): + return world.browser.evaluate_script("$('div.new-component').css('display')") == 'none' + world.wait_for(animation_done) # Go to advanced tab. - world.css_click('#ui-id-2', success_condition=lambda: world.css_has_class('div.ui-tabs li', 'ui-state-active', index=1)) + world.css_click('#ui-id-2') world.click_component_from_menu("problem", "latex_problem.yaml", '.xmodule_CapaModule') From 6980e9f17503c023519c4cdae5fbe36f4c21b4a1 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Mon, 29 Jul 2013 16:36:45 -0400 Subject: [PATCH 042/112] Studio: bulletproofs actions on course team view --- cms/static/sass/views/_users.scss | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cms/static/sass/views/_users.scss b/cms/static/sass/views/_users.scss index 633385309b..c3a7d2cd5d 100644 --- a/cms/static/sass/views/_users.scss +++ b/cms/static/sass/views/_users.scss @@ -120,10 +120,12 @@ body.course.users { } .action-role { - margin-right: ($baseline/2); + width: flex-grid(3, 4); + margin-right: flex-gutter(); } .action-delete { + width: flex-grid(1, 4); // STATE: disabled &.is-disabled { @@ -167,7 +169,7 @@ body.course.users { } .notoggleforyou { - @extend .t-copy-sub2; + @extend .t-copy-sub1; color: $gray-l2; } } From 3d8b32bb6963ddeb7ada9283ec13ba6d62b34b1d Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Mon, 29 Jul 2013 17:18:52 -0400 Subject: [PATCH 043/112] Reverting 89c48f4a3047 to unbreak codejail for this release. --- lms/envs/common.py | 2 -- requirements/edx/github.txt | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lms/envs/common.py b/lms/envs/common.py index a0cc96c49d..e74c71bc3c 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -317,8 +317,6 @@ CODE_JAIL = { 'limits': { # How many CPU seconds can jailed code use? 'CPU': 1, - # How large a file can jailed code write? - 'FSIZE': 50000, }, } diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt index 97bc325d35..0d77dac179 100644 --- a/requirements/edx/github.txt +++ b/requirements/edx/github.txt @@ -9,5 +9,5 @@ # Our libraries: -e git+https://github.com/edx/XBlock.git@b697bebd45deebd0f868613fab6722a0460ca0c1#egg=XBlock --e git+https://github.com/edx/codejail.git@c08967fb44d1bcdb259d3ec58812e3ac592539c2#egg=codejail +-e git+https://github.com/edx/codejail.git@0a1b468#egg=codejail -e git+https://github.com/edx/diff-cover.git@v0.2.0#egg=diff_cover From 611dab077f849acea1e85bcbed86c3ea6a335dfe Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Mon, 29 Jul 2013 17:29:15 -0400 Subject: [PATCH 044/112] Studio: places 'add team member' call to course team view --- cms/static/sass/elements/_system-help.scss | 32 ++++++++++++++++++++-- cms/static/sass/views/_users.scss | 30 ++++++++++++++++++++ cms/templates/manage_users.html | 23 ++++++++++++++-- common/static/sass/_mixins.scss | 2 +- 4 files changed, 81 insertions(+), 6 deletions(-) diff --git a/cms/static/sass/elements/_system-help.scss b/cms/static/sass/elements/_system-help.scss index 3b33946e19..c5fcc6a0ec 100644 --- a/cms/static/sass/elements/_system-help.scss +++ b/cms/static/sass/elements/_system-help.scss @@ -55,8 +55,8 @@ margin-bottom: $baseline; .title { - @extend .t-title7; - margin-bottom: ($baseline/4); + @extend .t-title6; + margin-bottom: ($baseline/2); font-weight: 700; } @@ -167,6 +167,34 @@ } } +// particular notice - create +.notice-create { + background-color: $gray-l4; + + .title { + color: $gray-d2; + } + + .copy { + color: $gray-d2; + } + + &.has-actions { + + .list-actions { + + .action-item { + + } + + .action-primary { + @extend .btn-primary-green; + @extend .t-action3; + } + } + } +} + // particular notice - confirmation .notice-confirmation { background-color: $green-l5; diff --git a/cms/static/sass/views/_users.scss b/cms/static/sass/views/_users.scss index c3a7d2cd5d..30328540a0 100644 --- a/cms/static/sass/views/_users.scss +++ b/cms/static/sass/views/_users.scss @@ -27,6 +27,36 @@ body.course.users { } } + // ELEM: no users notice + .content .notice-create { + width: flexgrid(9, 9); + margin-top: $baseline; + + // CASE: notice has actions { + &.has-actions { + + .msg, .list-actions { + display: inline-block; + vertical-align: middle; + } + + .msg { + width: flex-grid(6, 9); + margin-right: flex-gutter(); + } + + .list-actions { + width: flex-grid(3, 9); + text-align: right; + margin-top: 0; + + .action-item { + + } + } + } + } + // ELEM: new user form .wrapper-create-user { diff --git a/cms/templates/manage_users.html b/cms/templates/manage_users.html index e9fccac80a..f412661c4f 100644 --- a/cms/templates/manage_users.html +++ b/cms/templates/manage_users.html @@ -19,7 +19,7 @@ @@ -125,6 +125,23 @@ % endfor + + %if allow_actions: +
    +
    +

    ${_('Add Team Members to This Course')}

    +
    +

    ${_('[Mark to provide copy] Adding team members helps you author and maintain your course. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.')}

    +
    +
    + + +
    + %endif