From 22d996fca58144230529b0c2c7867681311d0af4 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 9 Jul 2014 20:14:30 -0400 Subject: [PATCH 01/11] Studio: course outline revised styling with reference templates --- cms/static/sass/_developer.scss | 75 -- cms/static/sass/_variables.scss | 11 + cms/static/sass/assets/_anims.scss | 24 + cms/static/sass/elements/_controls.scss | 37 +- cms/static/sass/elements/_layout.scss | 10 +- cms/static/sass/elements/_modules.scss | 286 +++++++ cms/static/sass/elements/_typography.scss | 17 + cms/static/sass/views/_outline.scss | 617 +++++--------- cms/templates/ux/reference/outline.html | 768 ++++++++++++++++++ .../ux/reference/outline_add-section.html | 6 + .../ux/reference/outline_add-subsection.html | 6 + .../ux/reference/outline_add-unit.html | 6 + .../outline_section_header-collapsed.html | 36 + .../outline_section_header-expanded.html | 36 + .../ux/reference/outline_status_grading.html | 8 + .../outline_status_message-error.html | 4 + .../outline_status_message-lock.html | 4 + ...ne_status_message-unpublished_changes.html | 4 + ...line_status_message-unpublished_units.html | 4 + .../outline_status_release-draft.html | 10 + .../outline_status_release-lock.html | 9 + .../outline_status_release-released.html | 10 + ...e_status_release-released_with_parent.html | 9 + .../outline_status_release-scheduled.html | 9 + ..._status_release-scheduled_with_parent.html | 9 + .../outline_subsection_header-collapsed.html | 42 + .../outline_subsection_header-expanded.html | 42 + .../ux/reference/outline_unit_header.html | 32 + common/static/sass/_mixins.scss | 10 +- 29 files changed, 1652 insertions(+), 489 deletions(-) create mode 100644 cms/templates/ux/reference/outline.html create mode 100644 cms/templates/ux/reference/outline_add-section.html create mode 100644 cms/templates/ux/reference/outline_add-subsection.html create mode 100644 cms/templates/ux/reference/outline_add-unit.html create mode 100644 cms/templates/ux/reference/outline_section_header-collapsed.html create mode 100644 cms/templates/ux/reference/outline_section_header-expanded.html create mode 100644 cms/templates/ux/reference/outline_status_grading.html create mode 100644 cms/templates/ux/reference/outline_status_message-error.html create mode 100644 cms/templates/ux/reference/outline_status_message-lock.html create mode 100644 cms/templates/ux/reference/outline_status_message-unpublished_changes.html create mode 100644 cms/templates/ux/reference/outline_status_message-unpublished_units.html create mode 100644 cms/templates/ux/reference/outline_status_release-draft.html create mode 100644 cms/templates/ux/reference/outline_status_release-lock.html create mode 100644 cms/templates/ux/reference/outline_status_release-released.html create mode 100644 cms/templates/ux/reference/outline_status_release-released_with_parent.html create mode 100644 cms/templates/ux/reference/outline_status_release-scheduled.html create mode 100644 cms/templates/ux/reference/outline_status_release-scheduled_with_parent.html create mode 100644 cms/templates/ux/reference/outline_subsection_header-collapsed.html create mode 100644 cms/templates/ux/reference/outline_subsection_header-expanded.html create mode 100644 cms/templates/ux/reference/outline_unit_header.html diff --git a/cms/static/sass/_developer.scss b/cms/static/sass/_developer.scss index 30859b4094b3..f5c69e8e6b77 100644 --- a/cms/static/sass/_developer.scss +++ b/cms/static/sass/_developer.scss @@ -8,78 +8,3 @@ // } // -------------------- - -//.wrapper-xblock-header { - -.view-outline { - - .add-xblock-component { - text-align: center; - - .add-button { - padding: 5px 10px; - background-color: $blue; - color: $white; - text-align: center; - } - } - - .draggable-drop-indicator { - left: 0; - } - - .nav-actions { - .collapse-all { - .expand-all { - display: none; - } - } - - .expand-all { - .collapse-all { - display: none; - } - } - } - - .outline-item { - padding: 6px 8px 8px 16px; - text-wrap: avoid; - border: 1px solid $gray; - margin: 5px; - background-color: $white; - - .wrapper-xblock-header-secondary { - padding: 0px 8px 0px 26px; - - .meta-info { - font-size: 12px; - } - } - - .xblock-title { - width: 100%; - } - - .actions-list { - .action-item { - display: inline-block; - } - } - } - - .outline-item.collapsed { - .sortable-list, - .add-xblock-component { - display: none; - } - } - - .item-actions { - .configure-button { - float: left; - margin-right: 13px; - color: #a4aab7; - } - } -} diff --git a/cms/static/sass/_variables.scss b/cms/static/sass/_variables.scss index ce29dc96dc89..84cd91407b9b 100644 --- a/cms/static/sass/_variables.scss +++ b/cms/static/sass/_variables.scss @@ -160,6 +160,17 @@ $shadow-l2: rgba($black, 0.05); $shadow-d1: rgba($black, 0.4); $shadow-d2: rgba($black, 0.6); +// colors - application +$color-draft: $gray-l3; +$color-live: $blue; +$color-ready: $green; +$color-warning: $orange-l2; +$color-error: $red-l2; +$color-staff-only: $black; + +$color-heading-base: $gray-d2; +$color-copy-base: $gray-l1; + // ==================== // timing - used for animation/transition mixin syncing diff --git a/cms/static/sass/assets/_anims.scss b/cms/static/sass/assets/_anims.scss index 4726776d3998..cf3f49e59bd2 100644 --- a/cms/static/sass/assets/_anims.scss +++ b/cms/static/sass/assets/_anims.scss @@ -247,3 +247,27 @@ %anim-flashDouble { @include animation(flashDouble $tmg-f1 ease-in-out 1); } + + +// ==================== + + +// pulse +@include keyframes(pulse) { + 0% { + opacity: 0.0; + } + + 50% { + opacity: 1.0; + } + + 100% { + opacity: 0.0; + } +} + +// canned animation - use if you want out of the box/non-customized anim +%anim-pulse { + @include animation(pulse $tmg-f1 ease-in-out 1); +} diff --git a/cms/static/sass/elements/_controls.scss b/cms/static/sass/elements/_controls.scss index 163c128dfff2..f0f7179cb7e5 100644 --- a/cms/static/sass/elements/_controls.scss +++ b/cms/static/sass/elements/_controls.scss @@ -279,7 +279,7 @@ } } -// UI: elem is collapsible +// UI: elem is collapsible - TODO: this should be transitioned away from in favor of %ui-expand-collapse %expand-collapse { @include transition(all $tmg-f2 linear 0s); display: inline-block; @@ -305,6 +305,41 @@ } } +// UI: expand collapse +%ui-expand-collapse { + @include transition(all $tmg-f2 linear 0s); + + + // CASE: default (is expanded) + .ui-toggle-expansion { + @include transition(all $tmg-f2 ease-in-out 0s); + display: inline-block; + vertical-align: middle; + + .icon { + @include transition(all $tmg-f2 ease-in-out 0s); + } + + // STATE: hover/active + &:hover, &:active { + cursor: pointer; + color: $ui-link-color-focus; + } + } + + // CASE: is collapsed + &.is-collapsed { + + .ui-toggle-expansion { + + .icon { + @include transform(rotate(-90deg)); + @include transform-origin(50% 50%); + } + } + } +} + // UI: drag handles .drag-handle { diff --git a/cms/static/sass/elements/_layout.scss b/cms/static/sass/elements/_layout.scss index cbf615836bc4..764af355521d 100644 --- a/cms/static/sass/elements/_layout.scss +++ b/cms/static/sass/elements/_layout.scss @@ -77,10 +77,18 @@ vertical-align: baseline; } - &.new-button { + // CASE: new/create button + &.new-button, + &.button-new { @extend %btn-primary-green; @extend %sizing; } + + // CASE: toggle button + &.button-toggle { + @extend %btn-secondary-gray; + @extend %sizing; + } } } } diff --git a/cms/static/sass/elements/_modules.scss b/cms/static/sass/elements/_modules.scss index 725caf5e47dd..4e97a5f665a2 100644 --- a/cms/static/sass/elements/_modules.scss +++ b/cms/static/sass/elements/_modules.scss @@ -3,6 +3,7 @@ // Patterns for pieces of content - modules - used throughout the app // basic gray module with a strong top border and title, with related content box attached +// -------------------- %bar-module { margin-bottom: $baseline; border-top: 5px solid $gray-l1; @@ -37,6 +38,7 @@ } // blue bar and title bg version +// -------------------- %bar-module-blue { @extend %bar-module; border-top: 5px solid $blue; @@ -47,6 +49,7 @@ } // green bar and title bg version +// -------------------- %bar-module-green { @extend %bar-module; border-top: 5px solid $green; @@ -57,6 +60,7 @@ } // yellow bar and title bg version +// -------------------- %bar-module-yellow { @extend %bar-module; border-top: 5px solid $orange-l2; @@ -67,6 +71,7 @@ } // red bar and title bg version +// -------------------- %bar-module-red { @extend %bar-module; border-top: 5px solid $red-l2; @@ -88,6 +93,7 @@ // Add new component menu with big green buttons // outermost wrapper for add a new component menu +// -------------------- .add-xblock-component { margin: $baseline ($baseline/2); border: 1px solid $gray-l3; @@ -146,6 +152,7 @@ // outer most wrapper div for scroll up component picker menus // swaps in when a green button is clicked + // -------------------- .new-component-templates { @include clearfix; display: none; @@ -175,6 +182,7 @@ } // individual menus + // -------------------- .new-component-template { @include clearfix; @@ -207,6 +215,7 @@ } // basic and advanced problem tabs - also handled by jquery-ui tabs + // -------------------- .problem-type-tabs { @include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0)); list-style-type: none; @@ -252,4 +261,281 @@ } } +// outline UI +// -------------------- + +// outline: utilities +$outline-indent-width: $baseline; + +// UI: section +%outline-section { + @include transition(border-left-width $tmg-f2 linear 0s, border-left-color $tmg-f2 linear 0s); + border-left: 1px solid $color-draft; + + // STATE: is-collapsed + &.is-collapsed { + border-left-width: ($baseline/4); + + // CASE: is ready to be live + &.is-ready { + border-left-color: $color-ready; + } + + // CASE: is live + &.is-live { + border-left-color: $color-live; + } + + // CASE: has staff-only content + &.is-staff-only { + border-left-color: $color-staff-only; + } + + // CASE: has unpublished content + &.has-warnings { + border-left-color: $color-warning; + } + + // CASE: has errors + &.has-errors { + border-left-color: $color-error; + } + } +} + +// UI: subsection +%outline-subsection { + @include transition(border-left-color $tmg-f2 linear 0s); + border-left: ($baseline/4) solid $color-draft; + + // CASE: is ready to be live + &.is-ready { + border-left-color: $color-ready; + } + + // CASE: is live + &.is-live { + border-left-color: $color-live; + } + + // CASE: has staff-only content + &.is-staff-only { + border-left-color: $color-staff-only; + } + + // CASE: has unpublished content + &.has-warnings { + border-left-color: $color-warning; + } + + // CASE: has errors + &.has-errors { + border-left-color: $color-error; + } +} + +%outline-item { + + // UI: item title + .item-title { + @include transition(color $tmg-f2 ease-in-out 0s); + } + + // CASE: last-child in UI + &:last-child { + margin-bottom: 0; + } + + // CASE: has staff-only content + &.is-staff-only { + + // needed to make sure direct children only + > .section-status, + > .subsection-status, + > .unit-status { + + .icon { + color: $color-staff-only; + } + } + } + + // CASE: has unpublished content + &.has-warnings { + + // needed to make sure direct children only + > .section-status .status-message, + > .subsection-status .status-message, + > .unit-status .status-message { + + .icon { + color: $color-warning; + } + } + } + + // CASE: has errors + &.has-errors { + + // needed to make sure direct children only + > .section-status .status-message, + > .subsection-status .status-message, + > .unit-status .status-message, + > .section-status .status-message-copy, + > .subsection-status .status-message-copy, + > .unit-status .status-message-copy { + color: $color-error; + } + } +} + +%outline-item-status { + @extend %t-copy-sub2; + @extend %t-strong; + color: $color-copy-base; + + .icon { + @extend %t-icon5; + margin-right: ($baseline/4); + } +} + +// outline: sections +.outline-section { + @extend %ui-window; + @extend %outline-item; + @extend %outline-section; + margin-bottom: $baseline; + padding: ($baseline*0.75) $baseline; + + // header - title + .section-title { + @extend %t-title5; + @extend %t-strong; + color: $color-heading-base; + } + + // status + .section-status { + @extend %outline-item-status; + } + + // status - release + .status-release { + @include transition(opacity $tmg-f2 ease-in-out 0s); + opacity: 0.65; + } + + // status - grading + .status-grading { + @include transition(opacity $tmg-f2 ease-in-out 0s); + opacity: 0.65; + } + + .status-grading-value { + display: inline-block; + vertical-align: middle; + } + + .status-grading-date { + display: inline-block; + vertical-align: middle; + margin-left: ($baseline/4); + } + + // status - message + .status-message { + margin-top: ($baseline/2); + border-top: 1px solid $gray-l4; + padding-top: ($baseline/4); + + .icon { + margin-right: ($baseline/4); + } + } + + .status-message-copy { + display: inline-block; + color: $color-heading-base; + } + + // STATE: hover/active + &:hover, &:active { + + // status - release + > .section-status .status-release { + opacity: 1.0; + } + } +} + +// outline: subsections +.outline-subsection { + @extend %outline-item; + @extend %outline-subsection; + margin-bottom: ($baseline/2); + border: 1px solid $gray-l4; + border-left: ($baseline/4) solid $color-draft; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + padding: ($baseline*0.75); + + // STATE: hover/active + &:hover, &:active { + box-shadow: 0 1px 1px $shadow-l2; + } + + // STATE: is-collapsed + &.is-collapsed { + + } + + // header - title + .subsection-title { + @extend %t-title6; + color: $color-heading-base; + } + + // status + .subsection-status { + @extend %outline-item-status; + } + + // STATE: hover/active + &:hover, &:active { + + // status - release + > .subsection-status .status-release { + opacity: 1.0; + } + + // status - grading + > .subsection-status .status-grading { + opacity: 1.0; + } + } +} + +// outline: units +.outline-unit { + @extend %outline-item; + margin-bottom: ($baseline/2); + border: 1px solid $gray-l4; + padding: ($baseline/4) ($baseline/2); + + // STATE: hover/active + &:hover, &:active { + box-shadow: 0 1px 1px $shadow-l2; + } + + // header - title + .unit-title { + @extend %t-title7; + color: $color-heading-base; + } + + .unit-status { + @extend %outline-item-status; + } +} diff --git a/cms/static/sass/elements/_typography.scss b/cms/static/sass/elements/_typography.scss index 2d0ff054125d..e14d52f6d2e6 100644 --- a/cms/static/sass/elements/_typography.scss +++ b/cms/static/sass/elements/_typography.scss @@ -3,6 +3,23 @@ // Scale - (6, 7, 8, 9, 10, 11, 12, 14, 16, 18, 21, 24, 36, 48, 60, 72) +// weights +%t-ultrastrong { + font-weight: 800; +} +%t-strong { + font-weight: 600; +} +%t-regular { + font-weight: 400; +} +%t-light { + font-weight: 300; +} +%t-ultralight { + font-weight: 200; +} + // headings/titles %t-title { font-family: $f-sans-serif; diff --git a/cms/static/sass/views/_outline.scss b/cms/static/sass/views/_outline.scss index 4668be32741a..e3f5ba202dc7 100644 --- a/cms/static/sass/views/_outline.scss +++ b/cms/static/sass/views/_outline.scss @@ -1,9 +1,25 @@ // studio - views - course outline // ==================== +// view-specific utilities +// -------------------- +%outline-item-header { + @include clearfix(); + line-height: 0; +} + +%outline-item-content-hidden { + display: none; +} + +%outline-item-content-shown { + display: block; +} + .view-outline { // page structure + // -------------------- .content-primary, .content-supplementary { @include box-sizing(border-box); @@ -37,26 +53,39 @@ } - // page header bits - .toggle-button-sections { - @extend %t-copy-sub2; - position: relative; - display: none; - float: right; - margin-top: ($baseline/4); - color: $gray-l1; + // page header + // -------------------- + .button-toggle-expand-collapse { + + // STATE: action will collapse all + &.collapse-all { - &.is-shown { - display: block; + .expand-all { + display: none; + } + + .collapse-all { + display: block; + } } - .label { - display: inline-block; + // STATE: action will expand all + &.expand-all { + + .collapse-all { + display: none; + } + + .expand-all { + display: block; + } } } + // adding outline elements + // -------------------- - // new section, subsection, unit + // forms .new-section-name, .new-subsection-name-input { @include font-size(16); @@ -84,6 +113,7 @@ color: $gray-l1; } + // buttons .new-subsection-item, .new-unit-item { @extend %ui-btn-flat-outline; @@ -106,491 +136,260 @@ // UI: general action list styles (section and subsection) - + // -------------------- .expand-collapse { @extend %expand-collapse; - margin: 0 ($baseline/4); } - // UI: element actions list - // TODO: outline page can be updated to reflect styling from %actions-list in _controls.scss - .actions-list { - display: inline-block; - margin-bottom: 0; - } - - .actions-item { - @include font-size(13); - display: inline-block; - padding: 0 ($baseline/5); - vertical-align: middle; + // outline + // -------------------- + .outline { - .action { - min-width: ($baseline*.75); - color: $gray-l2; + // add/new items + .add-item { + margin-top: ($baseline*0.75); - &:hover, - &.is-set { - color: $blue; - visibility: visible; - } + .button-new { + @extend %ui-btn-flat-outline; + padding: ($baseline/2) $baseline; + display: block; - //reset old drag handle style - &.drag-handle { - float: none; - margin: 0; - background: transparent url(../img/drag-handles.png) right 5px no-repeat; - text-align: center; + .icon { + display: inline-block; + vertical-align: middle; + margin-right: ($baseline/2); + } } - } - } + .add-section { + margin-bottom: $baseline; + } - // section styles - .courseware-section { - @extend %ui-window; - @include transition(background $tmg-avg ease-in-out 0); - position: relative; - padding: ($baseline*1.5) $baseline $baseline $baseline; + .add-subsection { - &.collapsed { - padding-bottom: 0; } - &.collapsed .subsection-list, - .collapsed .subsection-list, - .collapsed > ol { - display: none !important; + .add-unit { + margin-left: $outline-indent-width; } + } - &.new-section { - padding: ($baseline*1.5) $baseline 0 $baseline; - - header { - @include clearfix(); - height: auto; - border-bottom: 0; + // outline: items + .outline-item { - .expand-collapse { - display: none; - } + // CASE: expand/collapse-able + &.is-collapsible { - .item-details { - width: auto; + // only select the current item's toggle expansion controls + &:nth-child(1) .ui-toggle-expansion, &:nth-child(1) .item-title { - .section-name { - float: none; - width: 100%; - } + // STATE: hover/active + &:hover, &:active { + color: $blue; } } } - .section { - @include clearfix(); - min-height: 65px; // needed to align with edit input - margin-bottom: 0; - border: 0; - padding: 0; - - // section name area - .item-details { - @include clearfix(); - width: 400px; - float: none; - display: inline-block; - padding: 0 0 ($baseline/2) 0; - - .section-name { - @include font-size(19); - margin-right: ($baseline/2); - } - - .section-name-span { - @include transition(color $tmg-f2 linear 0s); - cursor: pointer; - - &:hover { - color: $blue; - } - } - - .section-name-edit { - position: relative; - width: ($baseline*20); - background: $white; - - input { - @include font-size(16); - } - - .save-button { - @include blue-button; - padding: 7px $baseline 7px; - margin-right: ($baseline/4); - } - - .cancel-button { - @include white-button; - padding: 7px $baseline 7px; - } - } - } - - - // section specific action styles - .item-actions { - position: relative; - display: inline-block; - float: right; - margin-bottom: ($baseline/2); - top: 0; - } - - .actions-item { - padding: 0 0 0 8px; - - &:last-child { - padding-right: 4px; - } - - &.pubdate { - padding-right: 0; - } - - .action { - - &.pubdate { - visibility: hidden; - } - - &:hover, - &.is-set { - color: $blue; - visibility: visible; - } - } - - .section-published-date { - padding: ($baseline/5) ($baseline/2); - border-radius: 3px; - background: $gray-l5; - text-align: right; - - .published-status { - @include font-size(12); - margin-right: 15px; + // item: title + .item-title { - strong { - font-weight: bold; - } - } + // STATE: is-editable + &.is-editable { - &.released .section-published-date { - background-color: transparent; - color: $gray-l1; - - a { - color: $gray-l2; + // editor + + .editor { + display: block; - &:hover { - color: $blue; - } - } + .item-edit-title { + width: 100%; } } } } } + // outline: sections + // -------------------- + .outline-section { - // subsection styles - .courseware-subsection { - @include clearfix(); - padding: 3px 0; - - &.visible { - border-left: 5px solid $green; - } - - &.mixed { - border-left: 5px solid $yellow-s1; - } - - .status { - @extend %cont-text-sr; + // header + .section-header { + @extend %outline-item-header; } - .section-item { - @include transition(background $tmg-avg ease-in-out 0); - @include font-size(13); - position: relative; - display: block; - background-color: $gray-l5; - padding: 6px 8px 8px 16px; - - &:hover { - background: $blue-l5; + .section-header-details { + float: left; + width: flex-grid(6, 9); - .item-actions { - display: block; - } + .icon, .wrapper-section-title { + display: inline-block; + vertical-align: top; } - &.editing { - background: $orange-l4; + .icon { + margin-right: ($baseline/4); } - } - .details { - display: block; - margin-bottom: 0; - width: 600px; - - a { - color: $baseFontColor; + .wrapper-section-title { + width: flex-grid(5, 6); + line-height: 0; } } - } - // gradable drop down - .gradable-status { - display: inline-block; - position: relative; - - .status-label { - @include font-size(12); - width: 110px; - padding: 5px 40px 5px 10px; - border-radius: 3px; - color: transparent; + .section-header-actions { + float: right; + width: flex-grid(3, 9); + margin-top: -($baseline/4); text-align: right; - font-weight: bold; - line-height: 16px; - } - .menu-toggle { - @extend %ui-depth1; - position: absolute; - top: 0; - right: 5px; - padding: 2px 5px; - color: $gray-l2; - - &:hover, - &.is-active { - color: $blue; - } - - &:focus { - outline: 0; + .actions-list { + @extend %actions-list; + @extend %t-action2; } } + // status + .section-status { + margin: 0 0 0 ($outline-indent-width*1.25); + } - // gradable dropdown menu default - .menu { - @include font-size(12); - @include transition(opacity $tmg-f2 linear 0s); - display: none; - opacity: 0.0; - z-index: 1; - position: absolute; - top: -4px; - right: 0; - margin: 0; - box-shadow: 0 1px 2px rgba(0, 0, 0, .2); - border: 1px solid $gray-l2; - border-radius: 4px; - padding: 8px 12px; - background: $white; - - li { - width: 115px; - margin-bottom: 3px; - border-bottom: 1px solid $gray-l4; - padding-bottom: 3px; - - &:last-child { - margin-bottom: 0; - border: none; - padding-bottom: 0; - - .gradable-status-notgraded { - color: $gray; - } - } - } - - a { - color: $blue; - - &.is-selected { - font-weight: bold; - } - } + // content + .section-content { + @extend %outline-item-content-shown; } - // gradable dropdown state - &.is-active { + // CASE: is-collapsible + &.is-collapsible { + @extend %ui-expand-collapse; - .menu { - @extend %ui-depth3; - display: block; - opacity: 1.0; - } - - .menu-toggle { - @extend %ui-depth4; + .ui-toggle-expansion { + @extend %t-icon3; + color: $gray-l3; } } - // set state - &.is-set { + // STATE: is-collapsed + &.is-collapsed { - .menu-toggle { - color: $blue; - } - - .status-label { - display: block; - color: $blue; + .section-content { + @extend %outline-item-content-hidden; } } } - .courseware-subsection .sortable-unit-list { - margin: ($baseline/4) 0 0 0; + // outline: subsections + // -------------------- + .list-subsections { + margin: $baseline 0 0 0; } - // unit styles - .courseware-unit { - margin: -1px 0 0 ($baseline*1.75); + .outline-subsection { - &.add-new-unit { - margin: 5px ($baseline*1.75) 0 ($baseline*1.75); + // header + .subsection-header { + @extend %outline-item-header; } - .section-item { - border: 0; - background-color: $white; - } + .subsection-header-details { + float: left; + width: flex-grid(6, 9); - .public-item { - color: $black; - } + .icon, .wrapper-subsection-title { + display: inline-block; + vertical-align: top; + } - .private-item { - color: $gray-l1; - } + .icon { + margin-right: ($baseline/4); + } - .draft-item { - color: $yellow-d1; + .wrapper-subsection-title { + width: flex-grid(5, 6); + margin-top: -($baseline/10); + line-height: 0; + } } - .draft-item:after, - .public-item:after, - .private-item:after { - @include font-size(9); - margin-left: 3px; - font-weight: 600; - text-transform: uppercase; - } + .subsection-header-actions { + float: right; + width: flex-grid(3, 9); + margin-top: -($baseline/4); + text-align: right; - .draft-item:after { - content: "- draft"; + .actions-list { + @extend %actions-list; + @extend %t-action2; + margin-right: ($baseline/2); + } } - .private-item:after { - content: "- private"; + // status + .subsection-status { + margin: 0 0 0 $outline-indent-width; } - } - - - // modal to edit section publish settings - // basic non-backbone modal-window set-up - .wrapper-modal-window { - @extend %ui-depth4; - @include transition(all $tmg-f2 ease-in-out); - visibility: hidden; - pointer-events: none; - display: none; - position: fixed; - top: 0; - overflow: scroll; - background: $black-t2; - width: 100%; - height: 100%; - text-align: center; - - &:before { - content: ''; - display: inline-block; - height: 100%; - vertical-align: middle; - margin-right: -0.25em; /* Adjusts for spacing */ + // content + .subsection-content { + @extend %outline-item-content-shown; } - .modal-window { - -webkit-transform: translate(-50%, -50%); - transform: translate(-50%, -50%); - position: absolute; - top: 50%; - left: 50%; - opacity: 0; - } - } + // CASE: is-collapsible + &.is-collapsible { + @extend %ui-expand-collapse; - // modal-window showing/hiding - &.modal-window-is-shown { - overflow: hidden; + .ui-toggle-expansion { + @extend %t-icon4; + color: $gray-l3; + } + } - .wrapper-modal-window.is-shown { - visibility: visible; - pointer-events: auto; - display: block; + // STATE: is-collapsed + &.is-collapsed { - .modal-window { - opacity: 1.0; + .subsection-content { + @extend %outline-item-content-hidden; } } } - .edit-section-publish-settings { - - .picker { - @include clearfix(); + // outline: units + // -------------------- + .list-units { + margin: $baseline 0 0 0; + } + .outline-unit { + margin-left: $outline-indent-width; - .field { - float: left; - margin: 0 ($baseline/2) ($baseline/2); + // header + .unit-header { + @extend %outline-item-header; + } - label, - input { - display: block; - text-align: left; - } + .unit-header-details { + float: left; + width: flex-grid(6, 9); + margin-top: ($baseline/4); + } - label { - @extend %t-copy-sub1; - margin-bottom: ($baseline/4); - font-weight: 600; - } + .unit-header-actions { + float: right; + width: flex-grid(3, 9); + margin-top: -($baseline/10); + text-align: right; - input[type="text"] { - @extend %t-copy-sub1; - } + .actions-list { + @extend %actions-list; + @extend %t-action2; } } } - - // UI: DnD - specific elems/cases - section - .courseware-section { + // UI: drag and drop: section + // -------------------- + .outline-section { .draggable-drop-indicator-before { top: -($baseline/2); @@ -613,8 +412,8 @@ } } - // UI: DnD - specific elems/cases - subsection - .courseware-subsection { + // UI: drag and drop: subsection + .outline-subsection { .draggable-drop-indicator-before { top: 0; @@ -638,8 +437,8 @@ } } - // UI: DnD - specific elems/cases - unit - .courseware-unit { + // // UI: drag and drop: unit + .outline-unit { .draggable-drop-indicator-before { top: 0; @@ -658,7 +457,7 @@ } } - // UI: DnD - specific elems/cases - empty parents splint + // UI: drag and drop: splints .ui-splint-indicator { position: relative; } diff --git a/cms/templates/ux/reference/outline.html b/cms/templates/ux/reference/outline.html new file mode 100644 index 000000000000..41857bda0f53 --- /dev/null +++ b/cms/templates/ux/reference/outline.html @@ -0,0 +1,768 @@ +<%inherit file="../../base.html" /> +<%! +from django.core.urlresolvers import reverse +%> +<%block name="title">[template] Course Outline UI +<%block name="bodyclass">is-signedin course view-outline + +<%block name="content"> + +
+ +
+
+

+ Content + > Course Outline +

+ + +
+
+ +
+
+ +
+ +
+
+

Your Course's Outline

+ +
    + + +
  1. + + <%include file="outline_section_header-expanded.html" /> + +
    + <%include file="outline_status_release-scheduled.html" /> +
    + + +
    +
      + +
    1. + + <%include file="outline_subsection_header-expanded.html" /> + +
      + <%include file="outline_status_release-scheduled.html" /> + + <%include file="outline_status_grading.html" /> +
      + + +
      +
        + +
      1. + <%include file="outline_unit_header.html" /> +
      2. +
      + +
      + + <%include file="outline_add-unit.html" /> + + +
    2. + + + +
    3. + + <%include file="outline_subsection_header-expanded.html" /> + +
      + <%include file="outline_status_release-scheduled.html" /> + + <%include file="outline_status_grading.html" /> +
      + + +
      +
        + +
      1. + <%include file="outline_unit_header.html" /> +
      2. + + +
      3. + <%include file="outline_unit_header.html" /> +
      4. + + +
      5. + <%include file="outline_unit_header.html" /> +
      6. + + +
      7. + <%include file="outline_unit_header.html" /> +
      8. +
      + + + <%include file="outline_add-unit.html" /> + +
      + +
    4. + +
    + + + <%include file="outline_add-subsection.html" /> + +
    + +
  2. + +
+ + + <%include file="outline_add-section.html" /> + + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
  1. + + <%include file="outline_section_header-expanded.html" /> + +
    + <%include file="outline_status_release-draft.html" /> +
    + + +
    +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
  2. + + + + + + + +
  3. + + <%include file="outline_section_header-expanded.html" /> + +
    + <%include file="outline_status_release-draft.html" /> +
    + + +
    +
      + +
    1. + + <%include file="outline_subsection_header-expanded.html" /> + +
      + <%include file="outline_status_release-scheduled.html" /> +
      + + +
      +
        + + +
      1. + <%include file="outline_unit_header.html" /> + +
        + +
        + +
      2. + + + + +
      3. + <%include file="outline_unit_header.html" /> + +
        + <%include file="outline_status_release-draft.html" /> +
        + +
      4. + + + +
      5. + <%include file="outline_unit_header.html" /> +
      6. + + + +
      7. + <%include file="outline_unit_header.html" /> + +
        + <%include file="outline_status_message-lock.html" /> +
        + +
      8. + + + +
      9. + <%include file="outline_unit_header.html" /> + +
        + <%include file="outline_status_message-unpublished_changes.html" /> +
        + +
      10. + + + +
      11. + <%include file="outline_unit_header.html" /> + +
        + <%include file="outline_status_message-unpublished_units.html" /> +
        + +
      12. + + + +
      13. + <%include file="outline_unit_header.html" /> + +
        + <%include file="outline_status_message-error.html" /> +
        + +
      14. + +
      + + + <%include file="outline_add-unit.html" /> + +
      + +
    2. + +
    + + <%include file="outline_add-subsection.html" /> + +
    +
  4. + +
+ + <%include file="outline_add-section.html" /> +
+
+ + + +
+ + + +
+
+ +
+ diff --git a/cms/templates/ux/reference/outline_add-section.html b/cms/templates/ux/reference/outline_add-section.html new file mode 100644 index 000000000000..d45181db58f2 --- /dev/null +++ b/cms/templates/ux/reference/outline_add-section.html @@ -0,0 +1,6 @@ +
+ + New Section + +
+ diff --git a/cms/templates/ux/reference/outline_add-subsection.html b/cms/templates/ux/reference/outline_add-subsection.html new file mode 100644 index 000000000000..551cf5ff3108 --- /dev/null +++ b/cms/templates/ux/reference/outline_add-subsection.html @@ -0,0 +1,6 @@ +
+ + New Subsection + +
+ diff --git a/cms/templates/ux/reference/outline_add-unit.html b/cms/templates/ux/reference/outline_add-unit.html new file mode 100644 index 000000000000..0541071e0d05 --- /dev/null +++ b/cms/templates/ux/reference/outline_add-unit.html @@ -0,0 +1,6 @@ +
+ + New Unit + +
+ diff --git a/cms/templates/ux/reference/outline_section_header-collapsed.html b/cms/templates/ux/reference/outline_section_header-collapsed.html new file mode 100644 index 000000000000..83956f5b029d --- /dev/null +++ b/cms/templates/ux/reference/outline_section_header-collapsed.html @@ -0,0 +1,36 @@ +
+

+ + + Section Title + +

+ + +
+ + diff --git a/cms/templates/ux/reference/outline_section_header-expanded.html b/cms/templates/ux/reference/outline_section_header-expanded.html new file mode 100644 index 000000000000..83956f5b029d --- /dev/null +++ b/cms/templates/ux/reference/outline_section_header-expanded.html @@ -0,0 +1,36 @@ +
+

+ + + Section Title + +

+ + +
+ + diff --git a/cms/templates/ux/reference/outline_status_grading.html b/cms/templates/ux/reference/outline_status_grading.html new file mode 100644 index 000000000000..57794030e9bb --- /dev/null +++ b/cms/templates/ux/reference/outline_status_grading.html @@ -0,0 +1,8 @@ +
+

+ Graded as: + + Homework + Due: December 31, 2014 +

+
diff --git a/cms/templates/ux/reference/outline_status_message-error.html b/cms/templates/ux/reference/outline_status_message-error.html new file mode 100644 index 000000000000..365799f429b8 --- /dev/null +++ b/cms/templates/ux/reference/outline_status_message-error.html @@ -0,0 +1,4 @@ +
+ +

Critical error

+
diff --git a/cms/templates/ux/reference/outline_status_message-lock.html b/cms/templates/ux/reference/outline_status_message-lock.html new file mode 100644 index 000000000000..8a06c0d346c1 --- /dev/null +++ b/cms/templates/ux/reference/outline_status_message-lock.html @@ -0,0 +1,4 @@ +
+ +

Contains Staff only content

+
diff --git a/cms/templates/ux/reference/outline_status_message-unpublished_changes.html b/cms/templates/ux/reference/outline_status_message-unpublished_changes.html new file mode 100644 index 000000000000..ac6a752bc0f0 --- /dev/null +++ b/cms/templates/ux/reference/outline_status_message-unpublished_changes.html @@ -0,0 +1,4 @@ +
+ +

Unpublished change(s) to live content

+
diff --git a/cms/templates/ux/reference/outline_status_message-unpublished_units.html b/cms/templates/ux/reference/outline_status_message-unpublished_units.html new file mode 100644 index 000000000000..cab479da3e3b --- /dev/null +++ b/cms/templates/ux/reference/outline_status_message-unpublished_units.html @@ -0,0 +1,4 @@ +
+ +

Unpublished unit(s) will not be released

+
diff --git a/cms/templates/ux/reference/outline_status_release-draft.html b/cms/templates/ux/reference/outline_status_release-draft.html new file mode 100644 index 000000000000..02304355abae --- /dev/null +++ b/cms/templates/ux/reference/outline_status_release-draft.html @@ -0,0 +1,10 @@ +
+

+ Release Status: + + + This item is + + Unscheduled +

+
diff --git a/cms/templates/ux/reference/outline_status_release-lock.html b/cms/templates/ux/reference/outline_status_release-lock.html new file mode 100644 index 000000000000..0f6d08268fd4 --- /dev/null +++ b/cms/templates/ux/reference/outline_status_release-lock.html @@ -0,0 +1,9 @@ +
+

+ Release Status: + + + Will never release - Contains Staff only content + +

+
diff --git a/cms/templates/ux/reference/outline_status_release-released.html b/cms/templates/ux/reference/outline_status_release-released.html new file mode 100644 index 000000000000..d6eb3dc91bf1 --- /dev/null +++ b/cms/templates/ux/reference/outline_status_release-released.html @@ -0,0 +1,10 @@ +
+

+ Release Status: + + + Released on: + + March 25, 2014 +

+
diff --git a/cms/templates/ux/reference/outline_status_release-released_with_parent.html b/cms/templates/ux/reference/outline_status_release-released_with_parent.html new file mode 100644 index 000000000000..c06f01819192 --- /dev/null +++ b/cms/templates/ux/reference/outline_status_release-released_with_parent.html @@ -0,0 +1,9 @@ +
+

+ Release Status: + + + Released with Section + +

+
diff --git a/cms/templates/ux/reference/outline_status_release-scheduled.html b/cms/templates/ux/reference/outline_status_release-scheduled.html new file mode 100644 index 000000000000..e6b62df57be8 --- /dev/null +++ b/cms/templates/ux/reference/outline_status_release-scheduled.html @@ -0,0 +1,9 @@ +
+

+ Release Status: + + + Scheduled: October 31, 2014 + +

+
diff --git a/cms/templates/ux/reference/outline_status_release-scheduled_with_parent.html b/cms/templates/ux/reference/outline_status_release-scheduled_with_parent.html new file mode 100644 index 000000000000..e5ae050b3d8c --- /dev/null +++ b/cms/templates/ux/reference/outline_status_release-scheduled_with_parent.html @@ -0,0 +1,9 @@ +
+

+ Release Status: + + + Scheduled: with Section + +

+
diff --git a/cms/templates/ux/reference/outline_subsection_header-collapsed.html b/cms/templates/ux/reference/outline_subsection_header-collapsed.html new file mode 100644 index 000000000000..c51e69520d2c --- /dev/null +++ b/cms/templates/ux/reference/outline_subsection_header-collapsed.html @@ -0,0 +1,42 @@ +
+

+ + + Subsection Title + + + + + + + + +

+ + +
+ diff --git a/cms/templates/ux/reference/outline_subsection_header-expanded.html b/cms/templates/ux/reference/outline_subsection_header-expanded.html new file mode 100644 index 000000000000..c51e69520d2c --- /dev/null +++ b/cms/templates/ux/reference/outline_subsection_header-expanded.html @@ -0,0 +1,42 @@ +
+

+ + + Subsection Title + + + + + + + + +

+ + +
+ diff --git a/cms/templates/ux/reference/outline_unit_header.html b/cms/templates/ux/reference/outline_unit_header.html new file mode 100644 index 000000000000..7487c56a4695 --- /dev/null +++ b/cms/templates/ux/reference/outline_unit_header.html @@ -0,0 +1,32 @@ +
+

+ Unit Name +

+ + +
+ diff --git a/common/static/sass/_mixins.scss b/common/static/sass/_mixins.scss index 2f24de41a3c2..d0366485f81c 100644 --- a/common/static/sass/_mixins.scss +++ b/common/static/sass/_mixins.scss @@ -90,11 +90,16 @@ // extends - UI - window %ui-window { @include clearfix(); - border-radius: 3px; - box-shadow: 0 1px 1px $shadow-l1; + border-radius: ($baseline/10); + box-shadow: 0 1px 1px $shadow-l2; margin-bottom: $baseline; border: 1px solid $gray-l2; background: $white; + + // STATE: hover/active + &:hover, &:active { + box-shadow: 0 1px 1px $shadow; + } } // extends - UI - visual link @@ -226,7 +231,6 @@ border-radius: ($baseline/4); border: 1px solid $blue-l2; padding: 1px ($baseline/2) 2px ($baseline/2); - background-color: $white; color: $blue-l2; &:hover, &:focus { From 8db70ce26ff67a2a2948dd8a06caf66cd8a39e69 Mon Sep 17 00:00:00 2001 From: Andy Armstrong Date: Wed, 16 Jul 2014 07:09:44 -0400 Subject: [PATCH 02/11] Integrate visual styling into the course outline --- .../contentstore/features/common.py | 6 +- .../contentstore/features/course-outline.py | 6 +- .../contentstore/features/courses.py | 2 +- .../contentstore/tests/test_contentstore.py | 2 +- cms/djangoapps/contentstore/tests/utils.py | 28 +-- cms/djangoapps/contentstore/utils.py | 6 +- .../contentstore/views/component.py | 4 +- cms/djangoapps/contentstore/views/course.py | 4 +- cms/djangoapps/contentstore/views/item.py | 87 +++++++-- .../views/tests/test_course_index.py | 7 +- .../contentstore/views/tests/test_item.py | 182 +++++++++++++++--- cms/static/js/models/xblock_info.js | 28 ++- .../views/pages/container_subviews_spec.js | 154 ++++++++------- .../spec/views/pages/course_outline_spec.js | 133 +++++++------ cms/static/js/spec/views/unit_outline_spec.js | 25 ++- cms/static/js/views/baseview.js | 6 +- cms/static/js/views/pages/container.js | 4 + .../js/views/pages/container_subviews.js | 28 ++- cms/static/js/views/pages/course_outline.js | 30 +-- cms/static/js/views/unit_outline.js | 5 +- cms/static/js/views/utils/view_utils.js | 8 +- cms/static/js/views/xblock_outline.js | 14 +- cms/static/sass/elements/_modules.scss | 2 +- cms/static/sass/views/_container.scss | 5 +- cms/static/sass/views/_outline.scss | 31 +++ cms/templates/container.html | 1 - cms/templates/course_outline.html | 6 +- cms/templates/js/course-outline.underscore | 168 +++++++++++----- .../mock/mock-course-outline-page.underscore | 8 +- cms/templates/js/publish-history.underscore | 30 +-- cms/templates/js/publish-xblock.underscore | 64 +++--- cms/templates/js/unit-outline.underscore | 61 ++++-- cms/templates/js/xblock-outline.underscore | 6 +- .../xmodule/xmodule/modulestore/__init__.py | 16 +- .../lib/xmodule/xmodule/modulestore/mixed.py | 6 +- .../xmodule/modulestore/mongo/draft.py | 16 +- .../modulestore/split_mongo/split_draft.py | 19 +- .../tests/test_mixed_modulestore.py | 14 +- .../test/acceptance/pages/studio/overview.py | 20 +- 39 files changed, 794 insertions(+), 448 deletions(-) diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 76c83b9e01b2..9aaa4f651605 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -203,8 +203,8 @@ def create_a_course(): def add_section(): - world.css_click('.course-outline .add-button') - assert_true(world.is_css_present('.outline-item-section .xblock-field-value')) + world.css_click('.outline .button-new') + assert_true(world.is_css_present('.outline-section .xblock-field-value')) def set_date_and_time(date_css, desired_date, time_css, desired_time, key=None): @@ -241,7 +241,7 @@ def create_unit_from_course_outline(): The end result is the page where the user is editing the new unit. """ css_selectors = [ - '.outline-item-subsection .expand-collapse', '.outline-item-subsection .add-button' + '.outline-subsection .expand-collapse', '.outline-subsection .button-new' ] for selector in css_selectors: world.css_click(selector) diff --git a/cms/djangoapps/contentstore/features/course-outline.py b/cms/djangoapps/contentstore/features/course-outline.py index f3203880aacb..865a5ad5c2cf 100644 --- a/cms/djangoapps/contentstore/features/course-outline.py +++ b/cms/djangoapps/contentstore/features/course-outline.py @@ -69,7 +69,7 @@ def i_add_a_section(step): @step(u'I press the "section" delete icon') def i_press_the_section_delete_icon(step): - delete_locator = 'section .outline-item-section > .wrapper-xblock-header a.delete-button' + delete_locator = 'section .outline-section > .wrapper-xblock-header a.delete-button' world.css_click(delete_locator) @@ -110,9 +110,9 @@ def i_click_the_collapse_expand_all_span(step, text): @step(u'I ([^"]*) the first section$') def i_collapse_expand_a_section(step, text): if text == "collapse": - locator = 'section .outline-item-section .ui-toggle-expansion' + locator = 'section .outline-section .ui-toggle-expansion' elif text == "expand": - locator = 'section .outline-item-section .ui-toggle-expansion' + locator = 'section .outline-section .ui-toggle-expansion' world.css_click(locator) diff --git a/cms/djangoapps/contentstore/features/courses.py b/cms/djangoapps/contentstore/features/courses.py index 6425d1c4c5cc..8f8bc2a1e8f6 100644 --- a/cms/djangoapps/contentstore/features/courses.py +++ b/cms/djangoapps/contentstore/features/courses.py @@ -66,5 +66,5 @@ def i_am_on_tab(step, tab_name): @step('I see a link for adding a new section$') def i_see_new_section_link(step): - link_css = '.course-outline .add-button' + link_css = '.outline .button-new' assert world.css_has_text(link_css, 'New Section') diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 3b254e3dacb5..a6736475b23f 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -1208,7 +1208,7 @@ def test_course_overview_view_with_course(self): resp = self._show_course_overview(course.id) self.assertContains( resp, - '
'.format( + '
'.format( locator='i4x://MITx/999/course/Robot_Super_Course', course_key='MITx/999/Robot_Super_Course', ), diff --git a/cms/djangoapps/contentstore/tests/utils.py b/cms/djangoapps/contentstore/tests/utils.py index f8875d5eaffc..a518bf445ce0 100644 --- a/cms/djangoapps/contentstore/tests/utils.py +++ b/cms/djangoapps/contentstore/tests/utils.py @@ -9,7 +9,7 @@ from django.contrib.auth.models import User from xmodule.contentstore.django import contentstore -from xmodule.modulestore import PublishState, ModuleStoreEnum, mongo +from xmodule.modulestore import LegacyPublishState, ModuleStoreEnum, mongo from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory @@ -149,16 +149,16 @@ def import_and_populate_course(self): # create a Draft vertical vertical = self.store.get_item(course_id.make_usage_key('vertical', self.TEST_VERTICAL), depth=1) draft_vertical = self.store.convert_to_draft(vertical.location, self.user.id) - self.assertEqual(self.store.compute_publish_state(draft_vertical), PublishState.draft) + self.assertEqual(self.store.compute_publish_state(draft_vertical), LegacyPublishState.draft) # create a Private (draft only) vertical private_vertical = self.store.create_item(self.user.id, course_id, 'vertical', self.PRIVATE_VERTICAL) - self.assertEqual(self.store.compute_publish_state(private_vertical), PublishState.private) + self.assertEqual(self.store.compute_publish_state(private_vertical), LegacyPublishState.private) # create a Published (no draft) vertical public_vertical = self.store.create_item(self.user.id, course_id, 'vertical', self.PUBLISHED_VERTICAL) public_vertical = self.store.publish(public_vertical.location, self.user.id) - self.assertEqual(self.store.compute_publish_state(public_vertical), PublishState.public) + self.assertEqual(self.store.compute_publish_state(public_vertical), LegacyPublishState.public) # add the new private and new public as children of the sequential sequential = self.store.get_item(course_id.make_usage_key('sequential', self.SEQUENTIAL)) @@ -195,7 +195,7 @@ def check_populated_course(self, course_id): def verify_item_publish_state(item, publish_state): """Verifies the publish state of the item is as expected.""" - if publish_state in (PublishState.private, PublishState.draft): + if publish_state in (LegacyPublishState.private, LegacyPublishState.draft): self.assertTrue(getattr(item, 'is_draft', False)) else: self.assertFalse(getattr(item, 'is_draft', False)) @@ -208,18 +208,18 @@ def get_and_verify_publish_state(item_type, item_name, publish_state): return item # verify that the draft vertical is draft - vertical = get_and_verify_publish_state('vertical', self.TEST_VERTICAL, PublishState.draft) + vertical = get_and_verify_publish_state('vertical', self.TEST_VERTICAL, LegacyPublishState.draft) for child in vertical.get_children(): - verify_item_publish_state(child, PublishState.draft) + verify_item_publish_state(child, LegacyPublishState.draft) # make sure that we don't have a sequential that is not in draft mode - sequential = get_and_verify_publish_state('sequential', self.SEQUENTIAL, PublishState.public) + sequential = get_and_verify_publish_state('sequential', self.SEQUENTIAL, LegacyPublishState.public) # verify that we have the private vertical - private_vertical = get_and_verify_publish_state('vertical', self.PRIVATE_VERTICAL, PublishState.private) + private_vertical = get_and_verify_publish_state('vertical', self.PRIVATE_VERTICAL, LegacyPublishState.private) # verify that we have the public vertical - public_vertical = get_and_verify_publish_state('vertical', self.PUBLISHED_VERTICAL, PublishState.public) + public_vertical = get_and_verify_publish_state('vertical', self.PUBLISHED_VERTICAL, LegacyPublishState.public) # verify verticals are children of sequential for vert in [vertical, private_vertical, public_vertical]: @@ -338,7 +338,7 @@ def compute_real_state(self, item): it'll return public in that case """ supposed_state = self.store.compute_publish_state(item) - if supposed_state == PublishState.draft and isinstance(item.runtime.modulestore, DraftModuleStore): + if supposed_state == LegacyPublishState.draft and isinstance(item.runtime.modulestore, DraftModuleStore): # see if the draft differs from the published published = self.store.get_item(item.location, revision=ModuleStoreEnum.RevisionOption.published_only) if item.get_explicitly_set_fields_by_scope() != published.get_explicitly_set_fields_by_scope(): @@ -351,13 +351,13 @@ def compute_real_state(self, item): # checking children: if published differs from item, return draft return supposed_state # published == item in all respects, so return public - return PublishState.public - elif supposed_state == PublishState.public and item.location.category in mongo.base.DIRECT_ONLY_CATEGORIES: + return LegacyPublishState.public + elif supposed_state == LegacyPublishState.public and item.location.category in mongo.base.DIRECT_ONLY_CATEGORIES: if not all([ self.store.has_item(child_loc, revision=ModuleStoreEnum.RevisionOption.draft_only) for child_loc in item.children ]): - return PublishState.draft + return LegacyPublishState.draft else: return supposed_state else: diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 72ffd9f911c4..bbf7fca3cac9 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -119,10 +119,10 @@ def compute_publish_state(xblock): Returns whether this xblock is draft, public, or private. Returns: - PublishState.draft - content is in the process of being edited, but still has a previous + LegacyPublishState.draft - content is in the process of being edited, but still has a previous version deployed to LMS - PublishState.public - content is locked and deployed to LMS - PublishState.private - content is editable and not deployed to LMS + LegacyPublishState.public - content is locked and deployed to LMS + LegacyPublishState.private - content is editable and not deployed to LMS """ return modulestore().compute_publish_state(xblock) diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index 8f97fb9f2e3e..0ab827e9d109 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -12,7 +12,7 @@ from edxmako.shortcuts import render_to_response from xmodule.modulestore.django import modulestore -from xmodule.modulestore import PublishState +from xmodule.modulestore import LegacyPublishState from xblock.core import XBlock from xblock.django.request import webob_to_django_response, django_to_webob_request @@ -123,7 +123,7 @@ def subsection_handler(request, usage_key_string): subsection_units = item.get_children() for unit in subsection_units: state = compute_publish_state(unit) - if state in (PublishState.public, PublishState.draft): + if state in (LegacyPublishState.public, LegacyPublishState.draft): can_view_live = True break diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index 462e30076178..a7d20a858699 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -282,7 +282,7 @@ def find_xblock_info(xblock_info, locator): """ if xblock_info['id'] == locator: return xblock_info - children = xblock_info['child_info']['children'] if xblock_info['child_info'] else None + children = xblock_info['child_info']['children'] if xblock_info.get('child_info', None) else None if children: for child_xblock_info in children: result = find_xblock_info(child_xblock_info, locator) @@ -295,7 +295,7 @@ def collect_all_locators(locators, xblock_info): Collect all the locators for an xblock and its children. """ locators.append(xblock_info['id']) - children = xblock_info['child_info']['children'] if xblock_info['child_info'] else None + children = xblock_info['child_info']['children'] if xblock_info.get('child_info', None) else None if children: for child_xblock_info in children: collect_all_locators(locators, child_xblock_info) diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py index 5751d2e210eb..6fd953b97e58 100644 --- a/cms/djangoapps/contentstore/views/item.py +++ b/cms/djangoapps/contentstore/views/item.py @@ -600,10 +600,6 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F In addition, an optional include_children_predicate argument can be provided to define whether or not a particular xblock should have its children included. """ - published = modulestore().has_item(xblock.location, revision=ModuleStoreEnum.RevisionOption.published_only) - - # Treat DEFAULT_START_DATE as a magic number that means the release date has not been set - release_date = get_default_time_display(xblock.start) if xblock.start != DEFAULT_START_DATE else None def safe_get_username(user_id): """ @@ -622,11 +618,23 @@ def safe_get_username(user_id): return None + # Compute the child info first so it can be included in aggregate information for the parent + if include_child_info and xblock.has_children: + child_info = _create_xblock_child_info( + xblock, include_children_predicate=include_children_predicate + ) + else: + child_info = None + + # Treat DEFAULT_START_DATE as a magic number that means the release date has not been set + release_date = get_default_time_display(xblock.start) if xblock.start != DEFAULT_START_DATE else None + published = modulestore().has_item(xblock.location, revision=ModuleStoreEnum.RevisionOption.published_only) + currently_visible_to_students = is_currently_visible_to_students(xblock) + xblock_info = { "id": unicode(xblock.location), "display_name": xblock.display_name_with_default, "category": xblock.category, - "has_changes": modulestore().has_changes(xblock.location), "published": published, "edited_on": get_default_time_display(xblock.subtree_edited_on) if xblock.subtree_edited_on else None, "edited_by": safe_get_username(xblock.subtree_edited_by), @@ -636,8 +644,8 @@ def safe_get_username(user_id): "released_to_students": datetime.now(UTC) > xblock.start, "release_date": release_date, "release_date_from": _get_release_date_from(xblock) if release_date else None, - "visible_to_staff_only": xblock.visible_to_staff_only, - "currently_visible_to_students": is_currently_visible_to_students(xblock), + "currently_visible_to_students": currently_visible_to_students, + "publish_state": _compute_publish_state(xblock, child_info) if not xblock.category == 'course' else None } if data is not None: xblock_info["data"] = data @@ -645,13 +653,70 @@ def safe_get_username(user_id): xblock_info["metadata"] = metadata if include_ancestor_info: xblock_info['ancestor_info'] = _create_xblock_ancestor_info(xblock) - if include_child_info and xblock.has_children: - xblock_info['child_info'] = _create_xblock_child_info( - xblock, include_children_predicate=include_children_predicate - ) + if child_info: + xblock_info['child_info'] = child_info return xblock_info +class PublishState(object): + """ + Represents the possible publish states for an xblock: + live - the block and all of its children are live to students (except for staff only items) + ready - the block and all of its children are ready to go live in the future + unscheduled - the block and all of its children are unscheduled + has_unpublished_content - the block or its children have unpublished content that is not staff only + staff_only - all of the block's content is to be shown to staff only + """ + live = 'live' + ready = 'ready' + unscheduled = 'unscheduled' + has_unpublished_content = 'has_unpublished_content' + staff_only = 'staff_only' + + +def _compute_publish_state(xblock, child_info): + """ + Returns the current publish state for the specified xblock and its children + """ + if xblock.visible_to_staff_only: + return PublishState.staff_only + elif is_unit(xblock) and modulestore().has_changes(xblock.location): + return PublishState.has_unpublished_content + is_unscheduled = xblock.start == DEFAULT_START_DATE + children = child_info and child_info['children'] + if children and len(children) > 0: + all_staff_only = True + all_unscheduled = True + all_live = True + for child in child_info['children']: + child_state = child['publish_state'] + if child_state == PublishState.has_unpublished_content: + return child_state + elif not child_state == PublishState.staff_only: + all_staff_only = False + if not child_state == PublishState.unscheduled: + all_unscheduled = False + if not child_state == PublishState.live: + all_live = False + if all_staff_only: + return PublishState.staff_only + elif all_unscheduled: + if not is_unscheduled: + return PublishState.has_unpublished_content + else: + return PublishState.unscheduled + elif all_live: + return PublishState.live + else: + return PublishState.ready + if is_unscheduled: + return PublishState.unscheduled + elif datetime.now(UTC) > xblock.start: + return PublishState.live + else: + return PublishState.ready + + def _create_xblock_ancestor_info(xblock): """ Returns information about the ancestors of an xblock. Note that the direct parent will also return diff --git a/cms/djangoapps/contentstore/views/tests/test_course_index.py b/cms/djangoapps/contentstore/views/tests/test_course_index.py index fde16b5c0b8b..e698a9484f50 100644 --- a/cms/djangoapps/contentstore/views/tests/test_course_index.py +++ b/cms/djangoapps/contentstore/views/tests/test_course_index.py @@ -9,7 +9,7 @@ from contentstore.tests.utils import CourseTestCase from contentstore.utils import reverse_course_url from contentstore.views.course import course_outline_initial_state -from contentstore.views.item import create_xblock_info +from contentstore.views.item import create_xblock_info, PublishState from xmodule.modulestore.django import modulestore from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory @@ -122,7 +122,7 @@ def test_json_responses(self): self.assertEqual(json_response['category'], 'course') self.assertEqual(json_response['id'], 'i4x://MITx/999/course/Robot_Super_Course') self.assertEqual(json_response['display_name'], 'Robot Super Course') - self.assertTrue(json_response['published']) + self.assertIsNone(json_response['publish_state']) # Now verify the first child children = json_response['child_info']['children'] @@ -131,7 +131,7 @@ def test_json_responses(self): self.assertEqual(first_child_response['category'], 'chapter') self.assertEqual(first_child_response['id'], 'i4x://MITx/999/chapter/Week_1') self.assertEqual(first_child_response['display_name'], 'Week 1') - self.assertTrue(first_child_response['published']) + self.assertEqual(first_child_response['publish_state'], PublishState.unscheduled) self.assertTrue(len(first_child_response['child_info']['children']) > 0) # Finally, validate the entire response for consistency @@ -144,7 +144,6 @@ def assert_correct_json_response(self, json_response): self.assertIsNotNone(json_response['display_name']) self.assertIsNotNone(json_response['id']) self.assertIsNotNone(json_response['category']) - self.assertIsNotNone(json_response['published']) if json_response.get('child_info', None): for child_response in json_response['child_info']['children']: self.assert_correct_json_response(child_response) diff --git a/cms/djangoapps/contentstore/views/tests/test_item.py b/cms/djangoapps/contentstore/views/tests/test_item.py index 85d251f468a4..0dc619530e47 100644 --- a/cms/djangoapps/contentstore/views/tests/test_item.py +++ b/cms/djangoapps/contentstore/views/tests/test_item.py @@ -1,7 +1,7 @@ """Tests for items views.""" import json -from datetime import datetime +from datetime import datetime, timedelta import ddt from mock import patch @@ -21,12 +21,11 @@ SPLIT_TEST_COMPONENT_TYPE ) -from contentstore.views.item import create_xblock_info, ALWAYS +from contentstore.views.item import create_xblock_info, ALWAYS, PublishState from contentstore.tests.utils import CourseTestCase from student.tests.factories import UserFactory from xmodule.capa_module import CapaDescriptor -from xmodule.modulestore import PublishState -from xmodule.modulestore import ModuleStoreEnum +from xmodule.modulestore import LegacyPublishState, ModuleStoreEnum from xmodule.modulestore.django import modulestore from xmodule.modulestore.tests.factories import ItemFactory from xmodule.x_module import STUDIO_VIEW, STUDENT_VIEW @@ -424,7 +423,8 @@ def verify_publish_state(self, usage_key, expected_publish_state): """ item = self.get_item_from_modulestore( usage_key, - (expected_publish_state == PublishState.private) or (expected_publish_state == PublishState.draft) + (expected_publish_state == LegacyPublishState.private) or + (expected_publish_state == LegacyPublishState.draft) ) self.assertEqual(expected_publish_state, self.store.compute_publish_state(item)) return item @@ -537,12 +537,12 @@ def test_reorder_children(self): def test_make_public(self): """ Test making a private problem public (publishing it). """ # When the problem is first created, it is only in draft (because of its category). - self.verify_publish_state(self.problem_usage_key, PublishState.private) + self.verify_publish_state(self.problem_usage_key, LegacyPublishState.private) self.client.ajax_post( self.problem_update_url, data={'publish': 'make_public'} ) - self.verify_publish_state(self.problem_usage_key, PublishState.public) + self.verify_publish_state(self.problem_usage_key, LegacyPublishState.public) def test_make_draft(self): """ Test creating a draft version of a public problem. """ @@ -555,7 +555,7 @@ def test_revert_to_published(self): self.problem_update_url, data={'publish': 'discard_changes'} ) - published = self.verify_publish_state(self.problem_usage_key, PublishState.public) + published = self.verify_publish_state(self.problem_usage_key, LegacyPublishState.public) self.assertIsNone(published.due) def test_republish(self): @@ -567,7 +567,7 @@ def test_republish(self): } # When the problem is first created, it is only in draft (because of its category). - self.verify_publish_state(self.problem_usage_key, PublishState.private) + self.verify_publish_state(self.problem_usage_key, LegacyPublishState.private) # Republishing when only in draft will update the draft but not cause a public item to be created. self.client.ajax_post( @@ -579,7 +579,7 @@ def test_republish(self): } } ) - self.verify_publish_state(self.problem_usage_key, PublishState.private) + self.verify_publish_state(self.problem_usage_key, LegacyPublishState.private) draft = self.get_item_from_modulestore(self.problem_usage_key, verify_is_draft=True) self.assertEqual(draft.display_name, new_display_name) @@ -600,7 +600,7 @@ def test_republish(self): } } ) - self.verify_publish_state(self.problem_usage_key, PublishState.public) + self.verify_publish_state(self.problem_usage_key, LegacyPublishState.public) published = modulestore().get_item( self.problem_usage_key, revision=ModuleStoreEnum.RevisionOption.published_only @@ -616,7 +616,7 @@ def _make_draft_content_different_from_published(self): self.problem_update_url, data={'publish': 'make_public'} ) - published = self.verify_publish_state(self.problem_usage_key, PublishState.public) + published = self.verify_publish_state(self.problem_usage_key, LegacyPublishState.public) # Update the draft version and check that published is different. self.client.ajax_post( @@ -650,7 +650,7 @@ def test_published_and_draft_contents_with_update(self): self.problem_update_url, data={'publish': 'make_public'} ) - published = self.verify_publish_state(self.problem_usage_key, PublishState.public) + published = self.verify_publish_state(self.problem_usage_key, LegacyPublishState.public) # Now make a draft self.client.ajax_post( @@ -695,8 +695,8 @@ def test_publish_states_of_nested_xblocks(self): # The unit and its children should be private initially unit_update_url = reverse_usage_url('xblock_handler', unit_usage_key) - self.verify_publish_state(unit_usage_key, PublishState.private) - self.verify_publish_state(html_usage_key, PublishState.private) + self.verify_publish_state(unit_usage_key, LegacyPublishState.private) + self.verify_publish_state(html_usage_key, LegacyPublishState.private) # Make the unit public and verify that the problem is also made public resp = self.client.ajax_post( @@ -704,8 +704,8 @@ def test_publish_states_of_nested_xblocks(self): data={'publish': 'make_public'} ) self.assertEqual(resp.status_code, 200) - self.verify_publish_state(unit_usage_key, PublishState.public) - self.verify_publish_state(html_usage_key, PublishState.public) + self.verify_publish_state(unit_usage_key, LegacyPublishState.public) + self.verify_publish_state(html_usage_key, LegacyPublishState.public) # Make a draft for the unit and verify that the problem also has a draft resp = self.client.ajax_post( @@ -716,8 +716,8 @@ def test_publish_states_of_nested_xblocks(self): } ) self.assertEqual(resp.status_code, 200) - self.verify_publish_state(unit_usage_key, PublishState.draft) - self.verify_publish_state(html_usage_key, PublishState.draft) + self.verify_publish_state(unit_usage_key, LegacyPublishState.draft) + self.verify_publish_state(html_usage_key, LegacyPublishState.draft) @skipUnless(settings.FEATURES.get('ENABLE_GROUP_CONFIGURATIONS'), 'Tests Group Configurations feature') @@ -1153,7 +1153,6 @@ def validate_course_xblock_info(self, xblock_info, has_child_info=True): self.assertEqual(xblock_info['category'], 'course') self.assertEqual(xblock_info['id'], 'i4x://MITx/999/course/Robot_Super_Course') self.assertEqual(xblock_info['display_name'], 'Robot Super Course') - self.assertTrue(xblock_info['published']) # Finally, validate the entire response for consistency self.validate_xblock_info_consistency(xblock_info, has_child_info=has_child_info) @@ -1165,7 +1164,6 @@ def validate_chapter_xblock_info(self, xblock_info, has_child_info=True): self.assertEqual(xblock_info['category'], 'chapter') self.assertEqual(xblock_info['id'], 'i4x://MITx/999/chapter/Week_1') self.assertEqual(xblock_info['display_name'], 'Week 1') - self.assertTrue(xblock_info['published']) self.assertEqual(xblock_info['edited_by'], 'testuser') # Finally, validate the entire response for consistency @@ -1178,7 +1176,6 @@ def validate_sequential_xblock_info(self, xblock_info, has_child_info=True): self.assertEqual(xblock_info['category'], 'sequential') self.assertEqual(xblock_info['id'], 'i4x://MITx/999/sequential/Lesson_1') self.assertEqual(xblock_info['display_name'], 'Lesson 1') - self.assertTrue(xblock_info['published']) self.assertEqual(xblock_info['edited_by'], 'testuser') # Finally, validate the entire response for consistency @@ -1191,7 +1188,6 @@ def validate_vertical_xblock_info(self, xblock_info): self.assertEqual(xblock_info['category'], 'vertical') self.assertEqual(xblock_info['id'], 'i4x://MITx/999/vertical/Unit_1') self.assertEqual(xblock_info['display_name'], 'Unit 1') - self.assertTrue(xblock_info['published']) self.assertEqual(xblock_info['edited_by'], 'testuser') # Validate that the correct ancestor info has been included @@ -1213,7 +1209,6 @@ def validate_component_xblock_info(self, xblock_info): self.assertEqual(xblock_info['category'], 'video') self.assertEqual(xblock_info['id'], 'i4x://MITx/999/video/My_Video') self.assertEqual(xblock_info['display_name'], 'My Video') - self.assertTrue(xblock_info['published']) self.assertEqual(xblock_info['edited_by'], 'testuser') # Finally, validate the entire response for consistency @@ -1226,7 +1221,6 @@ def validate_xblock_info_consistency(self, xblock_info, has_ancestor_info=False, self.assertIsNotNone(xblock_info['display_name']) self.assertIsNotNone(xblock_info['id']) self.assertIsNotNone(xblock_info['category']) - self.assertIsNotNone(xblock_info['published']) self.assertEqual(xblock_info['edited_by'], 'testuser') if has_ancestor_info: self.assertIsNotNone(xblock_info.get('ancestor_info', None)) @@ -1248,3 +1242,141 @@ def validate_xblock_info_consistency(self, xblock_info, has_ancestor_info=False, ) else: self.assertIsNone(xblock_info.get('child_info', None)) + + +class TestXBlockPublishingInfo(ItemTest): + """ + Unit tests for XBlock's outline handling. + """ + def _create_child(self, parent, category, display_name, publish_item=False): + return ItemFactory.create( + parent_location=parent.location, category=category, display_name=display_name, + user_id=self.user.id, publish_item=publish_item + ) + + def _get_child(self, xblock_info, index): + """ + Returns the child at the specified index. + """ + children = xblock_info['child_info']['children'] + self.assertTrue(len(children) > index) + return children[index] + + def _get_xblock_info(self, location): + """ + Returns the xblock info for the specified location. + """ + return create_xblock_info( + modulestore().get_item(location), + include_child_info=True, + include_children_predicate=ALWAYS, + ) + + def _set_release_date(self, location, start): + """ + Sets the release date for the specified xblock. + """ + xblock = modulestore().get_item(location) + xblock.start = start + self.store.update_item(xblock, self.user.id) + + def _set_staff_only(self, location, staff_only): + """ + Sets staff only for the specified xblock. + """ + xblock = modulestore().get_item(location) + xblock.visible_to_staff_only = staff_only + self.store.update_item(xblock, self.user.id) + + def _set_display_name(self, location, display_name): + """ + Sets the display name for the specified xblock. + """ + xblock = modulestore().get_item(location) + xblock.display_name = display_name + self.store.update_item(xblock, self.user.id) + + def test_empty_chapter_publishing_info(self): + empty_chapter = self._create_child(self.course, 'chapter', "Empty Chapter") + xblock_info = self._get_xblock_info(empty_chapter.location) + self.assertEqual(xblock_info['publish_state'], PublishState.unscheduled) + + def test_empty_section_publishing_info(self): + chapter = self._create_child(self.course, 'chapter', "Test Chapter") + self._create_child(chapter, 'sequential', "Empty Sequential") + xblock_info = self._get_xblock_info(chapter.location) + self.assertEqual(xblock_info['publish_state'], PublishState.unscheduled) + self.assertEqual(self._get_child(xblock_info, 0)['publish_state'], PublishState.unscheduled) + + def test_published_unit_publishing_info(self): + chapter = self._create_child(self.course, 'chapter', "Test Chapter") + sequential = self._create_child(chapter, 'sequential', "Test Sequential") + self._create_child(sequential, 'vertical', "Published Unit", publish_item=True) + self._set_release_date(chapter.location, datetime.now(UTC) + timedelta(days=1)) + xblock_info = self._get_xblock_info(chapter.location) + self.assertEqual(xblock_info['publish_state'], PublishState.ready) + sequential_child_info = self._get_child(xblock_info, 0) + self.assertEqual(sequential_child_info['publish_state'], PublishState.ready) + unit_child_info = self._get_child(sequential_child_info, 0) + self.assertEqual(unit_child_info['publish_state'], PublishState.ready) + + def test_released_unit_publishing_info(self): + chapter = self._create_child(self.course, 'chapter', "Test Chapter") + sequential = self._create_child(chapter, 'sequential', "Test Sequential") + self._create_child(sequential, 'vertical', "Published Unit", publish_item=True) + self._set_release_date(chapter.location, datetime.now(UTC) - timedelta(days=1)) + xblock_info = self._get_xblock_info(chapter.location) + self.assertEqual(xblock_info['publish_state'], PublishState.live) + sequential_child_info = self._get_child(xblock_info, 0) + self.assertEqual(sequential_child_info['publish_state'], PublishState.live) + unit_child_info = self._get_child(sequential_child_info, 0) + self.assertEqual(unit_child_info['publish_state'], PublishState.live) + + def test_partially_released_section_publishing_info(self): + chapter = self._create_child(self.course, 'chapter', "Test Chapter") + released_sequential = self._create_child(chapter, 'sequential', "Released Sequential") + self._create_child(released_sequential, 'vertical', "Released Unit", publish_item=True) + self._set_release_date(chapter.location, datetime.now(UTC) - timedelta(days=1)) + published_sequential = self._create_child(chapter, 'sequential', "Published Sequential") + self._create_child(published_sequential, 'vertical', "Published Unit", publish_item=True) + self._set_release_date(published_sequential.location, datetime.now(UTC) + timedelta(days=1)) + xblock_info = self._get_xblock_info(chapter.location) + + # Verify the state of the released sequential + released_sequential_child_info = self._get_child(xblock_info, 0) + released_unit_child_info = self._get_child(released_sequential_child_info, 0) + self.assertEqual(released_unit_child_info['publish_state'], PublishState.live) + self.assertEqual(released_sequential_child_info['publish_state'], PublishState.live) + + # Verify the state of the published sequential + public_sequential_child_info = self._get_child(xblock_info, 1) + public_unit_child_info = self._get_child(public_sequential_child_info, 0) + self.assertEqual(public_sequential_child_info['publish_state'], PublishState.ready) + self.assertEqual(public_unit_child_info['publish_state'], PublishState.ready) + + # Finally verify the state of the chapter + self.assertEqual(xblock_info['publish_state'], PublishState.ready) + + def test_unpublished_changes_publishing_info(self): + chapter = self._create_child(self.course, 'chapter', "Test Chapter") + sequential = self._create_child(chapter, 'sequential', "Test Sequential") + unit = self._create_child(sequential, 'vertical', "Published Unit", publish_item=True) + self._set_display_name(unit.location, 'Updated Unit') + xblock_info = self._get_xblock_info(chapter.location) + self.assertEqual(xblock_info['publish_state'], PublishState.has_unpublished_content) + sequential_child_info = self._get_child(xblock_info, 0) + self.assertEqual(sequential_child_info['publish_state'], PublishState.has_unpublished_content) + unit_child_info = self._get_child(sequential_child_info, 0) + self.assertEqual(unit_child_info['publish_state'], PublishState.has_unpublished_content) + + def test_staff_only_publishing_info(self): + chapter = self._create_child(self.course, 'chapter', "Test Chapter") + sequential = self._create_child(chapter, 'sequential', "Test Sequential") + unit = self._create_child(sequential, 'vertical', "Published Unit") + self._set_staff_only(unit.location, True) + xblock_info = self._get_xblock_info(chapter.location) + self.assertEqual(xblock_info['publish_state'], PublishState.staff_only) + sequential_child_info = self._get_child(xblock_info, 0) + self.assertEqual(sequential_child_info['publish_state'], PublishState.staff_only) + unit_child_info = self._get_child(sequential_child_info, 0) + self.assertEqual(unit_child_info['publish_state'], PublishState.staff_only) diff --git a/cms/static/js/models/xblock_info.js b/cms/static/js/models/xblock_info.js index 8cee477c0e14..02b07a4533b6 100644 --- a/cms/static/js/models/xblock_info.js +++ b/cms/static/js/models/xblock_info.js @@ -24,21 +24,6 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu * An optional object with information about each of the ancestors. */ "ancestor_info": null, - /** - * True iff: - * 1) Edits have been made to the xblock and no published version exists. - * 2) Edits have been made to the xblock since the last published version. - */ - "has_changes": null, - /** - * True iff a published version of the xblock exists. - */ - "published": null, - /** - * If true, only course staff can see the xblock regardless of publish status or - * release date status. - */ - "visible_to_staff_only": null, /** * Date of the last edit to this xblock or any of its descendants. */ @@ -47,6 +32,10 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu * User who last edited the xblock or any of its descendants. */ "edited_by":null, + /** + * True iff a published version of the xblock exists. + */ + "published": null, /** * Date of the last publish of this xblock, or null if never published. */ @@ -55,6 +44,15 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu * User who last published the xblock, or null if never published. */ "published_by": null, + /** + * Represents the possible publish states for an xblock: + * is_live - the block and all of its children are live to students (except for staff only items) + * is_ready - the block and all of its children are ready to go live in the future + * unscheduled - the block and all of its children are unscheduled + * has_unpublished_content - the block or its children have unpublished content that is not staff only + * is_staff_only - all of the block's content is to be shown to staff only + */ + "publish_state": null, /** * True iff the release date of the xblock is in the past. */ diff --git a/cms/static/js/spec/views/pages/container_subviews_spec.js b/cms/static/js/spec/views/pages/container_subviews_spec.js index 6aaaedc6ef1a..e247e3078f93 100644 --- a/cms/static/js/spec/views/pages/container_subviews_spec.js +++ b/cms/static/js/spec/views/pages/container_subviews_spec.js @@ -23,11 +23,9 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin id: 'locator-container', display_name: 'Test Container', category: 'vertical', - published: false, - has_changes: false, + publish_state: 'unscheduled', edited_on: "Jul 02, 2014 at 14:20 UTC", edited_by: "joe", published_on: "Jul 01, 2014 at 12:45 UTC", published_by: "amako", - visible_to_staff_only: false, currently_visible_to_students: false }; @@ -79,31 +77,30 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin var viewPublishedCss = '.button-view', previewCss = '.button-preview'; - it('renders correctly for private unit', function () { + it('renders correctly for unscheduled unit', function () { renderContainerPage(this, mockContainerXBlockHtml); expect(containerPage.$(viewPublishedCss)).toHaveClass(disabledCss); expect(containerPage.$(previewCss)).not.toHaveClass(disabledCss); }); - it('updates when published attribute changes', function () { + it('updates when publish state changes', function () { renderContainerPage(this, mockContainerXBlockHtml); - fetch({"published": true}); + fetch({publish_state: 'ready'}); expect(containerPage.$(viewPublishedCss)).not.toHaveClass(disabledCss); - fetch({"published": false}); + fetch({publish_state: 'unscheduled'}); expect(containerPage.$(viewPublishedCss)).toHaveClass(disabledCss); }); it('updates when has_changes attribute changes', function () { renderContainerPage(this, mockContainerXBlockHtml); - fetch({"has_changes": true}); + fetch({publish_state: 'has-unpublished-changes'}); expect(containerPage.$(previewCss)).not.toHaveClass(disabledCss); - fetch({"published": true, "has_changes": false}); + fetch({publish_state: 'ready'}); expect(containerPage.$(previewCss)).toHaveClass(disabledCss); - // If published is false, preview is always enabled. - fetch({"published": false, "has_changes": false}); + fetch({publish_state: 'unscheduled'}); expect(containerPage.$(previewCss)).not.toHaveClass(disabledCss); }); }); @@ -111,9 +108,10 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin describe("Publisher", function () { var headerCss = '.pub-status', bitPublishingCss = "div.bit-publishing", - publishedBit = "is-published", - draftBit = "is-draft", - staffOnlyBit = "is-staff-only", + liveClass = "is-live", + readyClass = "is-ready", + staffOnlyClass = "is-staff-only", + hasWarningsClass = 'has-warnings', publishButtonCss = ".action-publish", discardChangesButtonCss = ".action-discard", lastDraftCss = ".wrapper-last-draft", @@ -125,9 +123,9 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin // Helper function to do the discard operation, up until the server response. containerPage.render(); respondWithHtml(mockContainerXBlockHtml); - fetch({"published": true, "has_changes": true}); + fetch({publish_state: 'has_unpublished_content'}); expect(containerPage.$(discardChangesButtonCss)).not.toHaveClass('is-disabled'); - expect(containerPage.$(bitPublishingCss)).toHaveClass(draftBit); + expect(containerPage.$(bitPublishingCss)).toHaveClass(hasWarningsClass); // Click discard changes containerPage.$(discardChangesButtonCss).click(); @@ -145,43 +143,43 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin promptSpies.show.andReturn(this.promptSpies); }); - it('renders correctly with private content', function () { + it('renders correctly with unscheduled content', function () { var verifyPrivateState = function() { expect(containerPage.$(headerCss).text()).toContain('Draft (Never published)'); expect(containerPage.$(publishButtonCss)).not.toHaveClass(disabledCss); expect(containerPage.$(discardChangesButtonCss)).toHaveClass(disabledCss); - expect(containerPage.$(bitPublishingCss)).not.toHaveClass(draftBit); - expect(containerPage.$(bitPublishingCss)).not.toHaveClass(publishedBit); + expect(containerPage.$(bitPublishingCss)).not.toHaveClass(readyClass); }; renderContainerPage(this, mockContainerXBlockHtml); - fetch({"published": false, "has_changes": false}); - verifyPrivateState(); - - fetch({"published": false, "has_changes": true}); + fetch({publishState: 'unscheduled'}); verifyPrivateState(); }); - it('renders correctly with public content', function () { + it('renders correctly with published content', function () { renderContainerPage(this, mockContainerXBlockHtml); - fetch({"published": true, "has_changes": false}); + fetch({publish_state: 'ready'}); expect(containerPage.$(headerCss).text()).toContain('Published'); expect(containerPage.$(publishButtonCss)).toHaveClass(disabledCss); expect(containerPage.$(discardChangesButtonCss)).toHaveClass(disabledCss); - expect(containerPage.$(bitPublishingCss)).toHaveClass(publishedBit); + expect(containerPage.$(bitPublishingCss)).toHaveClass(readyClass); - fetch({"published": true, "has_changes": true}); + fetch({publish_state: 'has_unpublished_content'}); expect(containerPage.$(headerCss).text()).toContain('Draft (Unpublished changes)'); expect(containerPage.$(publishButtonCss)).not.toHaveClass(disabledCss); expect(containerPage.$(discardChangesButtonCss)).not.toHaveClass(disabledCss); - expect(containerPage.$(bitPublishingCss)).toHaveClass(draftBit); + expect(containerPage.$(bitPublishingCss)).toHaveClass(hasWarningsClass); + + fetch({publish_state: 'live'}); + expect(containerPage.$(headerCss).text()).toContain('Published'); + expect(containerPage.$(publishButtonCss)).toHaveClass(disabledCss); + expect(containerPage.$(discardChangesButtonCss)).toHaveClass(disabledCss); + expect(containerPage.$(bitPublishingCss)).toHaveClass(liveClass); }); it('can publish private content', function () { var notificationSpy = edit_helpers.createNotificationSpy(); renderContainerPage(this, mockContainerXBlockHtml); - fetch({"published": false, "has_changes": false}); - expect(containerPage.$(bitPublishingCss)).not.toHaveClass(draftBit); - expect(containerPage.$(bitPublishingCss)).not.toHaveClass(publishedBit); + expect(containerPage.$(bitPublishingCss)).not.toHaveClass(readyClass); // Click publish containerPage.$(publishButtonCss).click(); @@ -197,18 +195,17 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin create_sinon.expectJsonRequest(requests, "GET", "/xblock/locator-container"); // Response to fetch - respondWithJson({"id": "locator-container", "published": true, "has_changes": false}); + respondWithJson(createXBlockInfo({publish_state: 'ready'})); // Verify updates displayed - expect(containerPage.$(bitPublishingCss)).toHaveClass(publishedBit); - // Verify that the "published" value has been cleared out of the model. - expect(containerPage.model.get("publish")).toBeNull(); + expect(containerPage.$(bitPublishingCss)).toHaveClass(readyClass); + // Verify that the "publish_state" value has been updated + expect(containerPage.model.get("publish_state")).toBe('ready'); }); it('can does not fetch if publish fails', function () { renderContainerPage(this, mockContainerXBlockHtml); - fetch({"published": false}); - expect(containerPage.$(bitPublishingCss)).not.toHaveClass(publishedBit); + expect(containerPage.$(bitPublishingCss)).not.toHaveClass(readyClass); // Click publish containerPage.$(publishButtonCss).click(); @@ -220,9 +217,9 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin expect(requests.length).toEqual(numRequests); // Verify still in draft state. - expect(containerPage.$(bitPublishingCss)).not.toHaveClass(publishedBit); - // Verify that the "published" value has been cleared out of the model. - expect(containerPage.model.get("publish")).toBeNull(); + expect(containerPage.$(bitPublishingCss)).not.toHaveClass(readyClass); + // Verify that the "publish_state" value has been updated + expect(containerPage.model.get("publish_state")).toBe('unscheduled'); }); it('can discard changes', function () { @@ -263,11 +260,11 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin }); it('does not discard changes on cancel', function () { - renderContainerPage(this, mockContainerXBlockHtml); - fetch({"published": true, "has_changes": true}); + renderContainerPage(this, mockContainerXBlockHtml, { publish_state: 'has_unpublished_content' }); var numRequests = requests.length; // Click discard changes + expect(containerPage.$(discardChangesButtonCss)).not.toHaveClass('is-disabled'); containerPage.$(discardChangesButtonCss).click(); // Click cancel to confirmation. @@ -280,14 +277,17 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin it('renders the last published date and user when there are no changes', function () { renderContainerPage(this, mockContainerXBlockHtml); - fetch({"published_on": "Jul 01, 2014 at 12:45 UTC", "published_by": "amako"}); + fetch({published_on: "Jul 01, 2014 at 12:45 UTC", published_by: "amako"}); expect(containerPage.$(lastDraftCss).text()). toContain("Last published Jul 01, 2014 at 12:45 UTC by amako"); }); it('renders the last saved date and user when there are changes', function () { renderContainerPage(this, mockContainerXBlockHtml); - fetch({"has_changes": true, "edited_on": "Jul 02, 2014 at 14:20 UTC", "edited_by": "joe"}); + fetch({ + publish_state: 'has_unpublished_content', + edited_on: "Jul 02, 2014 at 14:20 UTC", edited_by: "joe" + }); expect(containerPage.$(lastDraftCss).text()). toContain("Draft saved on Jul 02, 2014 at 14:20 UTC by joe"); }); @@ -295,8 +295,10 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin describe("Release Date", function() { it('renders correctly when unreleased', function () { renderContainerPage(this, mockContainerXBlockHtml); - fetch({"published": true, "released_to_students": false, - "release_date": "Jul 02, 2014 at 14:20 UTC", "release_date_from": 'Section "Week 1"'}); + fetch({ + publish_state: 'ready', released_to_students: false, + release_date: "Jul 02, 2014 at 14:20 UTC", release_date_from: 'Section "Week 1"' + }); expect(containerPage.$(releaseDateTitleCss).text()).toContain("Scheduled:"); expect(containerPage.$(releaseDateContentCss).text()). toContain('Jul 02, 2014 at 14:20 UTC with Section "Week 1"'); @@ -304,8 +306,10 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin it('renders correctly when released', function () { renderContainerPage(this, mockContainerXBlockHtml); - fetch({"published": true, "released_to_students": true, - "release_date": "Jul 02, 2014 at 14:20 UTC", "release_date_from": 'Section "Week 1"' }); + fetch({ + publish_state: 'live', released_to_students: true, + release_date: "Jul 02, 2014 at 14:20 UTC", release_date_from: 'Section "Week 1"' + }); expect(containerPage.$(releaseDateTitleCss).text()).toContain("Released:"); expect(containerPage.$(releaseDateContentCss).text()). toContain('Jul 02, 2014 at 14:20 UTC with Section "Week 1"'); @@ -313,17 +317,20 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin it('renders correctly when the release date is not set', function () { renderContainerPage(this, mockContainerXBlockHtml); - fetch({"published": true, "released_to_students": false, - "release_date": null, "release_date_from": null }); + fetch({ + publish_state: 'unscheduled', "released_to_students": false, + release_date: null, release_date_from: null + }); expect(containerPage.$(releaseDateTitleCss).text()).toContain("Release:"); expect(containerPage.$(releaseDateContentCss).text()).toContain("Unscheduled"); }); it('renders correctly when the unit is not published', function () { renderContainerPage(this, mockContainerXBlockHtml); - fetch({"published": false, "released_to_students": true, - "release_date": "Jul 02, 2014 at 14:20 UTC", "release_date_from": 'Section "Week 1"' }); - // Force a render because none of the fetched fields will trigger a render + fetch({ + publish_state: 'has_unpublished_content', released_to_students: true, + release_date: "Jul 02, 2014 at 14:20 UTC", release_date_from: 'Section "Week 1"' + }); containerPage.xblockPublisher.render(); expect(containerPage.$(releaseDateTitleCss).text()).toContain("Release:"); expect(containerPage.$(releaseDateContentCss).text()). @@ -355,8 +362,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin }); create_sinon.expectJsonRequest(requests, 'GET', '/xblock/locator-container'); create_sinon.respondWithJson(requests, createXBlockInfo({ - published: containerPage.model.get('published'), - visible_to_staff_only: isStaffOnly + publish_state: isStaffOnly ? 'staff_only' : 'unscheduled' })); }; @@ -364,11 +370,11 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin if (isStaffOnly) { expect(containerPage.$('.action-staff-lock i')).toHaveClass('icon-check'); expect(containerPage.$('.wrapper-visibility .copy').text()).toBe('Staff Only'); - expect(containerPage.$(bitPublishingCss)).toHaveClass(staffOnlyBit); + expect(containerPage.$(bitPublishingCss)).toHaveClass(staffOnlyClass); } else { expect(containerPage.$('.action-staff-lock i')).toHaveClass('icon-check-empty'); expect(containerPage.$('.wrapper-visibility .copy').text()).toBe('Staff and Students'); - expect(containerPage.$(bitPublishingCss)).not.toHaveClass(staffOnlyBit); + expect(containerPage.$(bitPublishingCss)).not.toHaveClass(staffOnlyClass); } }; @@ -386,27 +392,16 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin it("can remove staff only setting", function() { promptSpy = edit_helpers.createPromptSpy(); - renderContainerPage(this, mockContainerXBlockHtml); - requestStaffOnly(true); + renderContainerPage(this, mockContainerXBlockHtml, { publish_state: 'staff_only' }); requestStaffOnly(false); verifyStaffOnly(false); - expect(containerPage.$(bitPublishingCss)).not.toHaveClass(publishedBit); - }); - - it("can remove staff only setting from published unit", function() { - promptSpy = edit_helpers.createPromptSpy(); - renderContainerPage(this, mockContainerXBlockHtml, { published: true }); - requestStaffOnly(true); - requestStaffOnly(false); - verifyStaffOnly(false); - expect(containerPage.$(bitPublishingCss)).toHaveClass(publishedBit); + expect(containerPage.$(bitPublishingCss)).not.toHaveClass(readyClass); }); it("does not refresh if removing staff only is canceled", function() { var requestCount; promptSpy = edit_helpers.createPromptSpy(); - renderContainerPage(this, mockContainerXBlockHtml); - requestStaffOnly(true); + renderContainerPage(this, mockContainerXBlockHtml, { publish_state: 'staff_only' }); requestCount = requests.length; containerPage.$('.action-staff-lock').click(); edit_helpers.confirmPrompt(promptSpy, true); // Click 'No' to cancel @@ -429,25 +424,26 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin describe("PublishHistory", function () { var lastPublishCss = ".wrapper-last-publish"; + it('renders never published when the block is unpublished', function () { + renderContainerPage(this, mockContainerXBlockHtml, { + published: false, published_on: null, published_by: null + }); + expect(containerPage.$(lastPublishCss).text()).toContain("Never published"); + }); + it('renders the last published date and user when the block is published', function() { renderContainerPage(this, mockContainerXBlockHtml); fetch({ - "published": true, "published_on": "Jul 01, 2014 at 12:45 UTC", "published_by": "amako" + published: true, published_on: "Jul 01, 2014 at 12:45 UTC", published_by: "amako" }); expect(containerPage.$(lastPublishCss).text()). toContain("Last published Jul 01, 2014 at 12:45 UTC by amako"); }); - it('renders never published when the block is unpublished', function () { - renderContainerPage(this, mockContainerXBlockHtml); - fetch({ "published": false }); - expect(containerPage.$(lastPublishCss).text()).toContain("Never published"); - }); - it('renders correctly when the block is published without publish info', function () { renderContainerPage(this, mockContainerXBlockHtml); fetch({ - "published": true, "published_on": null, "published_by": null + published: true, published_on: null, published_by: null }); expect(containerPage.$(lastPublishCss).text()).toContain("Previously published"); }); diff --git a/cms/static/js/spec/views/pages/course_outline_spec.js b/cms/static/js/spec/views/pages/course_outline_spec.js index 727b183dc16d..15ab173e9ff0 100644 --- a/cms/static/js/spec/views/pages/course_outline_spec.js +++ b/cms/static/js/spec/views/pages/course_outline_spec.js @@ -4,7 +4,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" describe("CourseOutlinePage", function() { var createCourseOutlinePage, displayNameInput, model, outlinePage, requests, - getHeaderElement, expandAndVerifyState, collapseAndVerifyState, + getItemsOfType, getItemHeaders, verifyItemsExpanded, expandItemsAndVerifyState, collapseItemsAndVerifyState, createMockCourseJSON, createMockSectionJSON, createMockSubsectionJSON, mockCourseJSON, mockEmptyCourseJSON, mockSingleSectionCourseJSON, mockOutlinePage = readFixtures('mock/mock-course-outline-page.underscore'); @@ -64,21 +64,31 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" }; }; - getHeaderElement = function(selector) { - var element = outlinePage.$(selector); - return element.find('> .wrapper-xblock-header'); + getItemsOfType = function(type) { + return outlinePage.$('.outline-' + type); }; - expandAndVerifyState = function(selector) { - var element = outlinePage.$(selector); - getHeaderElement(selector).find('.ui-toggle-expansion').click(); - expect(element).not.toHaveClass('collapsed'); + getItemHeaders = function(type) { + return getItemsOfType(type).find('> .' + type + '-header'); }; - collapseAndVerifyState = function(selector) { - var element = outlinePage.$(selector); - getHeaderElement(selector).find('.ui-toggle-expansion').click(); - expect(element).toHaveClass('collapsed'); + verifyItemsExpanded = function(type, isExpanded) { + var element = getItemsOfType(type); + if (isExpanded) { + expect(element).not.toHaveClass('is-collapsed'); + } else { + expect(element).toHaveClass('is-collapsed'); + } + }; + + expandItemsAndVerifyState = function(type) { + getItemHeaders(type).find('.ui-toggle-expansion').click(); + verifyItemsExpanded(type, true); + }; + + collapseItemsAndVerifyState = function(type) { + getItemHeaders(type).find('.ui-toggle-expansion').click(); + verifyItemsExpanded(type, false); }; createCourseOutlinePage = function(test, courseJSON, createOnly) { @@ -108,12 +118,10 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" category: 'vertical', studio_url: '/container/mock-unit', is_container: true, - has_changes: true, - published: false, + publish_state: 'unscheduled', edited_on: 'Jul 02, 2014 at 20:56 UTC', edited_by: 'MockUser' - } - ]) + }]) ]) ]); mockEmptyCourseJSON = createMockCourseJSON('mock-course', 'Mock Course', []); @@ -129,9 +137,9 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" describe('Initial display', function() { it('can render itself', function() { createCourseOutlinePage(this, mockCourseJSON); - expect(outlinePage.$('.sortable-course-list')).toExist(); - expect(outlinePage.$('.sortable-section-list')).toExist(); - expect(outlinePage.$('.sortable-subsection-list')).toExist(); + expect(outlinePage.$('.list-sections')).toExist(); + expect(outlinePage.$('.list-subsections')).toExist(); + expect(outlinePage.$('.list-units')).toExist(); }); it('shows a loading indicator', function() { @@ -142,18 +150,16 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" }); it('shows subsections initially collapsed', function() { - var subsectionElement; createCourseOutlinePage(this, mockCourseJSON); - subsectionElement = outlinePage.$('.outline-item-subsection'); - expect(subsectionElement).toHaveClass('collapsed'); - expect(outlinePage.$('.outline-item-unit')).not.toExist(); + verifyItemsExpanded('subsection', false); + expect(getItemsOfType('unit')).not.toExist(); }); }); describe("Button bar", function() { it('can add a section', function() { createCourseOutlinePage(this, mockEmptyCourseJSON); - outlinePage.$('.nav-actions .add-button').click(); + outlinePage.$('.nav-actions .button-new').click(); create_sinon.expectJsonRequest(requests, 'POST', '/xblock/', { 'category': 'chapter', 'display_name': 'Section', @@ -166,13 +172,13 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-course'); create_sinon.respondWithJson(requests, mockSingleSectionCourseJSON); expect(outlinePage.$('.no-content')).not.toExist(); - expect(outlinePage.$('.sortable-course-list li').data('locator')).toEqual('mock-section'); + expect(outlinePage.$('.list-sections li').data('locator')).toEqual('mock-section'); }); it('can add a second section', function() { var sectionElements; createCourseOutlinePage(this, mockSingleSectionCourseJSON); - outlinePage.$('.nav-actions .add-button').click(); + outlinePage.$('.nav-actions .button-new').click(); create_sinon.expectJsonRequest(requests, 'POST', '/xblock/', { 'category': 'chapter', 'display_name': 'Section', @@ -186,7 +192,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section-2'); create_sinon.respondWithJson(requests, createMockSectionJSON('mock-section-2', 'Mock Section 2', [])); - sectionElements = outlinePage.$('.sortable-course-list .outline-item-section'); + sectionElements = getItemsOfType('section'); expect(sectionElements.length).toBe(2); expect($(sectionElements[0]).data('locator')).toEqual('mock-section'); expect($(sectionElements[1]).data('locator')).toEqual('mock-section-2'); @@ -194,10 +200,11 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" it('can expand and collapse all sections', function() { createCourseOutlinePage(this, mockCourseJSON, false); - outlinePage.$('.nav-actions .toggle-button-expand-collapse').click(); - expect(outlinePage.$('.outline-item-section')).toHaveClass('collapsed'); - outlinePage.$('.nav-actions .toggle-button-expand-collapse').click(); - expect(outlinePage.$('.outline-item-section')).not.toHaveClass('collapsed'); + verifyItemsExpanded('section', true); + outlinePage.$('.nav-actions .button-toggle-expand-collapse .collapse-all').click(); + verifyItemsExpanded('section', false); + outlinePage.$('.nav-actions .button-toggle-expand-collapse .expand-all').click(); + verifyItemsExpanded('section', true); }); }); @@ -205,12 +212,12 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" it('shows an empty course message initially', function() { createCourseOutlinePage(this, mockEmptyCourseJSON); expect(outlinePage.$('.no-content')).not.toHaveClass('is-hidden'); - expect(outlinePage.$('.no-content .add-button')).toExist(); + expect(outlinePage.$('.no-content .button-new')).toExist(); }); it('can add a section', function() { createCourseOutlinePage(this, mockEmptyCourseJSON); - $('.no-content .add-button').click(); + $('.no-content .button-new').click(); create_sinon.expectJsonRequest(requests, 'POST', '/xblock/', { 'category': 'chapter', 'display_name': 'Section', @@ -223,13 +230,13 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-course'); create_sinon.respondWithJson(requests, mockSingleSectionCourseJSON); expect(outlinePage.$('.no-content')).not.toExist(); - expect(outlinePage.$('.sortable-course-list li').data('locator')).toEqual('mock-section'); + expect(outlinePage.$('.list-sections li').data('locator')).toEqual('mock-section'); }); it('remains empty if an add fails', function() { var requestCount; createCourseOutlinePage(this, mockEmptyCourseJSON); - $('.no-content .add-button').click(); + $('.no-content .button-new').click(); create_sinon.expectJsonRequest(requests, 'POST', '/xblock/', { 'category': 'chapter', 'display_name': 'Section', @@ -239,7 +246,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" create_sinon.respondWithError(requests); expect(requests.length).toBe(requestCount); // No additional requests should be made expect(outlinePage.$('.no-content')).not.toHaveClass('is-hidden'); - expect(outlinePage.$('.no-content .add-button')).toExist(); + expect(outlinePage.$('.no-content .button-new')).toExist(); }); }); @@ -247,7 +254,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" var getDisplayNameWrapper; getDisplayNameWrapper = function() { - return getHeaderElement('.outline-item-section').find('.wrapper-xblock-field').first(); + return getItemHeaders('section').find('.wrapper-xblock-field'); }; it('can be deleted', function() { @@ -256,7 +263,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" createMockSectionJSON('mock-section', 'Mock Section', []), createMockSectionJSON('mock-section-2', 'Mock Section 2', []) ])); - outlinePage.$('.outline-item-section .delete-button').first().click(); + getItemHeaders('section').find('.delete-button').first().click(); view_helpers.confirmPrompt(promptSpy); requestCount = requests.length; create_sinon.expectJsonRequest(requests, 'DELETE', '/xblock/mock-section'); @@ -269,32 +276,32 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" it('can be deleted if it is the only section', function() { var promptSpy = view_helpers.createPromptSpy(); createCourseOutlinePage(this, mockSingleSectionCourseJSON); - outlinePage.$('.outline-item-section .delete-button').click(); + getItemHeaders('section').find('.delete-button').click(); view_helpers.confirmPrompt(promptSpy); create_sinon.expectJsonRequest(requests, 'DELETE', '/xblock/mock-section'); create_sinon.respondWithJson(requests, {}); create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-course'); create_sinon.respondWithJson(requests, mockEmptyCourseJSON); expect(outlinePage.$('.no-content')).not.toHaveClass('is-hidden'); - expect(outlinePage.$('.no-content .add-button')).toExist(); + expect(outlinePage.$('.no-content .button-new')).toExist(); }); it('remains visible if its deletion fails', function() { var promptSpy = view_helpers.createPromptSpy(), requestCount; createCourseOutlinePage(this, mockSingleSectionCourseJSON); - outlinePage.$('.outline-item-section .delete-button').click(); + getItemHeaders('section').find('.delete-button').click(); view_helpers.confirmPrompt(promptSpy); create_sinon.expectJsonRequest(requests, 'DELETE', '/xblock/mock-section'); requestCount = requests.length; create_sinon.respondWithError(requests); expect(requests.length).toBe(requestCount); // No additional requests should be made - expect(outlinePage.$('.sortable-course-list li').data('locator')).toEqual('mock-section'); + expect(outlinePage.$('.list-sections li').data('locator')).toEqual('mock-section'); }); it('can add a subsection', function() { createCourseOutlinePage(this, mockCourseJSON); - outlinePage.$('.outline-item-section > .add-xblock-component .add-button').click(); + getItemsOfType('section').find('> .outline-content > .add-subsection .button-new').click(); create_sinon.expectJsonRequest(requests, 'POST', '/xblock/', { 'category': 'sequential', 'display_name': 'Subsection', @@ -329,9 +336,9 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" it('can be expanded and collapsed', function() { createCourseOutlinePage(this, mockCourseJSON); - collapseAndVerifyState('.outline-item-section'); - expandAndVerifyState('.outline-item-section'); - collapseAndVerifyState('.outline-item-section'); + collapseItemsAndVerifyState('section'); + expandItemsAndVerifyState('section'); + collapseItemsAndVerifyState('section'); }); }); @@ -339,13 +346,13 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" var getDisplayNameWrapper; getDisplayNameWrapper = function() { - return getHeaderElement('.outline-item-subsection').find('.wrapper-xblock-field').first(); + return getItemHeaders('subsection').find('.wrapper-xblock-field'); }; it('can be deleted', function() { var promptSpy = view_helpers.createPromptSpy(); createCourseOutlinePage(this, mockCourseJSON); - getHeaderElement('.outline-item-subsection').find('.delete-button').click(); + getItemHeaders('subsection').find('.delete-button').click(); view_helpers.confirmPrompt(promptSpy); create_sinon.expectJsonRequest(requests, 'DELETE', '/xblock/mock-subsection'); create_sinon.respondWithJson(requests, {}); @@ -358,7 +365,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" var redirectSpy; createCourseOutlinePage(this, mockCourseJSON); redirectSpy = spyOn(ViewUtils, 'redirect'); - outlinePage.$('.outline-item-subsection > .add-xblock-component .add-button').click(); + getItemsOfType('subsection').find('> .outline-content > .add-unit .button-new').click(); create_sinon.expectJsonRequest(requests, 'POST', '/xblock/', { 'category': 'vertical', 'display_name': 'Unit', @@ -387,20 +394,18 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" createMockSubsectionJSON('mock-subsection', updatedDisplayName, []) ])); // Find the display name again in the refreshed DOM and verify it - displayNameWrapper = getHeaderElement('.outline-item-subsection').find('.wrapper-xblock-field').first(); + displayNameWrapper = getItemHeaders('subsection').find('.wrapper-xblock-field'); view_helpers.verifyInlineEditChange(displayNameWrapper, updatedDisplayName); subsectionModel = outlinePage.model.get('child_info').children[0].get('child_info').children[0]; expect(subsectionModel.get('display_name')).toBe(updatedDisplayName); }); it('can be expanded and collapsed', function() { - var subsectionElement; createCourseOutlinePage(this, mockCourseJSON); - subsectionElement = outlinePage.$('.outline-item-subsection'); - expect(subsectionElement).toHaveClass('collapsed'); - expandAndVerifyState('.outline-item-subsection'); - collapseAndVerifyState('.outline-item-subsection'); - expandAndVerifyState('.outline-item-subsection'); + verifyItemsExpanded('subsection', false); + expandItemsAndVerifyState('subsection'); + collapseItemsAndVerifyState('subsection'); + expandItemsAndVerifyState('subsection'); }); }); @@ -409,8 +414,8 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" it('can be deleted', function() { var promptSpy = view_helpers.createPromptSpy(); createCourseOutlinePage(this, mockCourseJSON); - expandAndVerifyState('.outline-item-subsection'); - getHeaderElement('.outline-item-unit').find('.delete-button').click(); + expandItemsAndVerifyState('subsection'); + getItemHeaders('unit').find('.delete-button').click(); view_helpers.confirmPrompt(promptSpy); create_sinon.expectJsonRequest(requests, 'DELETE', '/xblock/mock-unit'); create_sinon.respondWithJson(requests, {}); @@ -420,12 +425,16 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" }); it('has a link to the unit page', function() { - var anchor; + var unitAnchor; createCourseOutlinePage(this, mockCourseJSON); - expandAndVerifyState('.outline-item-subsection'); - anchor = outlinePage.$('.outline-item-unit .xblock-title a'); - expect(anchor.attr('href')).toBe('/container/mock-unit'); + expandItemsAndVerifyState('subsection'); + unitAnchor = getItemsOfType('unit').find('.unit-title a'); + expect(unitAnchor.attr('href')).toBe('/container/mock-unit'); }); }); + + describe("Publishing State", function() { + // TODO: implement this!!!! + }); }); }); diff --git a/cms/static/js/spec/views/unit_outline_spec.js b/cms/static/js/spec/views/unit_outline_spec.js index 5396ce896c9d..63aab30c938b 100644 --- a/cms/static/js/spec/views/unit_outline_spec.js +++ b/cms/static/js/spec/views/unit_outline_spec.js @@ -25,12 +25,14 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" category: 'vertical', display_name: displayName, studio_url: '/container/mock-unit', + publish_state: 'unscheduled', ancestor_info: { ancestors: [{ id: 'mock-subsection', category: 'sequential', display_name: 'Mock Subsection', studio_url: '/course/mock-course?show=mock-subsection', + publish_state: 'unscheduled', child_info: { category: 'vertical', display_name: 'Unit', @@ -38,24 +40,28 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" id: 'mock-unit', category: 'vertical', display_name: displayName, - studio_url: '/container/mock-unit' + studio_url: '/container/mock-unit', + publish_state: 'unscheduled' }, { id: 'mock-unit-2', category: 'vertical', display_name: 'Mock Unit 2', - studio_url: '/container/mock-unit-2' + studio_url: '/container/mock-unit-2', + publish_state: 'unscheduled' }] } }, { id: 'mock-section', category: 'chapter', display_name: 'Section', - studio_url: '/course/slashes:mock-course?show=mock-section' + studio_url: '/course/slashes:mock-course?show=mock-section', + publish_state: 'unscheduled' }, { id: 'mock-course', category: 'course', display_name: 'Mock Course', - studio_url: '/course/mock-course' + studio_url: '/course/mock-course', + publish_state: 'unscheduled' }] }, metadata: { @@ -77,16 +83,16 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" it('can render itself', function() { createUnitOutlineView(this, createMockXBlockInfo('Mock Unit')); - expect(unitOutlineView.$('.sortable-course-list')).toExist(); - expect(unitOutlineView.$('.sortable-section-list')).toExist(); - expect(unitOutlineView.$('.sortable-subsection-list')).toExist(); + expect(unitOutlineView.$('.list-sections')).toExist(); + expect(unitOutlineView.$('.list-subsections')).toExist(); + expect(unitOutlineView.$('.list-units')).toExist(); }); it('can add a unit', function() { var redirectSpy; createUnitOutlineView(this, createMockXBlockInfo('Mock Unit')); redirectSpy = spyOn(ViewUtils, 'redirect'); - unitOutlineView.$('.outline-item-subsection > .add-xblock-component .add-button').click(); + unitOutlineView.$('.outline-subsection > .outline-content > .add-unit .button-new').click(); create_sinon.expectJsonRequest(requests, 'POST', '/xblock/', { category: 'vertical', display_name: 'Unit', @@ -106,8 +112,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" create_sinon.expectJsonRequest(requests, 'GET', '/xblock/mock-unit'); create_sinon.respondWithJson(requests, createMockXBlockInfo(updatedDisplayName)); - unitHeader = unitOutlineView.$('.outline-item-unit .wrapper-xblock-header'); - expect(unitHeader.find('.xblock-title').first().text().trim()).toBe(updatedDisplayName); + expect(unitOutlineView.$('.outline-unit .unit-title').first().text().trim()).toBe(updatedDisplayName); }); }); }); diff --git a/cms/static/js/views/baseview.js b/cms/static/js/views/baseview.js index bf0d456bbe05..b89591c98794 100644 --- a/cms/static/js/views/baseview.js +++ b/cms/static/js/views/baseview.js @@ -16,6 +16,10 @@ define(["jquery", "underscore", "backbone", "gettext", "js/utils/handle_iframe_b "click .ui-toggle-expansion": "toggleExpandCollapse" }, + options: { + collapsedClass: 'collapsed' + }, + //override the constructor function constructor: function(options) { _.bindAll(this, 'beforeRender', 'render', 'afterRender'); @@ -48,7 +52,7 @@ define(["jquery", "underscore", "backbone", "gettext", "js/utils/handle_iframe_b // this element, e.g. clicking on the element of a child view container in a parent. event.stopPropagation(); event.preventDefault(); - ViewUtils.toggleExpandCollapse(target); + ViewUtils.toggleExpandCollapse(target, this.options.collapsedClass); }, /** diff --git a/cms/static/js/views/pages/container.js b/cms/static/js/views/pages/container.js index cb2c33bc57f7..257b76fe15dd 100644 --- a/cms/static/js/views/pages/container.js +++ b/cms/static/js/views/pages/container.js @@ -12,6 +12,10 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views var XBlockContainerPage = BasePage.extend({ // takes XBlockInfo as a model + options: { + collapsedClass: 'is-collapsed' + }, + view: 'container_preview', initialize: function(options) { diff --git a/cms/static/js/views/pages/container_subviews.js b/cms/static/js/views/pages/container_subviews.js index 7bf27078a8d1..37bf7a2513d7 100644 --- a/cms/static/js/views/pages/container_subviews.js +++ b/cms/static/js/views/pages/container_subviews.js @@ -7,7 +7,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ var disabledCss = "is-disabled"; /** - * A view that calls render when "has_changes" or "published" values in XBlockInfo have changed + * A view that refreshes the view when certain values in the XBlockInfo have changed * after a server sync operation. */ var ContainerStateListenerView = BaseView.extend({ @@ -53,19 +53,20 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ */ var PreviewActionController = ContainerStateListenerView.extend({ shouldRefresh: function(model) { - return ViewUtils.hasChangedAttributes(model, ['has_changes', 'published']); + return ViewUtils.hasChangedAttributes(model, ['edited_on', 'published_on', 'publish_state']); }, render: function() { var previewAction = this.$el.find('.button-preview'), - viewLiveAction = this.$el.find('.button-view'); - if (this.model.get('published')) { + viewLiveAction = this.$el.find('.button-view'), + publishState = this.model.get('publish_state'); + if (publishState !== 'unscheduled') { viewLiveAction.removeClass(disabledCss); } else { viewLiveAction.addClass(disabledCss); } - if (this.model.get('has_changes') || !this.model.get('published')) { + if (publishState !== 'live' && publishState !== 'ready') { previewAction.removeClass(disabledCss); } else { @@ -98,25 +99,22 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ }, onSync: function(model) { - if (ViewUtils.hasChangedAttributes(model, [ - 'has_changes', 'published', 'edited_on', 'edited_by', 'visible_to_staff_only' - ])) { + if (ViewUtils.hasChangedAttributes(model, [ 'edited_on', 'published_on', 'publish_state' ])) { this.render(); } }, render: function () { this.$el.html(this.template({ - hasChanges: this.model.get('has_changes'), - published: this.model.get('published'), + publishState: this.model.get('publish_state'), editedOn: this.model.get('edited_on'), editedBy: this.model.get('edited_by'), + published: this.model.get('published'), publishedOn: this.model.get('published_on'), publishedBy: this.model.get('published_by'), releasedToStudents: this.model.get('released_to_students'), releaseDate: this.model.get('release_date'), - releaseDateFrom: this.model.get('release_date_from'), - visibleToStaffOnly: this.model.get('visible_to_staff_only') + releaseDateFrom: this.model.get('release_date_from') })); return this; @@ -138,7 +136,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ }, discardChanges: function (e) { - var xblockInfo = this.model, that=this, renderPage = this.renderPage; + var xblockInfo = this.model, renderPage = this.renderPage; if (e && e.preventDefault) { e.preventDefault(); } @@ -164,7 +162,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ if (e && e.preventDefault) { e.preventDefault(); } - enableStaffLock = !xblockInfo.get('visible_to_staff_only'); + enableStaffLock = xblockInfo.get('publish_state') !== 'staff_only'; revertCheckBox = function() { self.checkStaffLock(!enableStaffLock); @@ -223,7 +221,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ }, onSync: function(model) { - if (ViewUtils.hasChangedAttributes(model, ['published', 'published_on', 'published_by'])) { + if (ViewUtils.hasChangedAttributes(model, ['published_on'])) { this.render(); } }, diff --git a/cms/static/js/views/pages/course_outline.js b/cms/static/js/views/pages/course_outline.js index 95367b0626ed..58c0d263d53d 100644 --- a/cms/static/js/views/pages/course_outline.js +++ b/cms/static/js/views/pages/course_outline.js @@ -8,14 +8,18 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views // takes XBlockInfo as a model events: { - "click .toggle-button-expand-collapse": "toggleExpandCollapse" + "click .button-toggle-expand-collapse": "toggleExpandCollapse" + }, + + options: { + collapsedClass: 'is-collapsed' }, initialize: function() { var self = this; this.initialState = this.options.initialState; BasePage.prototype.initialize.call(this); - this.$('.add-button').click(function(event) { + this.$('.button-new').click(function(event) { self.outlineView.handleAddEvent(event); }); this.model.on('change', this.setCollapseExpandVisibility, this); @@ -23,19 +27,18 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views setCollapseExpandVisibility: function() { var has_content = this.hasContent(), - collapseExpandButton = $('.toggle-button-expand-collapse'); + collapseExpandButton = $('.button-toggle-expand-collapse'); if (has_content) { - collapseExpandButton.show(); + collapseExpandButton.removeClass('is-hidden'); } else { - collapseExpandButton.hide(); + collapseExpandButton.addClass('is-hidden'); } }, renderPage: function() { - var locatorToShow; this.setCollapseExpandVisibility(); this.outlineView = new CourseOutlineView({ - el: this.$('.course-outline'), + el: this.$('.outline'), model: this.model, isRoot: true, initialState: this.initialState @@ -50,19 +53,16 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views }, toggleExpandCollapse: function(event) { - var toggleButton = this.$('.toggle-button-expand-collapse'), + var toggleButton = this.$('.button-toggle-expand-collapse'), collapse = toggleButton.hasClass('collapse-all'); event.preventDefault(); toggleButton.toggleClass('collapse-all expand-all'); - this.$('.course-outline > ol > li').each(function(index, domElement) { - var element = $(domElement), - expandCollapseElement = element.find('.expand-collapse').first(); + this.$('.list-sections > li').each(function(index, domElement) { + var element = $(domElement); if (collapse) { - expandCollapseElement.removeClass('expand').addClass('collapse'); - element.addClass('collapsed'); + element.addClass('is-collapsed'); } else { - expandCollapseElement.addClass('expand').removeClass('collapse'); - element.removeClass('collapsed'); + element.removeClass('is-collapsed'); } }); } diff --git a/cms/static/js/views/unit_outline.js b/cms/static/js/views/unit_outline.js index 49366dadfe83..867e6843f6cd 100644 --- a/cms/static/js/views/unit_outline.js +++ b/cms/static/js/views/unit_outline.js @@ -10,6 +10,7 @@ define(['js/views/xblock_outline'], // takes XBlockInfo as a model templateName: 'unit-outline', + className: 'group-configurations-list', render: function() { XBlockOutlineView.prototype.render.call(this); @@ -23,7 +24,7 @@ define(['js/views/xblock_outline'], previousAncestor = null; if (this.model.get('ancestor_info')) { ancestors = this.model.get('ancestor_info').ancestors; - listElement = this.$('.sortable-list'); + listElement = this.getListElement(); // Note: the ancestors are processed in reverse order because the tree wants to // start at the root, but the ancestors are ordered by closeness to the unit, // i.e. subsection and then section. @@ -33,7 +34,7 @@ define(['js/views/xblock_outline'], ancestorView.render(); listElement.append(ancestorView.$el); previousAncestor = ancestor; - listElement = ancestorView.$('.sortable-list'); + listElement = ancestorView.getListElement(); } } return ancestorView; diff --git a/cms/static/js/views/utils/view_utils.js b/cms/static/js/views/utils/view_utils.js index f4f44126a82e..98eab24d961c 100644 --- a/cms/static/js/views/utils/view_utils.js +++ b/cms/static/js/views/utils/view_utils.js @@ -10,9 +10,13 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js /** * Toggles the expanded state of the current element. */ - toggleExpandCollapse = function(target) { + toggleExpandCollapse = function(target, collapsedClass) { + // Support the old 'collapsed' option until fully switched over to is-collapsed + if (!collapsedClass) { + collapsedClass = 'collapsed'; + } target.closest('.expand-collapse').toggleClass('expand collapse'); - target.closest('.is-collapsible, .window').toggleClass('collapsed'); + target.closest('.is-collapsible, .window').toggleClass(collapsedClass); target.closest('.is-collapsible').children('article').slideToggle(); }; diff --git a/cms/static/js/views/xblock_outline.js b/cms/static/js/views/xblock_outline.js index 31c07552160c..a5f2a41f0070 100644 --- a/cms/static/js/views/xblock_outline.js +++ b/cms/static/js/views/xblock_outline.js @@ -21,6 +21,10 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ var XBlockOutlineView = BaseView.extend({ // takes XBlockInfo as a model + options: { + collapsedClass: 'is-collapsed' + }, + templateName: 'xblock-outline', initialize: function() { @@ -94,8 +98,12 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ this.renderedChildren = true; }, + getListElement: function() { + return this.$('> .outline-content > ol'); + }, + addChildView: function(childView) { - this.$('> .sortable-list').append(childView.$el); + this.getListElement().append(childView.$el); }, addNameEditor: function() { @@ -136,7 +144,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ addButtonActions: function(element) { var self = this; element.find('.delete-button').click(_.bind(this.handleDeleteEvent, this)); - element.find('.add-button').click(_.bind(this.handleAddEvent, this)); + element.find('.button-new').click(_.bind(this.handleAddEvent, this)); }, shouldRenderChildren: function() { @@ -163,7 +171,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ xblockType = 'section'; } else if (category === 'sequential') { xblockType = 'subsection'; - } else if (category === 'vertical' && parentInfo && parentInfo.get('category') === 'sequential') { + } else if (category === 'vertical' && (!parentInfo || parentInfo.get('category') === 'sequential')) { xblockType = 'unit'; } return xblockType; diff --git a/cms/static/sass/elements/_modules.scss b/cms/static/sass/elements/_modules.scss index 4e97a5f665a2..b46fce054f66 100644 --- a/cms/static/sass/elements/_modules.scss +++ b/cms/static/sass/elements/_modules.scss @@ -318,7 +318,7 @@ $outline-indent-width: $baseline; border-left-color: $color-live; } - // CASE: has staff-only content + // CASE: is presented for staff only &.is-staff-only { border-left-color: $color-staff-only; } diff --git a/cms/static/sass/views/_container.scss b/cms/static/sass/views/_container.scss index a6957810a8a4..53079dd99572 100644 --- a/cms/static/sass/views/_container.scss +++ b/cms/static/sass/views/_container.scss @@ -25,14 +25,15 @@ font-weight: 600; } + // TODO: abstract out .is-editable { .incontext-editor-input { @extend %t-title4; - background: none repeat scroll 0 0 white; + @extend %t-strong; + background: none repeat scroll 0 0 $white; border: 0; box-shadow: 0 0 2px 2px $shadow inset; - font-weight: 600; } } } diff --git a/cms/static/sass/views/_outline.scss b/cms/static/sass/views/_outline.scss index e3f5ba202dc7..a99588a8a1c5 100644 --- a/cms/static/sass/views/_outline.scss +++ b/cms/static/sass/views/_outline.scss @@ -6,6 +6,37 @@ %outline-item-header { @include clearfix(); line-height: 0; + + // CASE: is-editable + // TODO: abstract out + .is-editable { + + .incontext-editor-open-action { + @include transition(opacity $tmg-f1 ease-in-out 0); + opacity: 0.0; + } + + .incontext-editor-form { + width: 100%; + } + + .incontext-editor-input { + @extend %t-title5; + @extend %t-strong; + width: 100%; + background: none repeat scroll 0 0 $white; + border: 0; + box-shadow: 0 0 2px 2px $shadow-l1 inset; + } + + // STATE: hover/focus + &:hover, &:focus { + + .incontext-editor-open-action { + opacity: 1.0; + } + } + } } %outline-item-content-hidden { diff --git a/cms/templates/container.html b/cms/templates/container.html index bf611c0a2703..1981e3191ec2 100644 --- a/cms/templates/container.html +++ b/cms/templates/container.html @@ -10,7 +10,6 @@ <%! import json -from xmodule.modulestore import PublishState from contentstore.views.helpers import xblock_studio_url, xblock_type_display_name from django.utils.translation import ugettext as _ %> diff --git a/cms/templates/course_outline.html b/cms/templates/course_outline.html index 41242b72290b..e01430921972 100644 --- a/cms/templates/course_outline.html +++ b/cms/templates/course_outline.html @@ -47,13 +47,13 @@

${_("Page Actions")}

    @@ -72,7 +72,7 @@

    ${_("Page Actions")}

    <% course_locator = context_course.location %> -
    +
    diff --git a/cms/templates/js/course-outline.underscore b/cms/templates/js/course-outline.underscore index a9d52cfddbfa..05fd8eb86fd5 100644 --- a/cms/templates/js/course-outline.underscore +++ b/cms/templates/js/course-outline.underscore @@ -1,79 +1,141 @@ +<% +var category = xblockInfo.get('category'); +var releasedToStudents = xblockInfo.get('released_to_students'); +var publishState = xblockInfo.get('publish_state'); + +var publishClass = ''; +if (publishState === 'staff_only') { + publishClass = 'is-staff-only'; +} else if (publishState === 'live') { + publishClass = 'is-live'; +} else if (publishState === 'ready') { + publishClass = 'is-ready'; +} else if (publishState === 'has_unpublished_content') { + publishClass = 'has-warnings'; +} + +var listType = 'list-unknown'; +if (xblockType === 'course') { + listType = 'list-sections'; +} else if (xblockType === 'section') { + listType = 'list-subsections'; +} else if (xblockType === 'subsection') { + listType = 'list-units'; +} + +var statusMessage = null; +var statusType = null; +if (publishState === 'is_staff_only') { + statusType = 'staff-only'; + statusMessage = 'Contains staff only content'; +} else if (publishState === 'has_unpublished_content') { + if (category === 'vertical') { + statusType = 'warning'; + if (releasedToStudents) { + statusMessage = 'Unpublished changes to live content'; + } else { + statusMessage = 'Unpublished units will not be released'; + } + } +} + +var statusIconClass = ''; +if (statusType === 'warning') { + statusIconClass = 'icon-file-alt'; +} else if (statusType === 'error') { + statusIconClass = 'icon-warning-sign'; +} else if (statusType === 'staff-only') { + statusIconClass = 'icon-lock'; +} +%> <% if (parentInfo) { %> -
  • -
    -
    - <% if (includesChildren) { %> -

    - - <% } else { %> -

    - <% } %> +
    + <% if (includesChildren) { %> +

    + + <% } else { %> +

    + <% } %> - <% if (xblockInfo.get('category') === 'vertical') { %> - <%= xblockInfo.get('display_name') %> + <% if (category === 'vertical') { %> + + <%= xblockInfo.get('display_name') %> + <% } else { %> - "> - <%= xblockInfo.get('display_name') %> + "> + <%= xblockInfo.get('display_name') %> <% } %>

    - + -
    - <% if (xblockInfo.get('edited_on')) { %> -
    - <% if (xblockInfo.get('published')) { %> - - <%= gettext('Released:') %> Dec 31, 2015 at 21:00 UTC - <% } else { %> - - <%= gettext('Scheduled:') %> Dec 31, 2015 at 21:00 UTC - <% } %> +
    + <% if (statusMessage) { %> +
    + <% if (category !== 'vertical') { %> +
    +

    + Release Status: + + <% if (xblockInfo.get('released_to_students')) { %> + + <%= gettext('Released:') %> + <% } else if (xblockInfo.get('release_date')) { %> + + <%= gettext('Scheduled:') %> + <% } else { %> + + <%= gettext('Unscheduled') %> + <% } %> + <% if (xblockInfo.get('release_date')) { %> + <%= xblockInfo.get('release_date') %> + <% } %> + +

    <% } %> - - -
    -
      -
    +
    + +

    <%- statusMessage %>

    -
    + <% } %> <% } %> <% if (!parentInfo && xblockInfo.get('child_info') && xblockInfo.get('child_info').children.length === 0) { %> -
    +

    <%= gettext("You haven't added any content to this course yet.") %> - - <%= addChildLabel %> + <%= addChildLabel %>

    <% } else { %> -
      -
    - - <% if (childType) { %> - - <% } %> +
    +
      +
    + <% if (childType) { %> + + <% } %> +
    <% } %> <% if (parentInfo) { %> diff --git a/cms/templates/js/mock/mock-course-outline-page.underscore b/cms/templates/js/mock/mock-course-outline-page.underscore index 54186ca4f6b8..712ded240b2a 100644 --- a/cms/templates/js/mock/mock-course-outline-page.underscore +++ b/cms/templates/js/mock/mock-course-outline-page.underscore @@ -11,13 +11,13 @@

    Page Actions

      @@ -33,10 +33,10 @@
      -
      +

      You haven't added any content to this course yet. - + Add Section

      diff --git a/cms/templates/js/publish-history.underscore b/cms/templates/js/publish-history.underscore index d28c329ccfa0..3f4e5c5a90b4 100644 --- a/cms/templates/js/publish-history.underscore +++ b/cms/templates/js/publish-history.underscore @@ -1,16 +1,16 @@ +<% +var copy = gettext("Never published"); +if (published_on && published_by) { + var message = gettext("Last published %(last_published_date)s by %(publish_username)s"); + copy = interpolate(message, { + last_published_date: '' + published_on + '', + publish_username: '' + published_by + '' + }, true); +} else if (published) { + copy = gettext("Previously published"); +} +%> +
      -

      - <% if (published) { - if (published_on && published_by) { - var message = gettext("Last published %(last_published_date)s by %(publish_username)s"); %> - <%= interpolate(message, { - last_published_date: '' + published_on + '', - publish_username: '' + published_by + '' }, true) %> - <% } else { %> - <%= gettext("Previously published") %> - <% } %> - <% } else { %> - <%= gettext("Never published") %> - <% } %> -

      -
      \ No newline at end of file +

      <%= copy %>

      +
      diff --git a/cms/templates/js/publish-xblock.underscore b/cms/templates/js/publish-xblock.underscore index f9b30bcb38c1..21fc7a1fe7f8 100644 --- a/cms/templates/js/publish-xblock.underscore +++ b/cms/templates/js/publish-xblock.underscore @@ -1,31 +1,45 @@ <% -var publishClasses = ""; -var title = gettext("Draft (Never published)"); -if (published) { - if (published && hasChanges) { - publishClasses = publishClasses + " is-draft"; - title = gettext("Draft (Unpublished changes)"); - } else { - publishClasses = publishClasses + " is-published"; - title = gettext("Published"); - } -} -if (releaseDate) { - publishClasses = publishClasses + " is-scheduled"; +var publishClass = ''; +if (publishState === 'staff_only') { + publishClass = 'is-staff-only'; +} else if (publishState === 'live') { + publishClass = 'is-live is-published is-released'; +} else if (publishState === 'ready') { + publishClass = 'is-ready is-published'; +} else if (publishState === 'has_unpublished_content') { + publishClass = 'has-warnings is-draft'; } -if (visibleToStaffOnly) { - publishClasses = publishClasses + " is-staff-only"; + +var title = gettext("Draft (Never published)"); +if (publishState === 'staff_only') { title = gettext("Unpublished (Staff only)"); +} else if (publishState === 'live') { + title = gettext("Published and Live"); +} else if (publishState === 'ready') { + title = gettext("Published"); +} else if (publishState === 'has_unpublished_content') { + title = gettext("Draft (Unpublished changes)"); } + +var releaseLabel = gettext("Release:"); +if (publishState === 'live') { + releaseLabel = gettext("Released:"); +} else if (publishState === 'ready') { + releaseLabel = gettext("Scheduled:"); +} + +var canPublish = publishState !== 'ready' && publishState !== 'live'; +var canDiscardChanges = publishState === 'has_unpublished_content'; +var visibleToStaffOnly = publishState === 'staff_only'; %> -
      +

      <%= gettext("Publishing Status") %> <%= title %>

      - <% if (hasChanges && editedOn && editedBy) { + <% if (publishState === 'has_unpublished_content' && editedOn && editedBy) { var message = gettext("Draft saved on %(last_saved_date)s by %(edit_username)s") %> <%= interpolate(message, { last_saved_date: '' + editedOn + '', @@ -42,17 +56,7 @@ if (visibleToStaffOnly) {

      -
      - <% if (published && releaseDate) { - if (releasedToStudents) { %> - <%= gettext("Released:") %> - <% } else { %> - <%= gettext("Scheduled:") %> - <% } - } else { %> - <%= gettext("Release:") %> - <% } %> -
      +
      <%= releaseLabel %>

      <% if (releaseDate) { %> <% var message = gettext("%(release_date)s with %(section_or_subsection)s") %> @@ -87,12 +91,12 @@ if (visibleToStaffOnly) {

      • - <%= gettext("Publish") %>
      • - <%= gettext("Discard Changes") %>
      • diff --git a/cms/templates/js/unit-outline.underscore b/cms/templates/js/unit-outline.underscore index 70d3cf4123f7..ae30d8d565f3 100644 --- a/cms/templates/js/unit-outline.underscore +++ b/cms/templates/js/unit-outline.underscore @@ -1,27 +1,50 @@ +<% +var publishState = xblockInfo.get('publish_state'); +var publishClass = ''; +if (publishState === 'staff_only') { + publishClass = 'is-staff-only'; +} else if (publishState === 'live') { + publishClass = 'is-live'; +} else if (publishState === 'ready') { + publishClass = 'is-ready'; +} else if (publishState === 'has_unpublished_content') { + publishClass = 'has_warnings'; +} + +var listType = 'list-for-' + xblockType; +if (xblockType === 'course') { + listType = 'list-sections'; +} else if (xblockType === 'section') { + listType = 'list-subsections'; +} else if (xblockType === 'subsection') { + listType = 'list-units'; +} +%> <% if (parentInfo) { %> -
      • -
        - <% } %> -
          -
        - - <% if (childType) { %> - - <% } %> +
        +
          +
        + <% if (childType) { %> + + <% } %> +
        <% if (parentInfo) { %> -
      • + <% } %> diff --git a/cms/templates/js/xblock-outline.underscore b/cms/templates/js/xblock-outline.underscore index 2dec21afd66b..044309408854 100644 --- a/cms/templates/js/xblock-outline.underscore +++ b/cms/templates/js/xblock-outline.underscore @@ -1,5 +1,5 @@ <% if (parentInfo) { %> -
      • @@ -56,7 +56,7 @@ <% if (!parentInfo && xblockInfo.get('child_info') && xblockInfo.get('child_info').children.length === 0) { %>

        <%= gettext("You haven't added any content to this course yet.") %> - <%= addChildLabel %> @@ -68,7 +68,7 @@ <% if (childType) { %>

        - <%= addChildLabel %> diff --git a/common/lib/xmodule/xmodule/modulestore/__init__.py b/common/lib/xmodule/xmodule/modulestore/__init__.py index b2bc7b0eaf2c..8e5b13f31237 100644 --- a/common/lib/xmodule/xmodule/modulestore/__init__.py +++ b/common/lib/xmodule/xmodule/modulestore/__init__.py @@ -89,11 +89,11 @@ class UserID(object): # user ID to use for tests that do not have a django user available test = -3 -class PublishState(object): - """ - The publish state for a given xblock-- either 'draft', 'private', or 'public'. - Currently in CMS, an xblock can only be in 'draft' or 'private' if it is at or below the Unit level. +class LegacyPublishState(object): + """ + The legacy publish state for a given xblock-- either 'draft', 'private', or 'public'. These states + are no longer used in Studio directly, but are still referenced in a few places. """ draft = 'draft' private = 'private' @@ -301,10 +301,10 @@ def compute_publish_state(self, xblock): Returns whether this xblock is draft, public, or private. Returns: - PublishState.draft - content is in the process of being edited, but still has a previous + LegacyPublishState.draft - content is in the process of being edited, but still has a previous version deployed to LMS - PublishState.public - content is locked and deployed to LMS - PublishState.private - content is editable and not deployed to LMS + LegacyPublishState.public - content is locked and deployed to LMS + LegacyPublishState.private - content is editable and not deployed to LMS """ pass @@ -522,7 +522,7 @@ def compute_publish_state(self, xblock): """ Returns PublishState.public since this is a read-only store. """ - return PublishState.public + return LegacyPublishState.public def heartbeat(self): """ diff --git a/common/lib/xmodule/xmodule/modulestore/mixed.py b/common/lib/xmodule/xmodule/modulestore/mixed.py index 4061daf3c3d2..4759fe9e3519 100644 --- a/common/lib/xmodule/xmodule/modulestore/mixed.py +++ b/common/lib/xmodule/xmodule/modulestore/mixed.py @@ -438,10 +438,10 @@ def compute_publish_state(self, xblock): Returns whether this xblock is draft, public, or private. Returns: - PublishState.draft - content is in the process of being edited, but still has a previous + LegacyPublishState.draft - content is in the process of being edited, but still has a previous version deployed to LMS - PublishState.public - content is locked and deployed to LMS - PublishState.private - content is editable and not deployed to LMS + LegacyPublishState.public - content is locked and deployed to LMS + LegacyPublishState.private - content is editable and not deployed to LMS """ course_id = xblock.scope_ids.usage_id.course_key store = self._get_modulestore_for_courseid(course_id) diff --git a/common/lib/xmodule/xmodule/modulestore/mongo/draft.py b/common/lib/xmodule/xmodule/modulestore/mongo/draft.py index 63f960da5b84..99529080e01c 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo/draft.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo/draft.py @@ -11,7 +11,7 @@ from opaque_keys.edx.locations import Location from xmodule.exceptions import InvalidVersionError -from xmodule.modulestore import PublishState, ModuleStoreEnum +from xmodule.modulestore import LegacyPublishState, ModuleStoreEnum from xmodule.modulestore.exceptions import ( ItemNotFoundError, DuplicateItemError, InvalidBranchSetting, DuplicateCourseError ) @@ -601,7 +601,7 @@ def has_changes(self, location): return False # don't check children if this block has changes (is not public) - if self.compute_publish_state(item) != PublishState.public: + if self.compute_publish_state(item) != LegacyPublishState.public: return True # if this block doesn't have changes, then check its children elif item.has_children: @@ -780,10 +780,10 @@ def compute_publish_state(self, xblock): Returns whether this xblock is draft, public, or private. Returns: - PublishState.draft - content is in the process of being edited, but still has a previous + LegacyPublishState.draft - content is in the process of being edited, but still has a previous version deployed to LMS - PublishState.public - content is locked and deployed to LMS - PublishState.private - content is editable and not deployed to LMS + LegacyPublishState.public - content is locked and deployed to LMS + LegacyPublishState.private - content is editable and not deployed to LMS """ if getattr(xblock, 'is_draft', False): published_xblock_location = as_published(xblock.location) @@ -791,11 +791,11 @@ def compute_publish_state(self, xblock): {'_id': published_xblock_location.to_deprecated_son()} ) if published_item is None: - return PublishState.private + return LegacyPublishState.private else: - return PublishState.draft + return LegacyPublishState.draft else: - return PublishState.public + return LegacyPublishState.public def _verify_branch_setting(self, expected_branch_setting): """ diff --git a/common/lib/xmodule/xmodule/modulestore/split_mongo/split_draft.py b/common/lib/xmodule/xmodule/modulestore/split_mongo/split_draft.py index 844fee3ffcf4..90a000fc6d7e 100644 --- a/common/lib/xmodule/xmodule/modulestore/split_mongo/split_draft.py +++ b/common/lib/xmodule/xmodule/modulestore/split_mongo/split_draft.py @@ -4,9 +4,8 @@ from ..exceptions import ItemNotFoundError from split import SplitMongoModuleStore -from xmodule.modulestore import ModuleStoreEnum, PublishState +from xmodule.modulestore import ModuleStoreEnum, LegacyPublishState from xmodule.modulestore.draft_and_published import ModuleStoreDraftAndPublished -from xmodule.modulestore.draft import DIRECT_ONLY_CATEGORIES class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleStore): @@ -159,9 +158,9 @@ def compute_publish_state(self, xblock): Returns whether this xblock is draft, public, or private. Returns: - PublishState.draft - published exists and is different from draft - PublishState.public - published exists and is the same as draft - PublishState.private - no published version exists + LegacyPublishState.draft - published exists and is different from draft + LegacyPublishState.public - published exists and is the same as draft + LegacyPublishState.private - no published version exists """ # TODO figure out what to say if xblock is not from the HEAD of its branch def get_head(branch): @@ -172,7 +171,7 @@ def get_head(branch): try: other = get_head(ModuleStoreEnum.BranchName.published) except ItemNotFoundError: - return PublishState.private + return LegacyPublishState.private elif xblock.location.branch == ModuleStoreEnum.BranchName.published: other = get_head(ModuleStoreEnum.BranchName.draft) else: @@ -180,13 +179,13 @@ def get_head(branch): if not other: if xblock.location.branch == ModuleStoreEnum.BranchName.draft: - return PublishState.private + return LegacyPublishState.private else: - return PublishState.public + return LegacyPublishState.public elif xblock.update_version != other['edit_info']['update_version']: - return PublishState.draft + return LegacyPublishState.draft else: - return PublishState.public + return LegacyPublishState.public def convert_to_draft(self, location, user_id): """ diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py index 13efa715ecd2..fdd2a245740d 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py @@ -8,7 +8,7 @@ from xmodule.tests import DATA_DIR from opaque_keys.edx.locations import Location -from xmodule.modulestore import ModuleStoreEnum, PublishState +from xmodule.modulestore import ModuleStoreEnum, LegacyPublishState from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.exceptions import InvalidVersionError @@ -744,22 +744,22 @@ def test_compute_publish_state(self, default_ms): # start off as Private item = self.store.create_child(self.user_id, self.writable_chapter_location, 'problem', 'test_compute_publish_state') item_location = item.location.version_agnostic() - self.assertEquals(self.store.compute_publish_state(item), PublishState.private) + self.assertEquals(self.store.compute_publish_state(item), LegacyPublishState.private) # Private -> Public self.store.publish(item_location, self.user_id) item = self.store.get_item(item_location) - self.assertEquals(self.store.compute_publish_state(item), PublishState.public) + self.assertEquals(self.store.compute_publish_state(item), LegacyPublishState.public) # Public -> Private self.store.unpublish(item_location, self.user_id) item = self.store.get_item(item_location) - self.assertEquals(self.store.compute_publish_state(item), PublishState.private) + self.assertEquals(self.store.compute_publish_state(item), LegacyPublishState.private) # Private -> Public self.store.publish(item_location, self.user_id) item = self.store.get_item(item_location) - self.assertEquals(self.store.compute_publish_state(item), PublishState.public) + self.assertEquals(self.store.compute_publish_state(item), LegacyPublishState.public) # Public -> Draft with NO changes # Note: This is where Split and Mongo differ @@ -767,14 +767,14 @@ def test_compute_publish_state(self, default_ms): item = self.store.get_item(item_location) self.assertEquals( self.store.compute_publish_state(item), - PublishState.draft if default_ms == 'draft' else PublishState.public + LegacyPublishState.draft if default_ms == 'draft' else LegacyPublishState.public ) # Draft WITH changes item.display_name = 'new name' item = self.store.update_item(item, self.user_id) self.assertTrue(self.store.has_changes(item.location)) - self.assertEquals(self.store.compute_publish_state(item), PublishState.draft) + self.assertEquals(self.store.compute_publish_state(item), LegacyPublishState.draft) @ddt.data('draft', 'split') def test_get_courses_for_wiki_shared(self, default_ms): diff --git a/common/test/acceptance/pages/studio/overview.py b/common/test/acceptance/pages/studio/overview.py index 12b3cf5207cd..804f1f75ff04 100644 --- a/common/test/acceptance/pages/studio/overview.py +++ b/common/test/acceptance/pages/studio/overview.py @@ -113,7 +113,7 @@ def add_child(self, require_notification=True): """ click_css( self, - self._bounded_selector(".add-xblock-component a.add-button"), + self._bounded_selector(".add-item a.button-new"), require_notification=require_notification, ) @@ -125,7 +125,7 @@ def toggle_expand(self): self.browser.execute_script("jQuery.fx.off = true;") def subsection_expanded(): - add_button = self.q(css=self._bounded_selector('> .add-xblock-component a.add-button')).first.results + add_button = self.q(css=self._bounded_selector('> .outline-content > .add-item a.button-new')).first.results return add_button and add_button[0].is_displayed() currently_expanded = subsection_expanded() @@ -171,8 +171,8 @@ class CourseOutlineUnit(CourseOutlineChild): PageObject that wraps a unit link on the Studio Course Outline page. """ url = None - BODY_SELECTOR = '.outline-item-unit' - NAME_SELECTOR = '.xblock-title a' + BODY_SELECTOR = '.outline-unit' + NAME_SELECTOR = '.unit-title a' def go_to(self): """ @@ -188,7 +188,9 @@ class CourseOutlineSubsection(CourseOutlineChild, CourseOutlineContainer): """ url = None - BODY_SELECTOR = '.outline-item-subsection' + BODY_SELECTOR = '.outline-subsection' + NAME_SELECTOR = '.subsection-title' + NAME_FIELD_WRAPPER_SELECTOR = '.subsection-header .wrapper-xblock-field' CHILD_CLASS = CourseOutlineUnit def unit(self, title): @@ -221,7 +223,9 @@ class CourseOutlineSection(CourseOutlineChild, CourseOutlineContainer): :class`.PageObject` that wraps a section block on the Studio Course Outline page. """ url = None - BODY_SELECTOR = '.outline-item-section' + BODY_SELECTOR = '.outline-section' + NAME_SELECTOR = '.section-title' + NAME_FIELD_WRAPPER_SELECTOR = '.section-header .wrapper-xblock-field' CHILD_CLASS = CourseOutlineSubsection def subsection(self, title): @@ -265,7 +269,7 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer): url_path = "course" CHILD_CLASS = CourseOutlineSection EXPAND_COLLAPSE_CSS = '.toggle-button-expand-collapse' - BOTTOM_ADD_SECTION_BUTTON = '.course-outline > .add-xblock-component .add-button' + BOTTOM_ADD_SECTION_BUTTON = '.outline > .add-section .button-new' def is_browser_on_page(self): return self.q(css='body.view-outline').present @@ -299,7 +303,7 @@ def add_section_from_top_button(self): """ Clicks the button for adding a section which resides at the top of the screen. """ - click_css(self, '.wrapper-mast nav.nav-actions .add-button') + click_css(self, '.wrapper-mast nav.nav-actions .button-new') def add_section_from_bottom_button(self): """ From 31d8f51616b4dd0df88c7a3eb43bff1dcb74c4fb Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Thu, 24 Jul 2014 18:51:13 -0400 Subject: [PATCH 03/11] Studio: outline UI revisions: * syncing up unit publishing state UI with stateful names/styles * revising outline item status message display logic to show release status * fixing publishState value typo in outline UI template * refining and syncing incontext editor styling * maintaining visual alignment of collapsed/expanded sections in outline UI * simplifying page-level action styles on outline UI --- cms/static/sass/elements/_forms.scss | 3 +- cms/static/sass/elements/_layout.scss | 6 --- cms/static/sass/elements/_modules.scss | 12 +++-- cms/static/sass/views/_container.scss | 21 ++++++-- cms/static/sass/views/_outline.scss | 23 +++++++-- cms/templates/course_outline.html | 10 ++-- cms/templates/js/course-outline.underscore | 59 +++++++++++----------- cms/templates/js/publish-xblock.underscore | 4 +- cms/templates/ux/reference/outline.html | 10 ++-- 9 files changed, 91 insertions(+), 57 deletions(-) diff --git a/cms/static/sass/elements/_forms.scss b/cms/static/sass/elements/_forms.scss index 1c1ff8eef0ad..da648a644752 100644 --- a/cms/static/sass/elements/_forms.scss +++ b/cms/static/sass/elements/_forms.scss @@ -327,8 +327,9 @@ form[class^="create-"] { } -// form - inline xblock name edit on unit, container, outline? +// form - inline xblock name edit on unit, container, outline +// TOOD: abstract this out into a Sass placeholder .incontext-editor.is-editable { .incontext-editor-value, diff --git a/cms/static/sass/elements/_layout.scss b/cms/static/sass/elements/_layout.scss index 764af355521d..942a85e7ef4b 100644 --- a/cms/static/sass/elements/_layout.scss +++ b/cms/static/sass/elements/_layout.scss @@ -83,12 +83,6 @@ @extend %btn-primary-green; @extend %sizing; } - - // CASE: toggle button - &.button-toggle { - @extend %btn-secondary-gray; - @extend %sizing; - } } } } diff --git a/cms/static/sass/elements/_modules.scss b/cms/static/sass/elements/_modules.scss index b46fce054f66..16c5f89bb829 100644 --- a/cms/static/sass/elements/_modules.scss +++ b/cms/static/sass/elements/_modules.scss @@ -269,12 +269,15 @@ $outline-indent-width: $baseline; // UI: section %outline-section { - @include transition(border-left-width $tmg-f2 linear 0s, border-left-color $tmg-f2 linear 0s); + @include transition(border-left-width $tmg-f2 linear 0s, border-left-color $tmg-f2 linear 0s, padding-left $tmg-f2 linear 0s); border-left: 1px solid $color-draft; + margin-bottom: $baseline; + padding: ($baseline*0.75) $baseline ($baseline*0.75) ($baseline + 4); // STATE: is-collapsed &.is-collapsed { border-left-width: ($baseline/4); + padding-left: $baseline; // CASE: is ready to be live &.is-ready { @@ -306,7 +309,12 @@ $outline-indent-width: $baseline; // UI: subsection %outline-subsection { @include transition(border-left-color $tmg-f2 linear 0s); + margin-bottom: ($baseline/2); + border: 1px solid $gray-l4; border-left: ($baseline/4) solid $color-draft; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + padding: ($baseline*0.75); // CASE: is ready to be live &.is-ready { @@ -405,8 +413,6 @@ $outline-indent-width: $baseline; @extend %ui-window; @extend %outline-item; @extend %outline-section; - margin-bottom: $baseline; - padding: ($baseline*0.75) $baseline; // header - title .section-title { diff --git a/cms/static/sass/views/_container.scss b/cms/static/sass/views/_container.scss index 53079dd99572..08b279941e33 100644 --- a/cms/static/sass/views/_container.scss +++ b/cms/static/sass/views/_container.scss @@ -25,15 +25,23 @@ font-weight: 600; } - // TODO: abstract out .is-editable { + // TOOD: abstract this out into a Sass placeholder .incontext-editor-input { + @include transition(box-shadow $tmg-f1 ease-in-out 0, color $tmg-f1 ease-in-out 0); @extend %t-title4; @extend %t-strong; + width: 100%; background: none repeat scroll 0 0 $white; border: 0; box-shadow: 0 0 2px 2px $shadow inset; + + // STATE: focus + &:focus { + box-shadow: 0 0 2px 2px rgba($ui-action-primary-color-focus, 0.50) inset; + color: $ui-action-primary-color-focus; + } } } } @@ -99,16 +107,23 @@ .bit-publishing { @extend %bar-module; - &.published, - &.is-published { + // CASE: content is ready to be made live + &.is-ready { @extend %bar-module-green; } + // CASE: content is live + &.is-live { + @extend %bar-module-blue; + } + + // CASE: content is draft &.draft , &.is-draft { @extend %bar-module-yellow; } + // CASE: content is staff only &.staff-only, &.is-staff-only { @extend %bar-module-black; diff --git a/cms/static/sass/views/_outline.scss b/cms/static/sass/views/_outline.scss index a99588a8a1c5..3a7c70a2fb7b 100644 --- a/cms/static/sass/views/_outline.scss +++ b/cms/static/sass/views/_outline.scss @@ -18,15 +18,23 @@ .incontext-editor-form { width: 100%; + position: relative; + top: -($baseline/4); } + // TOOD: abstract this out into a Sass placeholder .incontext-editor-input { - @extend %t-title5; - @extend %t-strong; + @include transition(box-shadow $tmg-f1 ease-in-out 0, color $tmg-f1 ease-in-out 0); width: 100%; background: none repeat scroll 0 0 $white; border: 0; - box-shadow: 0 0 2px 2px $shadow-l1 inset; + box-shadow: 0 0 2px 2px $shadow inset; + + // STATE: focus + &:focus { + box-shadow: 0 0 2px 2px rgba($ui-action-primary-color-focus, 0.50) inset; + color: $ui-action-primary-color-focus; + } } // STATE: hover/focus @@ -247,6 +255,11 @@ // header .section-header { @extend %outline-item-header; + + .incontext-editor-input { + @extend %t-strong; + @extend %t-title5; + } } .section-header-details { @@ -320,6 +333,10 @@ // header .subsection-header { @extend %outline-item-header; + + .incontext-editor-input { + @extend %t-title6; + } } .subsection-header-details { diff --git a/cms/templates/course_outline.html b/cms/templates/course_outline.html index e01430921972..29753df80485 100644 --- a/cms/templates/course_outline.html +++ b/cms/templates/course_outline.html @@ -47,14 +47,14 @@

        ${_("Page Actions")}

        - <% if (statusMessage) { %> -
        - <% if (category !== 'vertical') { %> -
        -

        - Release Status: - - <% if (xblockInfo.get('released_to_students')) { %> - - <%= gettext('Released:') %> - <% } else if (xblockInfo.get('release_date')) { %> - - <%= gettext('Scheduled:') %> - <% } else { %> - - <%= gettext('Unscheduled') %> - <% } %> - <% if (xblockInfo.get('release_date')) { %> - <%= xblockInfo.get('release_date') %> - <% } %> - -

        -
        - <% } %> -
        - -

        <%- statusMessage %>

        +
        + <% if (category !== 'vertical') { %> +
        +

        + Release Status: + + <% if (xblockInfo.get('released_to_students')) { %> + + <%= gettext('Released:') %> + <% } else if (xblockInfo.get('release_date')) { %> + + <%= gettext('Scheduled:') %> + <% } else { %> + + <%= gettext('Unscheduled') %> + <% } %> + <% if (xblockInfo.get('release_date')) { %> + <%= xblockInfo.get('release_date') %> + <% } %> + +

        + <% } %> + + <% if (statusMessage) { %> +
        + +

        <%- statusMessage %>

        - <% } %> + <% } %> +
        <% } %> <% if (!parentInfo && xblockInfo.get('child_info') && xblockInfo.get('child_info').children.length === 0) { %> diff --git a/cms/templates/js/publish-xblock.underscore b/cms/templates/js/publish-xblock.underscore index 21fc7a1fe7f8..76d5238c6871 100644 --- a/cms/templates/js/publish-xblock.underscore +++ b/cms/templates/js/publish-xblock.underscore @@ -3,9 +3,9 @@ var publishClass = ''; if (publishState === 'staff_only') { publishClass = 'is-staff-only'; } else if (publishState === 'live') { - publishClass = 'is-live is-published is-released'; + publishClass = 'is-live'; } else if (publishState === 'ready') { - publishClass = 'is-ready is-published'; + publishClass = 'is-ready'; } else if (publishState === 'has_unpublished_content') { publishClass = 'has-warnings is-draft'; } diff --git a/cms/templates/ux/reference/outline.html b/cms/templates/ux/reference/outline.html index 41857bda0f53..656a957b60d7 100644 --- a/cms/templates/ux/reference/outline.html +++ b/cms/templates/ux/reference/outline.html @@ -20,14 +20,14 @@

        Page Actions

        diff --git a/cms/templates/js/course-outline.underscore b/cms/templates/js/course-outline.underscore index 379029d71a33..2bcf4a36b85d 100644 --- a/cms/templates/js/course-outline.underscore +++ b/cms/templates/js/course-outline.underscore @@ -1,18 +1,7 @@ <% var category = xblockInfo.get('category'); var releasedToStudents = xblockInfo.get('released_to_students'); -var publishState = xblockInfo.get('publish_state'); - -var publishClass = ''; -if (publishState === 'staff_only') { - publishClass = 'is-staff-only'; -} else if (publishState === 'live') { - publishClass = 'is-live'; -} else if (publishState === 'ready') { - publishClass = 'is-ready'; -} else if (publishState === 'has_unpublished_content') { - publishClass = 'has-warnings'; -} +var visibilityState = xblockInfo.get('visibility_state'); var listType = 'list-unknown'; if (xblockType === 'course') { @@ -25,10 +14,10 @@ if (xblockType === 'course') { var statusMessage = null; var statusType = null; -if (publishState === 'staff_only') { +if (visibilityState === 'staff_only') { statusType = 'staff-only'; statusMessage = 'Contains staff only content'; -} else if (publishState === 'has_unpublished_content') { +} else if (visibilityState === 'needs_attention') { if (category === 'vertical') { statusType = 'warning'; if (releasedToStudents) { @@ -49,7 +38,7 @@ if (statusType === 'warning') { } %> <% if (parentInfo) { %> -
      • diff --git a/cms/templates/js/publish-xblock.underscore b/cms/templates/js/publish-xblock.underscore index 76d5238c6871..09a5a516530f 100644 --- a/cms/templates/js/publish-xblock.underscore +++ b/cms/templates/js/publish-xblock.underscore @@ -1,45 +1,32 @@ <% -var publishClass = ''; -if (publishState === 'staff_only') { - publishClass = 'is-staff-only'; -} else if (publishState === 'live') { - publishClass = 'is-live'; -} else if (publishState === 'ready') { - publishClass = 'is-ready'; -} else if (publishState === 'has_unpublished_content') { - publishClass = 'has-warnings is-draft'; -} - var title = gettext("Draft (Never published)"); -if (publishState === 'staff_only') { +if (visibilityState === 'staff_only') { title = gettext("Unpublished (Staff only)"); -} else if (publishState === 'live') { +} else if (visibilityState === 'live') { title = gettext("Published and Live"); -} else if (publishState === 'ready') { +} else if (visibilityState === 'ready') { title = gettext("Published"); -} else if (publishState === 'has_unpublished_content') { +} else if (visibilityState === 'needs_attention') { title = gettext("Draft (Unpublished changes)"); } var releaseLabel = gettext("Release:"); -if (publishState === 'live') { +if (visibilityState === 'live') { releaseLabel = gettext("Released:"); -} else if (publishState === 'ready') { +} else if (visibilityState === 'ready') { releaseLabel = gettext("Scheduled:"); } -var canPublish = publishState !== 'ready' && publishState !== 'live'; -var canDiscardChanges = publishState === 'has_unpublished_content'; -var visibleToStaffOnly = publishState === 'staff_only'; +var visibleToStaffOnly = visibilityState === 'staff_only'; %> -
        +

        <%= gettext("Publishing Status") %> <%= title %>

        - <% if (publishState === 'has_unpublished_content' && editedOn && editedBy) { + <% if (hasChanges && editedOn && editedBy) { var message = gettext("Draft saved on %(last_saved_date)s by %(edit_username)s") %> <%= interpolate(message, { last_saved_date: '' + editedOn + '', @@ -91,12 +78,12 @@ var visibleToStaffOnly = publishState === 'staff_only';

        • - <%= gettext("Publish") %>
        • - <%= gettext("Discard Changes") %>
        • diff --git a/cms/templates/js/unit-outline.underscore b/cms/templates/js/unit-outline.underscore index ae30d8d565f3..10eeb4de9f5c 100644 --- a/cms/templates/js/unit-outline.underscore +++ b/cms/templates/js/unit-outline.underscore @@ -1,16 +1,4 @@ <% -var publishState = xblockInfo.get('publish_state'); -var publishClass = ''; -if (publishState === 'staff_only') { - publishClass = 'is-staff-only'; -} else if (publishState === 'live') { - publishClass = 'is-live'; -} else if (publishState === 'ready') { - publishClass = 'is-ready'; -} else if (publishState === 'has_unpublished_content') { - publishClass = 'has_warnings'; -} - var listType = 'list-for-' + xblockType; if (xblockType === 'course') { listType = 'list-sections'; @@ -21,7 +9,7 @@ if (xblockType === 'course') { } %> <% if (parentInfo) { %> -
        • diff --git a/cms/templates/widgets/units.html b/cms/templates/widgets/units.html index 04777a21d352..b874034a06fe 100644 --- a/cms/templates/widgets/units.html +++ b/cms/templates/widgets/units.html @@ -1,5 +1,4 @@ <%! from django.utils.translation import ugettext as _ %> -<%! from contentstore.utils import compute_publish_state %> <%! from contentstore.views.helpers import xblock_studio_url %>