From 33c57c8fc47244abc4c23fbc51609cda62b13755 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 9 Jul 2014 20:14:30 -0400 Subject: [PATCH 01/32] Studio: adding static course outline reference template (w/ initial working HTML) --- cms/templates/ux/reference/outline.html | 255 ++++++++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 cms/templates/ux/reference/outline.html diff --git a/cms/templates/ux/reference/outline.html b/cms/templates/ux/reference/outline.html new file mode 100644 index 000000000000..3981630fc658 --- /dev/null +++ b/cms/templates/ux/reference/outline.html @@ -0,0 +1,255 @@ +<%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 +

+ + +
+
+ +
+
+ +
+ +
+ +
+ + + +
+ + + +
+
+ +
+ From d9352ba2d32712af8133df54a381b0068666c40e Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Thu, 10 Jul 2014 11:10:05 -0400 Subject: [PATCH 02/32] Studio: revising static course outline reference template * static markup for all section states in outline UI * static markup for all subsection states in outline UI * static markup for all unit states in outline UI --- cms/templates/ux/reference/outline.html | 895 ++++++++++++++---- .../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 | 43 + .../outline_section_header-expanded.html | 43 + .../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 + 20 files changed, 1015 insertions(+), 180 deletions(-) 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/templates/ux/reference/outline.html b/cms/templates/ux/reference/outline.html index 3981630fc658..d3f5ee5b7f17 100644 --- a/cms/templates/ux/reference/outline.html +++ b/cms/templates/ux/reference/outline.html @@ -20,18 +20,18 @@

Page Actions

@@ -44,212 +44,747 @@

Page Actions

-
+
+

Your Course's Outline

-
    +
      -
    1. - - - -
      - -
      -

      - - - Introduction - - - - -

      - -
      -
        -
      • - - - Delete - -
      • - - + +
        +
          + +
        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. - --> -
      -
      -
      - - -
      -
      - - Released: Dec 31, 2015 at 21:00 UTC -
      - -
      -
        - -
      -
      -
      -
      - - -
        - -
      1. - - - -
        -
        - -

        - - - - Demo Course Overview - - - - - -

        - -
        -
          -
        • - - - Delete - + + +
        • + <%include file="outline_unit_header.html" />
        • - -
        + +
      2. + <%include file="outline_unit_header.html" /> +
      3. + + +
      4. + <%include file="outline_unit_header.html" /> +
      5. +
      +
- + + + <%include file="outline_add-unit.html" /> + + + + + + <%include file="outline_add-subsection.html" /> + + + + + + + + + <%include file="outline_add-section.html" /> + + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
  1. + + <%include file="outline_section_header-expanded.html" /> + +
    + <%include file="outline_status_release-draft.html" /> +
    + + +
    +
      + +
    - + +
  2. + - -
      + +
    1. + <%include file="outline_subsection_header-collapsed.html" /> - +
      + <%include file="outline_status_release-draft.html" /> -
      -
      + <%include file="outline_status_grading.html" /> +
      + -

      - Introduction: Video and Sequences -

      -
      +
      +
      + +
    2. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + - - - -
- - - - - - - - - + + + + +
  • + + <%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. + +
      + +
      + +
    2. + +
    +
    +
  • + +
    - + - + - - - + + 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..0a03a520e22c --- /dev/null +++ b/cms/templates/ux/reference/outline_add-section.html @@ -0,0 +1,6 @@ + + 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..90d2db6dd5d3 --- /dev/null +++ b/cms/templates/ux/reference/outline_add-subsection.html @@ -0,0 +1,6 @@ + + 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..3a8325828672 --- /dev/null +++ b/cms/templates/ux/reference/outline_add-unit.html @@ -0,0 +1,6 @@ + + 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..610fdece8d7c --- /dev/null +++ b/cms/templates/ux/reference/outline_section_header-collapsed.html @@ -0,0 +1,43 @@ +
    +

    + + + 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..2a236379cb27 --- /dev/null +++ b/cms/templates/ux/reference/outline_section_header-expanded.html @@ -0,0 +1,43 @@ +
    +

    + + + 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..beb37b56d647 --- /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..e14d95832728 --- /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..bafa323cdf95 --- /dev/null +++ b/cms/templates/ux/reference/outline_status_release-draft.html @@ -0,0 +1,10 @@ +
    +

    + Release Status: + + + Draft that 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..3d38a2e8efe8 --- /dev/null +++ b/cms/templates/ux/reference/outline_subsection_header-collapsed.html @@ -0,0 +1,42 @@ +
    +

    + + + Subsection Name + + + + + + + + +

    + + +
    + 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..7dd0790a0165 --- /dev/null +++ b/cms/templates/ux/reference/outline_subsection_header-expanded.html @@ -0,0 +1,42 @@ +
    +

    + + + Subsection Name + + + + + + + + +

    + + +
    + 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 +

    + + +
    + From 770895bb267c015c458629d37967a914c486cc9e Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Sun, 13 Jul 2014 21:35:35 -0400 Subject: [PATCH 03/32] Studio: styling revamped course outline-related UI * adding in outline-related color values * styling for publishing states * light styling abstraction for specific outline and unit/container contexts * revising add item to outline button styles * adding a few bells and whistles (hover states and error pulsing) --- cms/static/sass/_developer.scss | 75 --- cms/static/sass/_variables.scss | 13 + cms/static/sass/assets/_anims.scss | 23 + cms/static/sass/elements/_layout.scss | 14 +- cms/static/sass/elements/_modules.scss | 268 +++++++++ cms/static/sass/elements/_typography.scss | 17 + cms/static/sass/views/_outline.scss | 555 +++++------------- cms/templates/ux/reference/outline.html | 4 +- .../outline_section_header-collapsed.html | 11 +- .../outline_section_header-expanded.html | 11 +- .../outline_status_release-draft.html | 2 +- .../outline_subsection_header-collapsed.html | 4 +- .../outline_subsection_header-expanded.html | 4 +- common/static/sass/_mixins.scss | 10 +- 14 files changed, 494 insertions(+), 517 deletions(-) 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..79384b6f4485 100644 --- a/cms/static/sass/_variables.scss +++ b/cms/static/sass/_variables.scss @@ -49,6 +49,7 @@ $gray-d3: shade($gray,60%); $gray-d4: shade($gray,80%); $blue: rgb(0, 159, 230); +$blue-alt: rgb(88, 152, 219); $blue-l1: tint($blue,20%); $blue-l2: tint($blue,40%); $blue-l3: tint($blue,60%); @@ -104,6 +105,7 @@ $red-u2: desaturate($red,30%); $red-u3: desaturate($red,45%); $green: rgb(37, 184, 90); +$green-alt: rgb(48, 183, 94); $green-l1: tint($green,20%); $green-l2: tint($green,40%); $green-l3: tint($green,60%); @@ -160,6 +162,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-released: $blue; +$color-published: $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..5c12ab7daeb0 100644 --- a/cms/static/sass/assets/_anims.scss +++ b/cms/static/sass/assets/_anims.scss @@ -247,3 +247,26 @@ %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/_layout.scss b/cms/static/sass/elements/_layout.scss index cbf615836bc4..51731b890ff9 100644 --- a/cms/static/sass/elements/_layout.scss +++ b/cms/static/sass/elements/_layout.scss @@ -77,10 +77,22 @@ 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; + } + + // CASE: view live button + &.button-view, &.view-button { + + } } } } diff --git a/cms/static/sass/elements/_modules.scss b/cms/static/sass/elements/_modules.scss index 725caf5e47dd..bbdf1eaf1123 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,263 @@ } } +// outline UI +// -------------------- + +// outline: utilities +$outline-indent-width: $baseline; + +// UI: containing elements on a course outline (currently sections/subsections) +%outline-container { + // STATE: is-collapsed + &.is-collapsed { + border-left: ($baseline/4) solid $color-draft; + } + + // CASE: is published by not released + &.is-published { + border-left-color: $color-published; + + + &.is-published.is-released { + border-left-color: $color-released; + } + } + + // CASE: has staff-only content + &.has-staff-only { + border-left-color: $color-staff-only; + } + + // CASE: has changes + &.is-published.is-released.has-changes, // needed for poor specificity + &.is-published.is-scheduled.has-changes, // needed for poor specificity + &.has-changes { + border-left-color: $color-warning; + } + + // CASE: has errors + &.is-published.is-released.has-errors, // needed for poor specificity + &.is-published.is-scheduled.has-errors, // needed for poor specificity + &.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 + &.has-staff-only { + + // aint the prettiest, but needed to make sure direct children only + > .section-status, + > .subsection-status, + > .unit-status { + + .icon { + color: $color-staff-only; + } + } + } + + // CASE: has changes + &.is-published.is-released.has-changes, // needed for poor specificity + &.is-published.is-scheduled.has-changes, // needed for poor specificity + &.has-changes { + + // aint the prettiest, but 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 + &.is-published.is-released.has-errors, // needed for poor specificity + &.is-published.is-scheduled.has-errors, // needed for poor specificity + &.has-errors { + + // aint the prettiest, but 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; + } + + > .section-status .status-message, + > .subsection-status .status-message, + > .unit-status .status-message { + + .icon { + @include animation(pulse $tmg-s3 ease-in-out infinite); + } + } + } +} + +%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-container; + 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-container; + margin-bottom: ($baseline/2); + border: 1px solid $gray-l4; + 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 { + border-left: ($baseline/4) solid $color-draft; + } + + // 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..a88eeaad1a7e 100644 --- a/cms/static/sass/views/_outline.scss +++ b/cms/static/sass/views/_outline.scss @@ -4,6 +4,7 @@ .view-outline { // page structure + // -------------------- .content-primary, .content-supplementary { @include box-sizing(border-box); @@ -37,7 +38,8 @@ } - // page header bits + // page header + // -------------------- .toggle-button-sections { @extend %t-copy-sub2; position: relative; @@ -55,8 +57,10 @@ } } + // adding outline elements + // -------------------- - // new section, subsection, unit + // forms .new-section-name, .new-subsection-name-input { @include font-size(16); @@ -84,6 +88,7 @@ color: $gray-l1; } + // buttons .new-subsection-item, .new-unit-item { @extend %ui-btn-flat-outline; @@ -106,491 +111,215 @@ // 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; - } + .action-add { + @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/4); + } } - } - } + .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; + // outline: items + .outline-item { - header { - @include clearfix(); - height: auto; - border-bottom: 0; + // CASE: expand/collapse-able + &.is-collapsible .expand-collapse { - .expand-collapse { - display: none; - } + // STATE: hover/active + &:hover, &:active { - .item-details { - width: auto; - - .section-name { - float: none; - width: 100%; - } + .item-title, .ui-toggle-expansion { + 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; - } - } - } + // item: title + .item-title { + // STATE: is-editable + &.is-editable { - // 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; - - strong { - font-weight: bold; - } - } - - &.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 { + @include clearfix(); } - .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); + margin-top: -($baseline/4); + 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/2); 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; } } - - // 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; - } - } - } - - // gradable dropdown state - &.is-active { - - .menu { - @extend %ui-depth3; - display: block; - opacity: 1.0; - } - - .menu-toggle { - @extend %ui-depth4; - } - } - - // set state - &.is-set { - - .menu-toggle { - color: $blue; - } - - .status-label { - display: block; - color: $blue; - } + // status + .section-status { + margin: 0 0 0 $outline-indent-width; } } - .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); - - &.add-new-unit { - margin: 5px ($baseline*1.75) 0 ($baseline*1.75); - } + .outline-subsection { - .section-item { - border: 0; - background-color: $white; - } - - .public-item { - color: $black; - } - - .private-item { - color: $gray-l1; + // header + .subsection-header { + @include clearfix(); } - .draft-item { - color: $yellow-d1; - } + .subsection-header-details { + float: left; + width: flex-grid(6, 9); - .draft-item:after, - .public-item:after, - .private-item:after { - @include font-size(9); - margin-left: 3px; - font-weight: 600; - text-transform: uppercase; - } + .icon, .wrapper-subsection-title { + display: inline-block; + vertical-align: top; + } - .draft-item:after { - content: "- draft"; - } + .icon { + margin-right: ($baseline/4); + } - .private-item:after { - content: "- private"; + .wrapper-subsection-title { + width: flex-grid(5, 6); + margin-top: -($baseline/10); + line-height: 0; + } } - } - + .subsection-header-actions { + float: right; + width: flex-grid(3, 9); + margin-top: -($baseline/4); + text-align: right; - // 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 */ + .actions-list { + @extend %actions-list; + @extend %t-action2; + margin-right: ($baseline/2); + } } - .modal-window { - -webkit-transform: translate(-50%, -50%); - transform: translate(-50%, -50%); - position: absolute; - top: 50%; - left: 50%; - opacity: 0; + // status + .subsection-status { + margin: 0 0 0 $outline-indent-width; } } - // modal-window showing/hiding - &.modal-window-is-shown { - overflow: hidden; - - .wrapper-modal-window.is-shown { - visibility: visible; - pointer-events: auto; - display: block; - - .modal-window { - opacity: 1.0; - } - } + // outline: units + // -------------------- + .list-units { + margin: $baseline 0 0 0; } - .edit-section-publish-settings { + .outline-unit { + margin-left: $outline-indent-width; - .picker { + // header + .unit-header { @include clearfix(); + // margin-bottom: ($baseline/4); + } + .unit-header-details { + float: left; + width: flex-grid(6, 9); + margin-top: ($baseline/4); + } - .field { - float: left; - margin: 0 ($baseline/2) ($baseline/2); - - label, - input { - display: block; - text-align: left; - } - - 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 +342,8 @@ } } - // UI: DnD - specific elems/cases - subsection - .courseware-subsection { + // UI: drag and drop: subsection + .outline-subsection { .draggable-drop-indicator-before { top: 0; @@ -638,8 +367,8 @@ } } - // UI: DnD - specific elems/cases - unit - .courseware-unit { + // // UI: drag and drop: unit + .outline-unit { .draggable-drop-indicator-before { top: 0; @@ -658,7 +387,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 index d3f5ee5b7f17..93ec582d1edf 100644 --- a/cms/templates/ux/reference/outline.html +++ b/cms/templates/ux/reference/outline.html @@ -20,7 +20,7 @@

    Page Actions

    diff --git a/cms/templates/ux/reference/outline_section_header-collapsed.html b/cms/templates/ux/reference/outline_section_header-collapsed.html index 610fdece8d7c..0b1dae162971 100644 --- a/cms/templates/ux/reference/outline_section_header-collapsed.html +++ b/cms/templates/ux/reference/outline_section_header-collapsed.html @@ -1,15 +1,8 @@

    - - Section Title - - - - - - - + + Section Title

    diff --git a/cms/templates/ux/reference/outline_section_header-expanded.html b/cms/templates/ux/reference/outline_section_header-expanded.html index 2a236379cb27..c841e0501768 100644 --- a/cms/templates/ux/reference/outline_section_header-expanded.html +++ b/cms/templates/ux/reference/outline_section_header-expanded.html @@ -1,15 +1,8 @@

    - - Section Title - - - - - - - + + Section Title

    diff --git a/cms/templates/ux/reference/outline_status_release-draft.html b/cms/templates/ux/reference/outline_status_release-draft.html index bafa323cdf95..1cd2f47b68eb 100644 --- a/cms/templates/ux/reference/outline_status_release-draft.html +++ b/cms/templates/ux/reference/outline_status_release-draft.html @@ -3,7 +3,7 @@ Release Status: - Draft that is: + This item is Unscheduled

    diff --git a/cms/templates/ux/reference/outline_subsection_header-collapsed.html b/cms/templates/ux/reference/outline_subsection_header-collapsed.html index 3d38a2e8efe8..aa83af728ea8 100644 --- a/cms/templates/ux/reference/outline_subsection_header-collapsed.html +++ b/cms/templates/ux/reference/outline_subsection_header-collapsed.html @@ -1,8 +1,8 @@

    - - Subsection Name + + Subsection Title diff --git a/cms/templates/ux/reference/outline_subsection_header-expanded.html b/cms/templates/ux/reference/outline_subsection_header-expanded.html index 7dd0790a0165..00d17567ac3c 100644 --- a/cms/templates/ux/reference/outline_subsection_header-expanded.html +++ b/cms/templates/ux/reference/outline_subsection_header-expanded.html @@ -1,8 +1,8 @@

    - - Subsection Name + + Subsection Title 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 bf9076531051fd54765cd9d9a4fc9600e41374cf Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Thu, 17 Jul 2014 17:57:24 -0400 Subject: [PATCH 04/32] Studio: outline UI styling revisions * sync up stateful class names * show status border/visual for subsections in collapsed and expanded states * hide new-centric actions and messages on collapsed items * show/hide content elements for outline containers based on collapsed states * button-new semantic changes and icon-padding visual tweaks * replaced draft-like icon to something not used for actions * refactored styling and markup for collapse/expand --- cms/static/sass/_variables.scss | 6 +- cms/static/sass/assets/_anims.scss | 1 + cms/static/sass/elements/_controls.scss | 37 ++++- cms/static/sass/elements/_layout.scss | 8 +- cms/static/sass/elements/_modules.scss | 94 ++++++++----- cms/static/sass/views/_outline.scss | 118 ++++++++++++---- cms/templates/ux/reference/outline.html | 130 ++++++++---------- .../ux/reference/outline_add-section.html | 2 +- .../ux/reference/outline_add-subsection.html | 2 +- .../ux/reference/outline_add-unit.html | 2 +- .../outline_section_header-collapsed.html | 4 +- .../outline_section_header-expanded.html | 4 +- ...ne_status_message-unpublished_changes.html | 2 +- ...line_status_message-unpublished_units.html | 2 +- .../outline_status_release-draft.html | 2 +- .../outline_subsection_header-collapsed.html | 4 +- .../outline_subsection_header-expanded.html | 4 +- 17 files changed, 259 insertions(+), 163 deletions(-) diff --git a/cms/static/sass/_variables.scss b/cms/static/sass/_variables.scss index 79384b6f4485..84cd91407b9b 100644 --- a/cms/static/sass/_variables.scss +++ b/cms/static/sass/_variables.scss @@ -49,7 +49,6 @@ $gray-d3: shade($gray,60%); $gray-d4: shade($gray,80%); $blue: rgb(0, 159, 230); -$blue-alt: rgb(88, 152, 219); $blue-l1: tint($blue,20%); $blue-l2: tint($blue,40%); $blue-l3: tint($blue,60%); @@ -105,7 +104,6 @@ $red-u2: desaturate($red,30%); $red-u3: desaturate($red,45%); $green: rgb(37, 184, 90); -$green-alt: rgb(48, 183, 94); $green-l1: tint($green,20%); $green-l2: tint($green,40%); $green-l3: tint($green,60%); @@ -164,8 +162,8 @@ $shadow-d2: rgba($black, 0.6); // colors - application $color-draft: $gray-l3; -$color-released: $blue; -$color-published: $green; +$color-live: $blue; +$color-ready: $green; $color-warning: $orange-l2; $color-error: $red-l2; $color-staff-only: $black; diff --git a/cms/static/sass/assets/_anims.scss b/cms/static/sass/assets/_anims.scss index 5c12ab7daeb0..cf3f49e59bd2 100644 --- a/cms/static/sass/assets/_anims.scss +++ b/cms/static/sass/assets/_anims.scss @@ -248,6 +248,7 @@ @include animation(flashDouble $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 51731b890ff9..764af355521d 100644 --- a/cms/static/sass/elements/_layout.scss +++ b/cms/static/sass/elements/_layout.scss @@ -78,7 +78,8 @@ } // CASE: new/create button - &.new-button, &.button-new { + &.new-button, + &.button-new { @extend %btn-primary-green; @extend %sizing; } @@ -88,11 +89,6 @@ @extend %btn-secondary-gray; @extend %sizing; } - - // CASE: view live button - &.button-view, &.view-button { - - } } } } diff --git a/cms/static/sass/elements/_modules.scss b/cms/static/sass/elements/_modules.scss index bbdf1eaf1123..4e97a5f665a2 100644 --- a/cms/static/sass/elements/_modules.scss +++ b/cms/static/sass/elements/_modules.scss @@ -267,38 +267,68 @@ // outline: utilities $outline-indent-width: $baseline; -// UI: containing elements on a course outline (currently sections/subsections) -%outline-container { +// 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: ($baseline/4) solid $color-draft; - } + 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: is published by not released - &.is-published { - border-left-color: $color-published; + // 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; + } - &.is-published.is-released { - border-left-color: $color-released; + // 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 - &.has-staff-only { + &.is-staff-only { border-left-color: $color-staff-only; } - // CASE: has changes - &.is-published.is-released.has-changes, // needed for poor specificity - &.is-published.is-scheduled.has-changes, // needed for poor specificity - &.has-changes { + // CASE: has unpublished content + &.has-warnings { border-left-color: $color-warning; } // CASE: has errors - &.is-published.is-released.has-errors, // needed for poor specificity - &.is-published.is-scheduled.has-errors, // needed for poor specificity &.has-errors { border-left-color: $color-error; } @@ -317,9 +347,9 @@ $outline-indent-width: $baseline; } // CASE: has staff-only content - &.has-staff-only { + &.is-staff-only { - // aint the prettiest, but needed to make sure direct children only + // needed to make sure direct children only > .section-status, > .subsection-status, > .unit-status { @@ -330,12 +360,10 @@ $outline-indent-width: $baseline; } } - // CASE: has changes - &.is-published.is-released.has-changes, // needed for poor specificity - &.is-published.is-scheduled.has-changes, // needed for poor specificity - &.has-changes { + // CASE: has unpublished content + &.has-warnings { - // aint the prettiest, but needed to make sure direct children only + // needed to make sure direct children only > .section-status .status-message, > .subsection-status .status-message, > .unit-status .status-message { @@ -347,11 +375,9 @@ $outline-indent-width: $baseline; } // CASE: has errors - &.is-published.is-released.has-errors, // needed for poor specificity - &.is-published.is-scheduled.has-errors, // needed for poor specificity &.has-errors { - // aint the prettiest, but needed to make sure direct children only + // needed to make sure direct children only > .section-status .status-message, > .subsection-status .status-message, > .unit-status .status-message, @@ -360,15 +386,6 @@ $outline-indent-width: $baseline; > .unit-status .status-message-copy { color: $color-error; } - - > .section-status .status-message, - > .subsection-status .status-message, - > .unit-status .status-message { - - .icon { - @include animation(pulse $tmg-s3 ease-in-out infinite); - } - } } } @@ -387,7 +404,7 @@ $outline-indent-width: $baseline; .outline-section { @extend %ui-window; @extend %outline-item; - @extend %outline-container; + @extend %outline-section; margin-bottom: $baseline; padding: ($baseline*0.75) $baseline; @@ -455,9 +472,10 @@ $outline-indent-width: $baseline; // outline: subsections .outline-subsection { @extend %outline-item; - @extend %outline-container; + @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); @@ -469,7 +487,7 @@ $outline-indent-width: $baseline; // STATE: is-collapsed &.is-collapsed { - border-left: ($baseline/4) solid $color-draft; + } // header - title diff --git a/cms/static/sass/views/_outline.scss b/cms/static/sass/views/_outline.scss index a88eeaad1a7e..e3f5ba202dc7 100644 --- a/cms/static/sass/views/_outline.scss +++ b/cms/static/sass/views/_outline.scss @@ -1,6 +1,21 @@ // 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 @@ -40,20 +55,30 @@ // page header // -------------------- - .toggle-button-sections { - @extend %t-copy-sub2; - position: relative; - display: none; - float: right; - margin-top: ($baseline/4); - color: $gray-l1; + .button-toggle-expand-collapse { + + // STATE: action will collapse all + &.collapse-all { + + .expand-all { + display: none; + } - &.is-shown { - display: block; + .collapse-all { + display: block; + } } - .label { - display: inline-block; + // STATE: action will expand all + &.expand-all { + + .collapse-all { + display: none; + } + + .expand-all { + display: block; + } } } @@ -124,7 +149,7 @@ .add-item { margin-top: ($baseline*0.75); - .action-add { + .button-new { @extend %ui-btn-flat-outline; padding: ($baseline/2) $baseline; display: block; @@ -132,7 +157,7 @@ .icon { display: inline-block; vertical-align: middle; - margin-right: ($baseline/4); + margin-right: ($baseline/2); } } } @@ -154,12 +179,13 @@ .outline-item { // CASE: expand/collapse-able - &.is-collapsible .expand-collapse { + &.is-collapsible { - // STATE: hover/active - &:hover, &:active { + // only select the current item's toggle expansion controls + &:nth-child(1) .ui-toggle-expansion, &:nth-child(1) .item-title { - .item-title, .ui-toggle-expansion { + // STATE: hover/active + &:hover, &:active { color: $blue; } } @@ -189,7 +215,7 @@ // header .section-header { - @include clearfix(); + @extend %outline-item-header; } .section-header-details { @@ -207,7 +233,6 @@ .wrapper-section-title { width: flex-grid(5, 6); - margin-top: -($baseline/4); line-height: 0; } } @@ -215,7 +240,7 @@ .section-header-actions { float: right; width: flex-grid(3, 9); - margin-top: -($baseline/2); + margin-top: -($baseline/4); text-align: right; .actions-list { @@ -226,7 +251,30 @@ // status .section-status { - margin: 0 0 0 $outline-indent-width; + margin: 0 0 0 ($outline-indent-width*1.25); + } + + // content + .section-content { + @extend %outline-item-content-shown; + } + + // CASE: is-collapsible + &.is-collapsible { + @extend %ui-expand-collapse; + + .ui-toggle-expansion { + @extend %t-icon3; + color: $gray-l3; + } + } + + // STATE: is-collapsed + &.is-collapsed { + + .section-content { + @extend %outline-item-content-hidden; + } } } @@ -240,7 +288,7 @@ // header .subsection-header { - @include clearfix(); + @extend %outline-item-header; } .subsection-header-details { @@ -280,6 +328,29 @@ .subsection-status { margin: 0 0 0 $outline-indent-width; } + + // content + .subsection-content { + @extend %outline-item-content-shown; + } + + // CASE: is-collapsible + &.is-collapsible { + @extend %ui-expand-collapse; + + .ui-toggle-expansion { + @extend %t-icon4; + color: $gray-l3; + } + } + + // STATE: is-collapsed + &.is-collapsed { + + .subsection-content { + @extend %outline-item-content-hidden; + } + } } // outline: units @@ -293,8 +364,7 @@ // header .unit-header { - @include clearfix(); - // margin-bottom: ($baseline/4); + @extend %outline-item-header; } .unit-header-details { diff --git a/cms/templates/ux/reference/outline.html b/cms/templates/ux/reference/outline.html index 93ec582d1edf..41857bda0f53 100644 --- a/cms/templates/ux/reference/outline.html +++ b/cms/templates/ux/reference/outline.html @@ -22,16 +22,16 @@

    Page Actions

    @@ -82,9 +82,10 @@

    Your Course's Outline

    - <%include file="outline_add-unit.html" /> + + @@ -123,10 +124,11 @@

    Your Course's Outline

    + + <%include file="outline_add-unit.html" /> +

    - - <%include file="outline_add-unit.html" /> @@ -166,40 +168,8 @@

    Your Course's Outline

    - - - - - - - - - - -
    - + - -
    - + - - - - + - - - + -
  • +
  • <%include file="outline_unit_header.html" />
  • @@ -719,8 +689,8 @@

    Your Course's Outline

    - -
  • + +
  • <%include file="outline_unit_header.html" />
    @@ -728,10 +698,10 @@

    Your Course's Outline

  • - + - -
  • + +
  • <%include file="outline_unit_header.html" />
    @@ -739,7 +709,7 @@

    Your Course's Outline

  • - +
  • @@ -753,15 +723,23 @@

    Your Course's Outline

    + + <%include file="outline_add-unit.html" /> +
  • + + <%include file="outline_add-subsection.html" /> + + + <%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 index 0a03a520e22c..d45181db58f2 100644 --- a/cms/templates/ux/reference/outline_add-section.html +++ b/cms/templates/ux/reference/outline_add-section.html @@ -1,5 +1,5 @@ diff --git a/cms/templates/ux/reference/outline_add-subsection.html b/cms/templates/ux/reference/outline_add-subsection.html index 90d2db6dd5d3..551cf5ff3108 100644 --- a/cms/templates/ux/reference/outline_add-subsection.html +++ b/cms/templates/ux/reference/outline_add-subsection.html @@ -1,5 +1,5 @@ diff --git a/cms/templates/ux/reference/outline_add-unit.html b/cms/templates/ux/reference/outline_add-unit.html index 3a8325828672..0541071e0d05 100644 --- a/cms/templates/ux/reference/outline_add-unit.html +++ b/cms/templates/ux/reference/outline_add-unit.html @@ -1,5 +1,5 @@ diff --git a/cms/templates/ux/reference/outline_section_header-collapsed.html b/cms/templates/ux/reference/outline_section_header-collapsed.html index 0b1dae162971..83956f5b029d 100644 --- a/cms/templates/ux/reference/outline_section_header-collapsed.html +++ b/cms/templates/ux/reference/outline_section_header-collapsed.html @@ -1,6 +1,6 @@
    -

    - +

    + Section Title diff --git a/cms/templates/ux/reference/outline_section_header-expanded.html b/cms/templates/ux/reference/outline_section_header-expanded.html index c841e0501768..83956f5b029d 100644 --- a/cms/templates/ux/reference/outline_section_header-expanded.html +++ b/cms/templates/ux/reference/outline_section_header-expanded.html @@ -1,6 +1,6 @@
    -

    - +

    + Section Title diff --git a/cms/templates/ux/reference/outline_status_message-unpublished_changes.html b/cms/templates/ux/reference/outline_status_message-unpublished_changes.html index beb37b56d647..ac6a752bc0f0 100644 --- a/cms/templates/ux/reference/outline_status_message-unpublished_changes.html +++ b/cms/templates/ux/reference/outline_status_message-unpublished_changes.html @@ -1,4 +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 index e14d95832728..cab479da3e3b 100644 --- a/cms/templates/ux/reference/outline_status_message-unpublished_units.html +++ b/cms/templates/ux/reference/outline_status_message-unpublished_units.html @@ -1,4 +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 index 1cd2f47b68eb..02304355abae 100644 --- a/cms/templates/ux/reference/outline_status_release-draft.html +++ b/cms/templates/ux/reference/outline_status_release-draft.html @@ -2,7 +2,7 @@

    Release Status: - + This item is Unscheduled diff --git a/cms/templates/ux/reference/outline_subsection_header-collapsed.html b/cms/templates/ux/reference/outline_subsection_header-collapsed.html index aa83af728ea8..c51e69520d2c 100644 --- a/cms/templates/ux/reference/outline_subsection_header-collapsed.html +++ b/cms/templates/ux/reference/outline_subsection_header-collapsed.html @@ -1,6 +1,6 @@

    -

    - +

    + Subsection Title diff --git a/cms/templates/ux/reference/outline_subsection_header-expanded.html b/cms/templates/ux/reference/outline_subsection_header-expanded.html index 00d17567ac3c..c51e69520d2c 100644 --- a/cms/templates/ux/reference/outline_subsection_header-expanded.html +++ b/cms/templates/ux/reference/outline_subsection_header-expanded.html @@ -1,6 +1,6 @@
    -

    - +

    + Subsection Title From 1c23483cea058f3b0c72397fcd60f60648a9d26f Mon Sep 17 00:00:00 2001 From: Andy Armstrong Date: Wed, 16 Jul 2014 07:09:44 -0400 Subject: [PATCH 05/32] Integrate visual styling into the course outline STUD-1849 --- .../contentstore/tests/test_contentstore.py | 2 +- cms/djangoapps/contentstore/tests/utils.py | 28 +-- .../contentstore/views/component.py | 4 +- cms/djangoapps/contentstore/views/course.py | 4 +- cms/djangoapps/contentstore/views/item.py | 121 +++++++++++- .../views/tests/test_course_index.py | 7 +- .../contentstore/views/tests/test_item.py | 182 +++++++++++++++--- cms/static/js/models/xblock_info.js | 20 +- .../views/pages/container_subviews_spec.js | 110 ++++++----- .../spec/views/pages/course_outline_spec.js | 127 ++++++------ 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 | 25 ++- 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 | 172 +++++++++++------ .../mock/mock-course-outline-page.underscore | 6 +- cms/templates/js/publish-xblock.underscore | 62 +++--- cms/templates/js/unit-outline.underscore | 61 ++++-- cms/templates/js/xblock-outline.underscore | 2 +- .../xmodule/xmodule/modulestore/__init__.py | 10 +- .../xmodule/modulestore/mongo/draft.py | 16 +- .../modulestore/split_mongo/split_draft.py | 19 +- 31 files changed, 750 insertions(+), 365 deletions(-) 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/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..64b8540aff0e 100644 --- a/cms/djangoapps/contentstore/views/item.py +++ b/cms/djangoapps/contentstore/views/item.py @@ -600,10 +600,8 @@ 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,12 +620,21 @@ 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 + + release_date = get_default_time_display(xblock.start) if xblock.start != DEFAULT_START_DATE else None + 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), "published_on": get_default_time_display(xblock.published_date) if xblock.published_date else None, @@ -636,8 +643,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 +652,107 @@ 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 _is_visible_to_staff_only(xblock, child_info): + """ + Returns true if the specified xblock and all of its children are shown to staff only. + """ + if xblock.visible_to_staff_only: + return True + elif child_info and len(child_info['children']) > 0: + return all(info['publish_state'] == PublishState.staff_only for info in child_info['children']) + return False + + +def _is_unscheduled(xblock, child_info): + """ + Returns true if the specified xblock and all of its children are unscheduled. + """ + if xblock.start != DEFAULT_START_DATE: + return False + elif child_info: + return all( + info['publish_state'] in [PublishState.unscheduled, PublishState.staff_only] + for info in child_info['children'] + ) + return True + + +def _has_unpublished_content(xblock, child_info): + """ + Returns true if the xblock or its children have unpublished content (that is not staff only) + """ + if is_unit(xblock): + return modulestore().has_changes(xblock.location) + elif child_info: + return any(info['publish_state'] == PublishState.has_unpublished_content for info in child_info['children']) + else: + return False + + 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..1850743d7a3d 100644 --- a/cms/static/js/models/xblock_info.js +++ b/cms/static/js/models/xblock_info.js @@ -25,20 +25,14 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu */ "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. + * 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 */ - "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, + "publish_state": null, /** * Date of the last edit to this xblock or any of its descendants. */ 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..78d4b01b6212 100644 --- a/cms/static/js/spec/views/pages/container_subviews_spec.js +++ b/cms/static/js/spec/views/pages/container_subviews_spec.js @@ -23,7 +23,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin id: 'locator-container', display_name: 'Test Container', category: 'vertical', - published: false, + publish_state: 'unscheduled', has_changes: false, edited_on: "Jul 02, 2014 at 14:20 UTC", edited_by: "joe", published_on: "Jul 01, 2014 at 12:45 UTC", published_by: "amako", @@ -79,31 +79,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 +110,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 +125,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 +145,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 +197,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); + expect(containerPage.$(bitPublishingCss)).toHaveClass(readyClass); // Verify that the "published" value has been cleared out of the model. expect(containerPage.model.get("publish")).toBeNull(); }); 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 +219,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); + expect(containerPage.$(bitPublishingCss)).not.toHaveClass(readyClass); // Verify that the "published" value has been cleared out of the model. - expect(containerPage.model.get("publish")).toBeNull(); + expect(containerPage.model.get("publish_state")).toBe('unscheduled'); }); it('can discard changes', function () { @@ -287,7 +286,10 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin 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 +297,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 +308,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 +319,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 +364,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: 'staff_only' })); }; @@ -364,11 +372,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); } }; @@ -390,7 +398,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin requestStaffOnly(true); requestStaffOnly(false); verifyStaffOnly(false); - expect(containerPage.$(bitPublishingCss)).not.toHaveClass(publishedBit); + expect(containerPage.$(bitPublishingCss)).not.toHaveClass(readyClass); }); it("can remove staff only setting from published unit", function() { @@ -399,7 +407,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin requestStaffOnly(true); requestStaffOnly(false); verifyStaffOnly(false); - expect(containerPage.$(bitPublishingCss)).toHaveClass(publishedBit); + expect(containerPage.$(bitPublishingCss)).toHaveClass(readyClass); }); it("does not refresh if removing staff only is canceled", function() { 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..65401e05e32e 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) { @@ -129,9 +139,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 +152,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 +174,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 +194,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 +202,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 +214,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 +232,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 +248,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 +256,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 +265,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 +278,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 +338,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 +348,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 +367,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 +396,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 +416,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 +427,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..58e2342533b1 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,21 @@ 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'), 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 +135,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(); } @@ -223,7 +220,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")}

    diff --git a/cms/templates/js/course-outline.underscore b/cms/templates/js/course-outline.underscore index a9d52cfddbfa..9680bacfe1a5 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 (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) { %> -
    +

    <%= 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..5aad908dca91 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,7 +33,7 @@
      -
      +

      You haven't added any content to this course yet. diff --git a/cms/templates/js/publish-xblock.underscore b/cms/templates/js/publish-xblock.underscore index f9b30bcb38c1..b956dcd2d4a5 100644 --- a/cms/templates/js/publish-xblock.underscore +++ b/cms/templates/js/publish-xblock.underscore @@ -1,31 +1,43 @@ <% -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'; } -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' || 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_changes' && editedOn && editedBy) { var message = gettext("Draft saved on %(last_saved_date)s by %(edit_username)s") %> <%= interpolate(message, { last_saved_date: '' + editedOn + '', @@ -42,17 +54,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 +89,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..6772f3cf32f1 100644 --- a/cms/templates/js/xblock-outline.underscore +++ b/cms/templates/js/xblock-outline.underscore @@ -1,5 +1,5 @@ <% if (parentInfo) { %> -
      • diff --git a/common/lib/xmodule/xmodule/modulestore/__init__.py b/common/lib/xmodule/xmodule/modulestore/__init__.py index b2bc7b0eaf2c..c8f1892fc692 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' @@ -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/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): """ From a64cbfe24760184bf7b9eead93f81c0adbf433c9 Mon Sep 17 00:00:00 2001 From: Andy Armstrong Date: Thu, 24 Jul 2014 16:31:46 -0400 Subject: [PATCH 06/32] Fix some broken selectors in tests --- .../contentstore/features/common.py | 4 +- .../contentstore/features/course-outline.py | 6 +-- cms/djangoapps/contentstore/views/item.py | 37 ------------------- .../test/acceptance/pages/studio/overview.py | 8 ++-- 4 files changed, 9 insertions(+), 46 deletions(-) diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 76c83b9e01b2..815fd2705ed2 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -204,7 +204,7 @@ 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')) + 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 .add-button' ] 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/views/item.py b/cms/djangoapps/contentstore/views/item.py index 64b8540aff0e..8e8b7049da69 100644 --- a/cms/djangoapps/contentstore/views/item.py +++ b/cms/djangoapps/contentstore/views/item.py @@ -716,43 +716,6 @@ def _compute_publish_state(xblock, child_info): return PublishState.ready -def _is_visible_to_staff_only(xblock, child_info): - """ - Returns true if the specified xblock and all of its children are shown to staff only. - """ - if xblock.visible_to_staff_only: - return True - elif child_info and len(child_info['children']) > 0: - return all(info['publish_state'] == PublishState.staff_only for info in child_info['children']) - return False - - -def _is_unscheduled(xblock, child_info): - """ - Returns true if the specified xblock and all of its children are unscheduled. - """ - if xblock.start != DEFAULT_START_DATE: - return False - elif child_info: - return all( - info['publish_state'] in [PublishState.unscheduled, PublishState.staff_only] - for info in child_info['children'] - ) - return True - - -def _has_unpublished_content(xblock, child_info): - """ - Returns true if the xblock or its children have unpublished content (that is not staff only) - """ - if is_unit(xblock): - return modulestore().has_changes(xblock.location) - elif child_info: - return any(info['publish_state'] == PublishState.has_unpublished_content for info in child_info['children']) - else: - return False - - 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/common/test/acceptance/pages/studio/overview.py b/common/test/acceptance/pages/studio/overview.py index 12b3cf5207cd..91eb581b963f 100644 --- a/common/test/acceptance/pages/studio/overview.py +++ b/common/test/acceptance/pages/studio/overview.py @@ -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,7 @@ class CourseOutlineSubsection(CourseOutlineChild, CourseOutlineContainer): """ url = None - BODY_SELECTOR = '.outline-item-subsection' + BODY_SELECTOR = '.outline-subsection' CHILD_CLASS = CourseOutlineUnit def unit(self, title): @@ -221,7 +221,7 @@ 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' CHILD_CLASS = CourseOutlineSubsection def subsection(self, title): From 6b984f6da584e07ba35fa86b27ebb9298cd87de5 Mon Sep 17 00:00:00 2001 From: Ben McMorran Date: Thu, 24 Jul 2014 17:34:24 -0400 Subject: [PATCH 07/32] Fix some bokchoy tests --- common/test/acceptance/pages/studio/overview.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/common/test/acceptance/pages/studio/overview.py b/common/test/acceptance/pages/studio/overview.py index 91eb581b963f..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() @@ -189,6 +189,8 @@ class CourseOutlineSubsection(CourseOutlineChild, CourseOutlineContainer): url = None BODY_SELECTOR = '.outline-subsection' + NAME_SELECTOR = '.subsection-title' + NAME_FIELD_WRAPPER_SELECTOR = '.subsection-header .wrapper-xblock-field' CHILD_CLASS = CourseOutlineUnit def unit(self, title): @@ -222,6 +224,8 @@ class CourseOutlineSection(CourseOutlineChild, CourseOutlineContainer): """ url = None 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 ec8d140c8675cd93da69cece3176b799bf33f450 Mon Sep 17 00:00:00 2001 From: Andy Armstrong Date: Thu, 24 Jul 2014 18:10:49 -0400 Subject: [PATCH 08/32] Fix all the unit tests (I hope) --- cms/djangoapps/contentstore/views/item.py | 5 +- cms/static/js/models/xblock_info.js | 22 +++++---- .../views/pages/container_subviews_spec.js | 48 +++++++------------ .../spec/views/pages/course_outline_spec.js | 6 +-- .../js/views/pages/container_subviews.js | 3 +- cms/templates/js/course-outline.underscore | 2 +- cms/templates/js/publish-history.underscore | 30 ++++++------ cms/templates/js/publish-xblock.underscore | 8 ++-- cms/templates/js/xblock-outline.underscore | 2 +- .../tests/test_mixed_modulestore.py | 14 +++--- 10 files changed, 67 insertions(+), 73 deletions(-) diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py index 8e8b7049da69..6fd953b97e58 100644 --- a/cms/djangoapps/contentstore/views/item.py +++ b/cms/djangoapps/contentstore/views/item.py @@ -601,8 +601,6 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F not a particular xblock should have its children included. """ - # Treat DEFAULT_START_DATE as a magic number that means the release date has not been set - def safe_get_username(user_id): """ Guard against bad user_ids, like the infamous "**replace_user**". @@ -628,13 +626,16 @@ def safe_get_username(user_id): 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, + "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), "published_on": get_default_time_display(xblock.published_date) if xblock.published_date else None, diff --git a/cms/static/js/models/xblock_info.js b/cms/static/js/models/xblock_info.js index 1850743d7a3d..02b07a4533b6 100644 --- a/cms/static/js/models/xblock_info.js +++ b/cms/static/js/models/xblock_info.js @@ -24,15 +24,6 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu * An optional object with information about each of the ancestors. */ "ancestor_info": 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, /** * Date of the last edit to this xblock or any of its descendants. */ @@ -41,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. */ @@ -49,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 78d4b01b6212..e247e3078f93 100644 --- a/cms/static/js/spec/views/pages/container_subviews_spec.js +++ b/cms/static/js/spec/views/pages/container_subviews_spec.js @@ -24,10 +24,8 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin display_name: 'Test Container', category: 'vertical', publish_state: 'unscheduled', - has_changes: false, 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 }; @@ -201,8 +199,8 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin // Verify updates displayed expect(containerPage.$(bitPublishingCss)).toHaveClass(readyClass); - // Verify that the "published" value has been cleared out of the model. - expect(containerPage.model.get("publish")).toBeNull(); + // 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 () { @@ -220,7 +218,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin // Verify still in draft state. expect(containerPage.$(bitPublishingCss)).not.toHaveClass(readyClass); - // Verify that the "published" value has been cleared out of the model. + // Verify that the "publish_state" value has been updated expect(containerPage.model.get("publish_state")).toBe('unscheduled'); }); @@ -262,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. @@ -279,7 +277,7 @@ 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"); }); @@ -364,7 +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({ - publish_state: 'staff_only' + publish_state: isStaffOnly ? 'staff_only' : 'unscheduled' })); }; @@ -394,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(readyClass); }); - 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(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 @@ -437,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 65401e05e32e..15ab173e9ff0 100644 --- a/cms/static/js/spec/views/pages/course_outline_spec.js +++ b/cms/static/js/spec/views/pages/course_outline_spec.js @@ -118,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', []); diff --git a/cms/static/js/views/pages/container_subviews.js b/cms/static/js/views/pages/container_subviews.js index 58e2342533b1..37bf7a2513d7 100644 --- a/cms/static/js/views/pages/container_subviews.js +++ b/cms/static/js/views/pages/container_subviews.js @@ -109,6 +109,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ 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'), @@ -161,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); diff --git a/cms/templates/js/course-outline.underscore b/cms/templates/js/course-outline.underscore index 9680bacfe1a5..d116d9593ce5 100644 --- a/cms/templates/js/course-outline.underscore +++ b/cms/templates/js/course-outline.underscore @@ -49,7 +49,7 @@ if (statusType === 'warning') { } %> <% if (parentInfo) { %> -
      • 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 b956dcd2d4a5..21fc7a1fe7f8 100644 --- a/cms/templates/js/publish-xblock.underscore +++ b/cms/templates/js/publish-xblock.underscore @@ -7,13 +7,15 @@ if (publishState === 'staff_only') { } else if (publishState === 'ready') { publishClass = 'is-ready is-published'; } else if (publishState === 'has_unpublished_content') { - publishClass = 'has-warnings'; + publishClass = 'has-warnings is-draft'; } var title = gettext("Draft (Never published)"); if (publishState === 'staff_only') { title = gettext("Unpublished (Staff only)"); -} else if (publishState === 'live' || publishState === 'ready') { +} 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)"); @@ -37,7 +39,7 @@ var visibleToStaffOnly = publishState === 'staff_only';

        - <% if (publishState === 'has_unpublished_changes' && 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 + '', diff --git a/cms/templates/js/xblock-outline.underscore b/cms/templates/js/xblock-outline.underscore index 6772f3cf32f1..5cfe9b4a62c4 100644 --- a/cms/templates/js/xblock-outline.underscore +++ b/cms/templates/js/xblock-outline.underscore @@ -1,5 +1,5 @@ <% if (parentInfo) { %> -

      • 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): From 30e6f98b26a7eb2d7af712d208481286cad74b8e Mon Sep 17 00:00:00 2001 From: Andy Armstrong Date: Thu, 24 Jul 2014 18:32:20 -0400 Subject: [PATCH 09/32] A few more tweaks --- .../contentstore/features/common.py | 4 +- .../contentstore/features/courses.py | 2 +- cms/djangoapps/contentstore/utils.py | 6 +-- cms/templates/js/course-outline.underscore | 52 +++++++++---------- .../mock/mock-course-outline-page.underscore | 2 +- cms/templates/js/xblock-outline.underscore | 4 +- .../xmodule/xmodule/modulestore/__init__.py | 6 +-- .../lib/xmodule/xmodule/modulestore/mixed.py | 6 +-- 8 files changed, 41 insertions(+), 41 deletions(-) diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 815fd2705ed2..9aaa4f651605 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -203,7 +203,7 @@ def create_a_course(): def add_section(): - world.css_click('.course-outline .add-button') + world.css_click('.outline .button-new') assert_true(world.is_css_present('.outline-section .xblock-field-value')) @@ -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-subsection .expand-collapse', '.outline-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/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/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/templates/js/course-outline.underscore b/cms/templates/js/course-outline.underscore index d116d9593ce5..05fd8eb86fd5 100644 --- a/cms/templates/js/course-outline.underscore +++ b/cms/templates/js/course-outline.underscore @@ -82,36 +82,36 @@ if (statusType === 'warning') {
      -
      - <% 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) { %> + <% 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) { %> diff --git a/cms/templates/js/mock/mock-course-outline-page.underscore b/cms/templates/js/mock/mock-course-outline-page.underscore index 5aad908dca91..712ded240b2a 100644 --- a/cms/templates/js/mock/mock-course-outline-page.underscore +++ b/cms/templates/js/mock/mock-course-outline-page.underscore @@ -36,7 +36,7 @@

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

      diff --git a/cms/templates/js/xblock-outline.underscore b/cms/templates/js/xblock-outline.underscore index 5cfe9b4a62c4..044309408854 100644 --- a/cms/templates/js/xblock-outline.underscore +++ b/cms/templates/js/xblock-outline.underscore @@ -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 c8f1892fc692..8e5b13f31237 100644 --- a/common/lib/xmodule/xmodule/modulestore/__init__.py +++ b/common/lib/xmodule/xmodule/modulestore/__init__.py @@ -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 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) From aa7f558505e1e15dd3345a50a167b1fa6001d945 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Thu, 24 Jul 2014 18:51:13 -0400 Subject: [PATCH 10/32] Studio: syncing up unit publishing state UI with stateful names/styles --- cms/static/sass/views/_container.scss | 11 +++++++++-- cms/templates/js/publish-xblock.underscore | 4 ++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/cms/static/sass/views/_container.scss b/cms/static/sass/views/_container.scss index 53079dd99572..7ce26da7a171 100644 --- a/cms/static/sass/views/_container.scss +++ b/cms/static/sass/views/_container.scss @@ -99,16 +99,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/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'; } From 0980c08cb16284012dfdb3c58277d6f9a589a7e3 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Thu, 24 Jul 2014 18:56:04 -0400 Subject: [PATCH 11/32] Studio: revising outline item status message display logic to show release status --- cms/templates/js/course-outline.underscore | 57 +++++++++++----------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/cms/templates/js/course-outline.underscore b/cms/templates/js/course-outline.underscore index 05fd8eb86fd5..b394512ff585 100644 --- a/cms/templates/js/course-outline.underscore +++ b/cms/templates/js/course-outline.underscore @@ -82,36 +82,37 @@ if (statusType === 'warning') {
    - <% 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) { %> From 288ca2e92337551a107b63660b7aec4b8d34e050 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Thu, 24 Jul 2014 19:03:19 -0400 Subject: [PATCH 12/32] fixing publishState value typo in outline UI template --- cms/templates/js/course-outline.underscore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/templates/js/course-outline.underscore b/cms/templates/js/course-outline.underscore index b394512ff585..379029d71a33 100644 --- a/cms/templates/js/course-outline.underscore +++ b/cms/templates/js/course-outline.underscore @@ -25,7 +25,7 @@ if (xblockType === 'course') { var statusMessage = null; var statusType = null; -if (publishState === 'is_staff_only') { +if (publishState === 'staff_only') { statusType = 'staff-only'; statusMessage = 'Contains staff only content'; } else if (publishState === 'has_unpublished_content') { From 3a01a925f11f7c06a67a0ca3469e352ca25304ff Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Thu, 24 Jul 2014 23:50:41 -0400 Subject: [PATCH 13/32] Studio: refining and syncing incontext editor styling --- cms/static/sass/elements/_forms.scss | 3 ++- cms/static/sass/views/_container.scss | 10 +++++++++- cms/static/sass/views/_outline.scss | 23 ++++++++++++++++++++--- 3 files changed, 31 insertions(+), 5 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/views/_container.scss b/cms/static/sass/views/_container.scss index 7ce26da7a171..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; + } } } } 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 { From 2c9966cf202d69e1f55451dfd81669471031dea1 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Thu, 24 Jul 2014 23:59:14 -0400 Subject: [PATCH 14/32] Studio: maintaining visual alignment of collapsed/expanded sections in outline UI --- cms/static/sass/elements/_modules.scss | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) 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 { From c596494c1d495a494bf2eefdb1f9d18bf2de3bd6 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Fri, 25 Jul 2014 00:20:44 -0400 Subject: [PATCH 15/32] Studio: simplifying page-level action styles on outline UI --- cms/static/sass/elements/_layout.scss | 6 ------ cms/templates/course_outline.html | 10 +++++----- cms/templates/ux/reference/outline.html | 10 +++++----- 3 files changed, 10 insertions(+), 16 deletions(-) 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/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")}

    diff --git a/cms/templates/js/course-outline.underscore b/cms/templates/js/course-outline.underscore index 379029d71a33..77d5f729f3cb 100644 --- a/cms/templates/js/course-outline.underscore +++ b/cms/templates/js/course-outline.underscore @@ -3,17 +3,6 @@ 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'; @@ -28,7 +17,7 @@ var statusType = null; if (publishState === 'staff_only') { statusType = 'staff-only'; statusMessage = 'Contains staff only content'; -} else if (publishState === 'has_unpublished_content') { +} else if (publishState === '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..05316eb01320 100644 --- a/cms/templates/js/publish-xblock.underscore +++ b/cms/templates/js/publish-xblock.underscore @@ -1,15 +1,4 @@ <% -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') { title = gettext("Unpublished (Staff only)"); @@ -17,7 +6,7 @@ if (publishState === 'staff_only') { title = gettext("Published and Live"); } else if (publishState === 'ready') { title = gettext("Published"); -} else if (publishState === 'has_unpublished_content') { +} else if (publishState === 'needs_attention') { title = gettext("Draft (Unpublished changes)"); } @@ -28,18 +17,16 @@ if (publishState === 'live') { 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 (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..e9b0aee7c516 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 %>