Commit 9f8f64cf by Mathew Peterson Committed by Ben McMorran

Course Reruns UI

Studio: adding course re-run-centric static template rendering

* initial HTML for dashboard states
* initial HTML for new course re-run view/form
* initial HTML placeholder for outline alert UI

Conflicts:
	cms/templates/index.html

Studio: adding styling for course re-run-centric views

* adding new view/page mast-wizard type
* refactoring create course/element form styling
* adding course re-run view specific styling
* adding courses processing styling (w/ alerts and status)

Course rerun server-side updates: support display_name and DuplicateCourseError.

Studio: further design revisions and tweaks from feedback

* removing new window attribute from re-run control
* removing links from processing courses
* revising look/feel of dismiss action on dashboard + alert
* correcting font-weight of dashboard processing title
* adding extra space to course rerun action on dashboard
* re-wording secondary cancel action on rerun view

Conflicts:
	cms/templates/index.html

Added interation on unsucceeded courses in dashboard

Studio: removing 'rel=external' property from course re-run actions

Studio: removing hover styles for processing courses

Fixed value bug in split and set course listing to display run

moved task.py for rerun
parent a986b46c
......@@ -5,6 +5,7 @@ This file contains celery tasks for contentstore views
from celery.task import task
from django.contrib.auth.models import User
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import DuplicateCourseError, ItemNotFoundError
from course_action_state.models import CourseRerunState
from contentstore.utils import initialize_permissions
......@@ -32,13 +33,11 @@ def rerun_course(source_course_key_string, destination_course_key_string, user_i
# update state: Succeeded
CourseRerunState.objects.succeeded(course_key=destination_course_key)
return "succeeded"
except DuplicateCourseError as exc:
# do NOT delete the original course, only update the status
CourseRerunState.objects.failed(course_key=destination_course_key, exception=exc)
return "duplicate course"
# catch all exceptions so we can update the state and properly cleanup the course.
......
......@@ -1580,9 +1580,12 @@ class RerunCourseTest(ContentStoreTestCase):
json_resp = parse_json(response)
self.assertNotIn('ErrMsg', json_resp)
destination_course_key = CourseKey.from_string(json_resp['destination_course_key'])
return destination_course_key
def create_course_listing_html(self, course_key):
"""Creates html fragment that is created for the given course_key in the course listing section"""
return '<a class="course-link" href="/course/{}"'.format(course_key)
def create_unsucceeded_course_action_html(self, course_key):
"""Creates html fragment that is created for the given course_key in the unsucceeded course action section"""
# TODO Update this once the Rerun UI LMS-11011 is implemented.
......@@ -1615,6 +1618,7 @@ class RerunCourseTest(ContentStoreTestCase):
rerun_state = CourseRerunState.objects.find_first(course_key=destination_course_key)
expected_states = {
'state': CourseRerunUIStateManager.State.SUCCEEDED,
'display_name': self.destination_course_data['display_name'],
'source_course_key': source_course.id,
'course_key': destination_course_key,
'should_display': True,
......
......@@ -329,7 +329,9 @@ class TestCourseListing(ModuleStoreTestCase):
# simulate initiation of course actions
for course in courses_in_progress:
CourseRerunState.objects.initiated(sourse_course_key, destination_course_key=course.id, user=self.user)
CourseRerunState.objects.initiated(
sourse_course_key, destination_course_key=course.id, user=self.user, display_name="test course"
)
# verify return values
for method in (_accessible_courses_list_from_groups, _accessible_courses_list):
......
......@@ -5,11 +5,9 @@ import json
import random
import string # pylint: disable=W0402
import logging
from django.utils.translation import ugettext as _
import django.utils
from django.contrib.auth.decorators import login_required
from django_future.csrf import ensure_csrf_cookie
from django.conf import settings
from django.views.decorators.http import require_http_methods
from django.core.exceptions import PermissionDenied
......@@ -25,11 +23,14 @@ from xmodule.modulestore.django import modulestore
from xmodule.contentstore.content import StaticContent
from xmodule.tabs import PDFTextbookTabs
from xmodule.partitions.partitions import UserPartition, Group
from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateCourseError
from opaque_keys import InvalidKeyError
from opaque_keys.edx.locations import Location
from opaque_keys.edx.keys import CourseKey
from django_future.csrf import ensure_csrf_cookie
from util.json_request import JsonResponse
from edxmako.shortcuts import render_to_response
from contentstore.course_info_model import get_course_updates, update_course_updates, delete_course_update
from contentstore.utils import (
add_instructor,
......@@ -47,7 +48,6 @@ from models.settings.course_grading import CourseGradingModel
from models.settings.course_metadata import CourseMetadata
from util.json_request import expect_json
from util.string_utils import _has_non_ascii_characters
from .access import has_course_access
from .component import (
OPEN_ENDED_COMPONENT_TYPES,
......@@ -56,10 +56,8 @@ from .component import (
SPLIT_TEST_COMPONENT_TYPE,
ADVANCED_COMPONENT_TYPES,
)
from .tasks import rerun_course
from contentstore.tasks import rerun_course
from .item import create_xblock_info
from opaque_keys.edx.keys import CourseKey
from course_creators.views import get_course_creator_status, add_user_with_status_unrequested
from contentstore import utils
from student.roles import (
......@@ -68,11 +66,11 @@ from student.roles import (
from student import auth
from course_action_state.models import CourseRerunState, CourseRerunUIStateManager
from course_action_state.managers import CourseActionStateItemNotFoundError
from microsite_configuration import microsite
__all__ = ['course_info_handler', 'course_handler', 'course_info_update_handler',
'course_rerun_handler',
'settings_handler',
'grading_handler',
'advanced_settings_handler',
......@@ -233,6 +231,25 @@ def course_handler(request, course_key_string=None):
else:
return HttpResponseNotFound()
@login_required
@ensure_csrf_cookie
@require_http_methods(["GET"])
def course_rerun_handler(request, course_key_string):
"""
The restful handler for course reruns.
GET
html: return html page with form to rerun a course for the given course id
"""
course_key = CourseKey.from_string(course_key_string)
course_module = _get_course_module(course_key, request.user, depth=3)
if request.method == 'GET':
return render_to_response('course-create-rerun.html', {
'source_course_key': course_key,
'display_name': course_module.display_name,
'user': request.user,
'course_creator_status': _get_course_creator_status(request.user),
'allow_unicode_course_id': settings.FEATURES.get('ALLOW_UNICODE_COURSE_ID', False)
})
def _course_outline_json(request, course_module):
"""
......@@ -340,9 +357,26 @@ def course_listing(request):
course.display_name,
reverse_course_url('course_handler', course.id),
get_lms_link_for_item(course.location),
_get_rerun_link_for_item(course.id),
course.display_org_with_default,
course.display_number_with_default,
course.location.name
course.location.run
)
def format_unsucceeded_course_for_view(uca):
"""
return tuple of the data which the view requires for each unsucceeded course
"""
return (
uca.display_name,
uca.course_key.org,
uca.course_key.course,
uca.course_key.run,
True if uca.state == CourseRerunUIStateManager.State.FAILED else False,
True if uca.state == CourseRerunUIStateManager.State.IN_PROGRESS else False,
reverse_course_url('course_notifications_handler', uca.course_key, kwargs={
'action_state_id': uca.id,
}) if uca.state == CourseRerunUIStateManager.State.FAILED else ''
)
# remove any courses in courses that are also in the unsucceeded_course_actions list
......@@ -353,6 +387,8 @@ def course_listing(request):
if not isinstance(c, ErrorDescriptor) and (c.id not in unsucceeded_action_course_keys)
]
unsucceeded_course_actions = [format_unsucceeded_course_for_view(uca) for uca in unsucceeded_course_actions]
return render_to_response('index.html', {
'courses': courses,
'unsucceeded_course_actions': unsucceeded_course_actions,
......@@ -363,6 +399,10 @@ def course_listing(request):
})
def _get_rerun_link_for_item(course_key):
return '/course_rerun/{}/{}/{}'.format(course_key.org, course_key.course, course_key.run)
@login_required
@ensure_csrf_cookie
def course_index(request, course_key):
......@@ -398,6 +438,9 @@ def course_index(request, course_key):
'rerun_notification_id': current_action.id if current_action else None,
'course_release_date': course_release_date,
'settings_url': settings_url,
'notification_dismiss_url': reverse_course_url('course_notifications_handler', current_action.course_key, kwargs={
'action_state_id': current_action.id,
}) if current_action else None,
})
......@@ -554,7 +597,7 @@ def _rerun_course(request, org, number, run, fields):
add_instructor(destination_course_key, request.user, request.user)
# Mark the action as initiated
CourseRerunState.objects.initiated(source_course_key, destination_course_key, request.user)
CourseRerunState.objects.initiated(source_course_key, destination_course_key, request.user, fields['display_name'])
# Rerun the course as a new celery task
rerun_course.delay(unicode(source_course_key), unicode(destination_course_key), request.user.id, fields)
......
......@@ -38,8 +38,15 @@ FEATURES['ALLOW_ALL_ADVANCED_COMPONENTS'] = True
################################# CELERY ######################################
# By default don't use a worker, execute tasks as if they were local functions
# TODO BEWARE: UNCOMMENT THIS BEFORE MERGING INTO MASTER
CELERY_ALWAYS_EAGER = True
# TODO BEWARE: DO NOT COMMIT THE REST OF THIS SECTION INTO MASTER - FOR LOCAL TESTING ONLY
# Test with Celery threads
# CELERY_ALWAYS_EAGER = False
# BROKER_URL = 'redis://'
################################ DEBUG TOOLBAR ################################
INSTALLED_APPS += ('debug_toolbar', 'debug_toolbar_mongo')
MIDDLEWARE_CLASSES += ('debug_toolbar.middleware.DebugToolbarMiddleware',)
......
require(["domReady", "jquery", "underscore", "js/utils/cancel_on_escape"],
function (domReady, $, _, CancelOnEscape) {
var dismissNotification = function (e) {
e.preventDefault();
$.ajax({
url: $('.dismiss-button').data('dismiss-link'),
type: 'DELETE',
success: function(result) {
window.location.reload()
}
});
};
var saveNewCourse = function (e) {
e.preventDefault();
......@@ -164,5 +175,6 @@ require(["domReady", "jquery", "underscore", "js/utils/cancel_on_escape"],
domReady(function () {
$('.new-course-button').bind('click', addNewCourse);
$('.dismiss-button').bind('click', dismissNotification);
});
});
require(["domReady", "jquery", "underscore", "js/utils/cancel_on_escape"],
function (domReady, $, _, CancelOnEscape) {
var saveRerunCourse = function (e) {
e.preventDefault();
// One final check for empty values
var errors = _.reduce(
['.rerun-course-name', '.rerun-course-org', '.rerun-course-number', '.rerun-course-run'],
function (acc, ele) {
var $ele = $(ele);
var error = validateRequiredField($ele.val());
setNewCourseFieldInErr($ele.parent('li'), error);
return error ? true : acc;
},
false
);
if (errors) {
return;
}
var $newCourseForm = $(this).closest('#rerun-course-form');
var display_name = $newCourseForm.find('.rerun-course-name').val();
var org = $newCourseForm.find('.rerun-course-org').val();
var number = $newCourseForm.find('.rerun-course-number').val();
var run = $newCourseForm.find('.rerun-course-run').val();
analytics.track('Reran a Course', {
'source_course_key': source_course_key,
'org': org,
'number': number,
'display_name': display_name,
'run': run
});
$.postJSON('/course/', {
'source_course_key': source_course_key,
'org': org,
'number': number,
'display_name': display_name,
'run': run
},
function (data) {
if (data.url !== undefined) {
window.location = data.url
} else if (data.ErrMsg !== undefined) {
$('.wrapper-error').addClass('is-shown').removeClass('is-hidden');
$('#course_rerun_error').html('<p>' + data.ErrMsg + '</p>');
$('.rerun-course-save').addClass('is-disabled').removeClass('is-processing').html(gettext('Create Re-run'));
$('.action-cancel').removeClass('is-hidden');
}
}
);
// Go into creating re-run state
$('.rerun-course-save').addClass('is-disabled').addClass('is-processing').html(
'<i class="icon-refresh icon-spin"></i>' + gettext('Processing Re-run Request')
);
$('.action-cancel').addClass('is-hidden');
};
var cancelRerunCourse = function (e) {
e.preventDefault();
// Clear out existing fields and errors
$('.rerun-course-run').val('');
$('#course_rerun_error').html('');
$('wrapper-error').removeClass('is-shown').addClass('is-hidden');
$('.rerun-course-save').off('click');
window.location.href = '/course/'
};
var validateRequiredField = function (msg) {
return msg.length === 0 ? gettext('Required field.') : '';
};
var setNewCourseFieldInErr = function (el, msg) {
if(msg) {
el.addClass('error');
el.children('span.tip-error').addClass('is-shown').removeClass('is-hidden').text(msg);
$('.rerun-course-save').addClass('is-disabled');
}
else {
el.removeClass('error');
el.children('span.tip-error').addClass('is-hidden').removeClass('is-shown');
// One "error" div is always present, but hidden or shown
if($('.error').length === 1) {
$('.rerun-course-save').removeClass('is-disabled');
}
}
};
domReady(function () {
var $cancelButton = $('.rerun-course-cancel');
var $courseRun = $('.rerun-course-run');
$courseRun.focus().select();
$('.rerun-course-save').on('click', saveRerunCourse);
$cancelButton.bind('click', cancelRerunCourse);
CancelOnEscape($cancelButton);
$('.cancel-button').bind('click', cancelRerunCourse);
// Check that a course (org, number, run) doesn't use any special characters
var validateCourseItemEncoding = function (item) {
var required = validateRequiredField(item);
if (required) {
return required;
}
if ($('.allow-unicode-course-id').val() === 'True'){
if (/\s/g.test(item)) {
return gettext('Please do not use any spaces in this field.');
}
}
else{
if (item !== encodeURIComponent(item)) {
return gettext('Please do not use any spaces or special characters in this field.');
}
}
return '';
};
// Ensure that org/course_num/run < 65 chars.
var validateTotalCourseItemsLength = function () {
var totalLength = _.reduce(
['.rerun-course-org', '.rerun-course-number', '.rerun-course-run'],
function (sum, ele) {
return sum + $(ele).val().length;
}, 0
);
if (totalLength > 65) {
$('.wrap-error').addClass('is-shown');
$('#course_creation_error').html('<p>' + gettext('The combined length of the organization, course number, and course run fields cannot be more than 65 characters.') + '</p>');
$('.rerun-course-save').addClass('is-disabled');
}
else {
$('.wrap-error').removeClass('is-shown');
}
};
// Handle validation asynchronously
_.each(
['.rerun-course-org', '.rerun-course-number', '.rerun-course-run'],
function (ele) {
var $ele = $(ele);
$ele.on('keyup', function (event) {
// Don't bother showing "required field" error when
// the user tabs into a new field; this is distracting
// and unnecessary
if (event.keyCode === 9) {
return;
}
var error = validateCourseItemEncoding($ele.val());
setNewCourseFieldInErr($ele.parent(), error);
validateTotalCourseItemsLength();
});
}
);
var $name = $('.rerun-course-name');
$name.on('keyup', function () {
var error = validateRequiredField($name.val());
setNewCourseFieldInErr($name.parent(), error);
validateTotalCourseItemsLength();
});
});
});
\ No newline at end of file
......@@ -5,6 +5,17 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
var modalSelector = '.edit-section-publish-settings';
var dismissNotification = function (e) {
e.preventDefault();
$.ajax({
url: $('.dismiss-button').data('dismiss-link'),
type: 'GET',
success: function(result) {
$('.wrapper-alert-announcement').remove()
}
});
};
var toggleSections = function(e) {
e.preventDefault();
......@@ -222,6 +233,8 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
$('.toggle-button-sections').bind('click', toggleSections);
$('.expand-collapse').bind('click', toggleSubmodules);
$('.dismiss-button').bind('click', dismissNotification);
var $body = $('body');
$body.on('click', '.section-published-date .edit-release-date', editSectionPublishDate);
$body.on('click', '.edit-section-publish-settings .action-save', saveSetSectionScheduleDate);
......
......@@ -240,6 +240,282 @@ p, ul, ol, dl {
}
}
// ====================
// layout - basic
.wrapper-view {
}
// ====================
// layout - basic page header
.wrapper-mast {
margin: ($baseline*1.5) 0 0 0;
padding: 0 $baseline;
position: relative;
.mast, .metadata {
@include clearfix();
position: relative;
max-width: $fg-max-width;
min-width: $fg-min-width;
width: flex-grid(12);
margin: 0 auto $baseline auto;
color: $gray-d2;
}
.mast {
border-bottom: 1px solid $gray-l4;
padding-bottom: ($baseline/2);
// layout with actions
.page-header {
width: flex-grid(12);
}
// layout with actions
&.has-actions {
@include clearfix();
.page-header {
float: left;
width: flex-grid(6,12);
margin-right: flex-gutter();
}
.nav-actions {
position: relative;
bottom: -($baseline*0.75);
float: right;
width: flex-grid(6,12);
text-align: right;
.nav-item {
display: inline-block;
vertical-align: top;
margin-right: ($baseline/2);
&:last-child {
margin-right: 0;
}
}
// buttons
.button {
padding: ($baseline/4) ($baseline/2) ($baseline/3) ($baseline/2);
}
.new-button {
}
.view-button {
}
}
}
// layout with actions
&.has-subtitle {
.nav-actions {
bottom: -($baseline*1.5);
}
}
// layout with navigation
&.has-navigation {
.nav-actions {
bottom: -($baseline*1.5);
}
.navigation-link {
@extend %cont-truncated;
display: inline-block;
vertical-align: bottom; // correct for extra padding in FF
max-width: 250px;
&.navigation-current {
@extend %ui-disabled;
color: $gray;
max-width: 250px;
&:before {
color: $gray;
}
}
}
.navigation-link:before {
content: " / ";
margin: ($baseline/4);
color: $gray;
&:hover {
color: $gray;
}
}
.navigation .navigation-link:first-child:before {
content: "";
margin: 0;
}
}
}
// CASE: wizard-based mast
.mast-wizard {
.page-header-sub {
@extend %t-title4;
color: $gray;
font-weight: 300;
}
.page-header-super {
@extend %t-title4;
float: left;
width: flex-grid(12,12);
margin-top: ($baseline/2);
border-top: 1px solid $gray-l4;
padding-top: ($baseline/2);
font-weight: 600;
}
}
// page metadata/action bar
.metadata {
}
}
// layout - basic page content
.wrapper-content {
margin: 0;
padding: 0 $baseline;
position: relative;
}
.content {
@include clearfix();
@extend %t-copy-base;
max-width: $fg-max-width;
min-width: $fg-min-width;
width: flex-grid(12);
margin: 0 auto;
color: $gray-d2;
header {
position: relative;
margin-bottom: $baseline;
border-bottom: 1px solid $gray-l4;
padding-bottom: ($baseline/2);
.title-sub {
@extend %t-copy-sub1;
display: block;
margin: 0;
color: $gray-l2;
}
.title-1 {
@extend %t-title3;
margin: 0;
padding: 0;
font-weight: 600;
color: $gray-d3;
}
}
}
.content-primary, .content-supplementary {
@include box-sizing(border-box);
}
// layout - primary content
.content-primary {
.title-1 {
@extend %t-title3;
}
.title-2 {
@extend %t-title4;
margin: 0 0 ($baseline/2) 0;
}
.title-3 {
@extend %t-title6;
margin: 0 0 ($baseline/2) 0;
}
header {
@include clearfix();
.title-2 {
width: flex-grid(5, 12);
margin: 0 flex-gutter() 0 0;
float: left;
}
.tip {
@extend %t-copy-sub2;
width: flex-grid(7, 12);
float: right;
margin-top: ($baseline/2);
text-align: right;
color: $gray-l2;
}
}
}
// layout - supplemental content
.content-supplementary {
> section {
margin: 0 0 $baseline 0;
}
}
// ====================
// layout - grandfathered
.main-wrapper {
position: relative;
margin: 0 ($baseline*2);
}
.inner-wrapper {
@include clearfix();
position: relative;
max-width: 1280px;
margin: auto;
> article {
clear: both;
}
}
.main-column {
clear: both;
float: left;
width: 70%;
}
.sidebar {
float: right;
width: 28%;
}
.left {
float: left;
}
.right {
float: right;
}
// ====================
......
......@@ -133,6 +133,27 @@
}
}
// white secondary button
%btn-secondary-white {
@extend %ui-btn-secondary;
border-color: $white-t2;
color: $white-t3;
&:hover, &:active {
border-color: $white;
color: $white;
}
&.current, &.active {
background: $gray-d2;
color: $gray-l5;
&:hover, &:active {
background: $gray-d2;
}
}
}
// green secondary button
%btn-secondary-green {
@extend %ui-btn-secondary;
......@@ -213,17 +234,6 @@
// ====================
// calls-to-action
// ====================
// specific buttons - view live
%view-live-button {
@extend %t-action4;
}
// ====================
// UI: element actions list
%actions-list {
......
......@@ -137,28 +137,10 @@ form {
}
}
// ELEM: form wrapper
.wrapper-create-element {
height: 0;
margin-bottom: $baseline;
opacity: 0.0;
pointer-events: none;
overflow: hidden;
&.animate {
@include transition(opacity $tmg-f1 ease-in-out 0s, height $tmg-f1 ease-in-out 0s);
}
&.is-shown {
height: auto; // define a specific height for the animating version of this UI to work properly
opacity: 1.0;
pointer-events: auto;
}
}
// ELEM: form
// form styling for creating a new content item (course, user, textbook)
form[class^="create-"] {
// TODO: refactor this into a placeholder to extend.
.form-create {
@extend %ui-window;
.title {
......@@ -253,12 +235,19 @@ form[class^="create-"] {
.tip {
@extend %t-copy-sub2;
@include transition(color, 0.15s, ease-in-out);
@include transition(color 0.15s ease-in-out);
display: block;
margin-top: ($baseline/4);
color: $gray-l3;
}
.tip-note {
display: block;
margin-top: ($baseline/4);
}
.tip-error {
display: none;
float: none;
......@@ -365,7 +354,6 @@ form[class^="create-"] {
}
}
// form - inline xblock name edit on unit, container, outline
// TOOD: abstract this out into a Sass placeholder
......@@ -402,6 +390,25 @@ form[class^="create-"] {
}
}
// ELEM: form wrapper
.wrapper-create-element {
height: 0;
margin-bottom: $baseline;
opacity: 0.0;
pointer-events: none;
overflow: hidden;
&.animate {
@include transition(opacity $tmg-f1 ease-in-out 0s, height $tmg-f1 ease-in-out 0s);
}
&.is-shown {
height: auto; // define a specific height for the animating version of this UI to work properly
opacity: 1.0;
pointer-events: auto;
}
}
// ====================
// forms - grandfathered
......
......@@ -527,7 +527,7 @@
&.wrapper-alert-warning {
box-shadow: 0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $orange;
[class^="icon"] {
.alert-symbol {
color: $orange;
}
}
......@@ -535,7 +535,7 @@
&.wrapper-alert-error {
box-shadow: 0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $red-l1;
[class^="icon"] {
.alert-symbol {
color: $red-l1;
}
}
......@@ -543,7 +543,7 @@
&.wrapper-alert-confirmation {
box-shadow: 0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $green;
[class^="icon"] {
.alert-symbol {
color: $green;
}
}
......@@ -551,7 +551,7 @@
&.wrapper-alert-announcement {
box-shadow: 0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $blue;
[class^="icon"] {
.alert-symbol {
color: $blue;
}
}
......@@ -559,7 +559,7 @@
&.wrapper-alert-step-required {
box-shadow: 0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $pink;
[class^="icon"] {
.alert-symbol {
color: $pink;
}
}
......@@ -579,11 +579,11 @@
@extend %t-strong;
}
[class^="icon"], .copy {
.alert-symbol, .copy {
float: left;
}
[class^="icon"] {
.alert-symbol {
@include transition (color 0.50s ease-in-out 0s);
@extend %t-icon3;
width: flex-grid(1, 12);
......@@ -605,7 +605,7 @@
// with actions
&.has-actions {
[class^="icon"] {
.alert-symbol {
width: flex-grid(1, 12);
}
......@@ -667,6 +667,28 @@
background: $gray-d1;
}
}
// with dismiss (to sunset action-alert-clos)
.action-dismiss {
.button {
@extend %btn-secondary-white;
}
.icon,.button-copy {
display: inline-block;
vertical-align: middle;
}
.icon {
@extend %t-icon4;
margin-right: ($baseline/4);
}
.button-copy {
@extend %t-copy-sub1;
}
}
}
// ====================
......
......@@ -38,6 +38,7 @@
@import 'views/dashboard';
@import 'views/export';
@import 'views/index';
@import 'views/course-create';
@import 'views/import';
@import 'views/outline';
@import 'views/settings';
......
// studio - views - course creation page
// ====================
.view-course-create {
// basic layout
// --------------------
.content-primary, .content-supplementary {
@include box-sizing(border-box);
float: left;
}
.content-primary {
width: flex-grid(9, 12);
margin-right: flex-gutter();
}
.content-supplementary {
width: flex-grid(3, 12);
}
//
// header/masthead
// --------------------
.mast .page-header-super {
.course-original-title-id, .course-original-title {
display: block;
}
.course-original-title-id {
@extend %t-title5;
}
}
// course re-run form
// --------------------
.rerun-course {
.row {
@include clearfix();
margin-bottom: ($baseline*0.75);
}
.column {
float: left;
width: 48%;
}
.column:first-child {
margin-right: 4%;
}
label {
@extend %t-title7;
display: block;
font-weight: 700;
}
.rerun-course-org,
.rerun-course-number,
.rerun-course-name,
.rerun-course-run {
width: 100%;
}
.rerun-course-name {
@extend %t-title5;
font-weight: 300;
}
.rerun-course-save {
@include blue-button;
}
.rerun-course-cancel {
@include white-button;
}
.item-details {
padding-bottom: 0;
}
.wrap-error {
@include transition(opacity $tmg-f2 ease 0s);
opacity: 0;
}
.wrap-error.is-shown {
opacity: 1;
}
.message-status {
display: block;
margin-bottom: 0;
padding: ($baseline*.5) ($baseline*1.5) 8px ($baseline*1.5);
font-weight: bold;
}
// NOTE: override for modern button styling until all buttons (in _forms.scss) can be converted
.actions {
.action-primary {
@include blue-button;
@extend %t-action2;
}
.action-secondary {
@include grey-button;
@extend %t-action2;
}
}
}
}
......@@ -294,120 +294,298 @@
// ELEM: course listings
.courses {
margin: $baseline 0;
}
.title {
@extend %t-title6;
margin-bottom: $baseline;
border-bottom: 1px solid $gray-l3;
padding-bottom: ($baseline/2);
color: $gray-l2;
}
}
.list-courses {
margin-top: $baseline;
border-radius: 3px;
border: 1px solid $gray;
border: 1px solid $gray-l2;
background: $white;
box-shadow: 0 1px 2px $shadow-l1;
box-shadow: 0 1px 1px $shadow-l1;
.course-item {
@include box-sizing(border-box);
width: flex-grid(9, 9);
position: relative;
border-bottom: 1px solid $gray-l1;
padding: $baseline;
// STATE: hover/focus
&:hover {
background: $paleYellow;
li:last-child {
margin-bottom: 0;
}
}
.course-actions .view-live-button {
opacity: 1.0;
pointer-events: auto;
}
.course-title {
color: $orange-d1;
}
// UI: course wrappers (needed for status messages)
.wrapper-course {
.course-metadata {
opacity: 1.0;
}
}
// CASE: has status
&.has-status {
.course-link, .course-actions {
.course-status {
@include box-sizing(border-box);
display: inline-block;
vertical-align: middle;
}
width: flex-grid(3, 9);
padding-right: ($baseline/2);
text-align: right;
// encompassing course link
.course-link {
@extend %ui-depth2;
width: flex-grid(7, 9);
margin-right: flex-gutter();
}
.value {
// course title
.course-title {
@extend %t-title4;
@extend %t-light;
margin: 0 ($baseline*2) ($baseline/4) 0;
.copy, *[class^="icon"] {
display: inline-block;
vertical-align: middle;
}
*[class^="icon"] {
@extend %t-icon4;
margin-right: ($baseline/2);
}
.copy {
@extend %t-copy-sub1;
}
}
}
// course metadata
.course-metadata {
.status-message {
@extend %t-copy-sub1;
@include transition(opacity $tmg-f1 ease-in-out 0);
color: $gray;
opacity: 0.75;
background-color: $gray-l5;
box-shadow: 0 2px 2px 0 $shadow inset;
padding: ($baseline*0.75) $baseline;
.metadata-item {
display: inline-block;
&.has-actions {
&:after {
content: "/";
margin-left: ($baseline/10);
margin-right: ($baseline/10);
color: $gray-l4;
.copy, .status-actions {
display: inline-block;
vertical-align: middle;
}
.copy {
width: 65%;
margin: 0 $baseline 0 0;
}
&:last-child {
.status-actions {
width: 30%;
text-align: right;
&:after {
content: "";
margin-left: 0;
margin-right: 0;
.button {
@extend %btn-secondary-white;
}
}
.label {
@extend %cont-text-sr;
.icon,.button-copy {
display: inline-block;
vertical-align: middle;
}
.icon {
@extend %t-icon4;
margin-right: ($baseline/4);
}
.button-copy {
@extend %t-copy-sub1;
}
}
}
}
}
}
// UI: individual course listings
.course-item {
@include box-sizing(border-box);
width: flex-grid(9, 9);
position: relative;
border-bottom: 1px solid $gray-l2;
padding: $baseline;
// STATE: hover/focus
&:hover {
background: $paleYellow;
.course-actions {
@extend %ui-depth3;
position: static;
width: flex-grid(2, 9);
text-align: right;
opacity: 1.0;
pointer-events: auto;
}
// view live button
.view-live-button {
@extend %ui-depth3;
@include transition(opacity $tmg-f2 ease-in-out 0);
@include box-sizing(border-box);
padding: ($baseline/2);
opacity: 0.0;
pointer-events: none;
.course-title {
color: $orange-d1;
}
&:hover {
opacity: 1.0;
pointer-events: auto;
.course-metadata {
opacity: 1.0;
}
}
.course-link, .course-actions {
@include box-sizing(border-box);
display: inline-block;
vertical-align: middle;
}
// encompassing course link
.course-link {
@extend %ui-depth2;
width: flex-grid(6, 9);
margin-right: flex-gutter();
}
// course title
.course-title {
@extend %t-title4;
margin: 0 ($baseline*2) ($baseline/4) 0;
font-weight: 300;
}
// course metadata
.course-metadata {
@extend %t-copy-sub1;
@include transition(opacity $tmg-f1 ease-in-out 0);
color: $gray;
opacity: 0.75;
.metadata-item {
display: inline-block;
&:after {
content: "/";
margin-left: ($baseline/10);
margin-right: ($baseline/10);
color: $gray-l4;
}
&:last-child {
&:after {
content: "";
margin-left: 0;
margin-right: 0;
}
}
.label {
@extend %cont-text-sr;
}
}
}
.course-actions {
@include transition(opacity $tmg-f2 ease-in-out 0);
@extend %ui-depth3;
position: static;
width: flex-grid(3, 9);
text-align: right;
opacity: 0;
pointer-events: none;
.action {
display: inline-block;
vertical-align: middle;
margin-right: ($baseline/2);
&:last-child {
border-bottom: none;
margin-right: 0;
}
}
.button {
@extend %t-action3;
}
// view live button
.view-button {
@include box-sizing(border-box);
padding: ($baseline/2);
}
// course re-run button
.action-rerun {
margin-right: $baseline;
}
.rerun-button {
font-weight: 600;
// TODO: sync up button styling and add secondary style here
}
}
// CASE: is processing
&.is-processing {
.course-status .value {
color: $gray-l2;
}
}
// CASE: has an error
&.has-error {
.course-status {
color: $red; // TODO: abstract this out to an error-based color variable
}
~ .status-message {
background: $red-l1; // TODO: abstract this out to an error-based color variable
color: $white;
}
}
// CASE: last course in listing
&:last-child {
border-bottom: none;
}
}
// ====================
// CASE: courses that are being processed
.courses-processing {
margin-bottom: ($baseline*2);
border-bottom: 1px solid $gray-l3;
padding-bottom: ($baseline*2);
// TODO: abstract this case out better with normal course listings
.list-courses {
border: none;
background: none;
box-shadow: none;
}
.wrapper-course {
@extend %ui-window;
position: relative;
}
.course-item {
border: none;
// STATE: hover/focus
&:hover {
background: inherit;
.course-title {
color: inherit;
}
}
}
// course details (replacement for course-link when a course cannot be linked)
.course-details {
@extend %ui-depth2;
display: inline-block;
vertical-align: middle;
width: flex-grid(6, 9);
margin-right: flex-gutter();
}
}
// ====================
// ELEM: new user form
.wrapper-create-course {
......@@ -494,6 +672,5 @@
margin-bottom: 0;
padding: ($baseline*.5) ($baseline*1.5) 8px ($baseline*1.5);
}
}
}
......@@ -343,7 +343,9 @@
<% online_help_token = self.online_help_token() if hasattr(self, 'online_help_token') else None %>
<%include file="widgets/header.html" args="online_help_token=online_help_token" />
<div id="page-alert"></div>
<div id="page-alert">
<%block name="page_alert"></%block>
</div>
<div id="content">
<%block name="content"></%block>
......
<!--
DESIGN/UI NOTES:
* changed tip-based UI text to have is-hidden/is-shown stateful classes rather than is-hiding/is-showing
* create-course and new-course prefixed classes have been changed to use rerun-courses
* changed form <input /> elements to <button> elements
- - -
TODO:
* sync up styling of stateful classes
* need to add support for allow_unicode_course_id in real view's template
-->
<%inherit file="base.html" />
<%! from django.utils.translation import ugettext as _ %>
<%! from django.core.urlresolvers import reverse %>
<%block name="title">${_("Create a Course Rerun of:")}</%block>
<%block name="bodyclass">is-signedin view-course-create view-course-create-rerun</%block>
<%block name="jsextra">
<script type="text/javascript">
require(["domReady!", "jquery", "jquery.form", "js/views/course_rerun"], function(doc, $) {
});
</script>
<script type="text/javascript">
var source_course_key = "${source_course_key}"
</script>
</%block>
<%block name="content">
<div id="content">
<div class="wrapper-mast wrapper">
<header class="mast mast-wizard has-actions">
<h1 class="page-header">
<span class="page-header-sub">${_("Create a re-run of a course")}</span>
</h1>
<nav class="nav-actions">
<h3 class="sr">Page Actions</h3>
<ul>
<li class="nav-item">
<a href="" class="button cancel-button">${_("Cancel")}</a>
</li>
</ul>
</nav>
<h2 class="page-header-super course-original">
<span class="sr">${_("You are creating a re-run from:")}</span>
<span class="course-original-title-id">${source_course_key.org} ${source_course_key.course} ${source_course_key.run}</span>
<span class="course-original-title">${display_name}</span>
</h2>
</header>
</div> <!-- /mast -->
<div class="wrapper-content wrapper">
<div class="inner-wrapper">
<section class="content">
<article class="content-primary">
<div class="introduction">
<div class="copy">
<p>
${_("Provide identifying information for this re-run of the course. The original course is not affected in any way by a re-run.")}
<strong>${_("Note: Together, the organization, course number, and course run must uniquely identify this new course instance.")}</strong>
<p>
</div>
</div><!-- /introduction -->
<!-- - - - -->
<div class="wrapper-rerun-course">
<form class="form-create rerun-course course-info" id="rerun-course-form" name="rerun-course-form">
<!-- NOTE: this element's contents should be only included when they are needed and not kept in the DOM for all states -->
<div class="wrapper-error is-hidden">
<div id="course_rerun_error" name="course_rerun_error" class="message message-status error" role="alert">
</div>
</div>
<div class="wrapper-form">
<fieldset>
<legend class="sr">${_("Required Information to Create a re-run of a course")}</legend>
<ol class="list-input">
<li class="field text required" id="field-course-name">
<label for="rerun-course-name">${_("Course Name")}</label>
<input class="rerun-course-name" id="rerun-course-name" type="text" name="rerun-course-name" aria-required="true" value="${display_name}" placeholder="${_('e.g. Introduction to Computer Science')}" />
<span class="tip">
${_("The public display name for the new course. (This name is often the same as the original course name.)")}
</span>
<span class="tip tip-error is-hidden"></span>
</li>
<li class="field text required" id="field-organization">
<label for="rerun-course-org">${_("Organization")}</label>
<input class="rerun-course-org" id="rerun-course-org" type="text" name="rerun-course-org" aria-required="true" value="${source_course_key.org}" placeholder="${_('e.g. UniversityX or OrganizationX')}" />
<span class="tip">
${_("The name of the organization sponsoring the new course. (This name is often the same as the original organization name.)")}
<strong class="tip-note" class="tip-note">${_("Note: No spaces or special characters are allowed.")}</strong>
</span>
<span class="tip tip-error is-hidden"></span>
</li>
<li class="row">
<div class="column field text required" id="field-course-number">
<label for="rerun-course-number">${_("Course Number")}</label>
<input class="rerun-course-number" id="rerun-course-number" type="text" name="rerun-course-number" aria-required="true" value="${source_course_key.course}" placeholder="${_('e.g. CS101')}" />
<span class="tip">
${_("The unique number that identifies the new course within the organization. (This number is often the same as the original course number.)")}
<strong class="tip-note" class="tip-note">${_("Note: No spaces or special characters are allowed.")}</strong>
</span>
<span class="tip tip-error is-hidden"></span>
</div>
<div class="column field text required" id="field-course-run">
<label for="rerun-course-run">${_("Course Run")}</label>
<input class="rerun-course-run" id="rerun-course-run" type="text" name="rerun-course-run" aria-required="true"placeholder="${_('e.g. 2014_T1')}" />
<span class="tip">
${_("The term in which the new course will run. (This value is often different than the original course run value.)")}
<strong class="tip-note" class="tip-note">${_("Note: No spaces or special characters are allowed.")}</strong>
</span>
<span class="tip tip-error is-hidden"></span>
</div>
</li>
</ol>
<input type="hidden" value="" class="allow-unicode-course-id" /> <!-- TODO: need to add support for allow_unicode_course_id in real view's template -->
</fieldset>
</div>
<div class="actions">
<button type="submit" class="action action-primary rerun-course-save is-disabled">${_('Create Re-run')}</button>
<button type="button" class="action action-secondary action-cancel rerun-course-cancel">${_('Cancel')}</button>
</div>
</form>
</div>
</article><!-- /content-primary -->
<aside class="content-supplementary" role="complimentary">
<div class="bit">
<h3 class="title-3">${_("When will my course re-run start?")}</h3>
<ul class="list-details">
<li class="item-detail">${_("Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Sed posuere consectetur est at lobortis. Maecenas faucibus mollis interdum.")}</li>
</ul>
</div>
<div class="bit">
<h3 class="title-3">${_("What transfers from the original course?")}</h3>
<ul class="list-details">
<li class="item-detail">${_("Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Sed posuere consectetur est at lobortis. Maecenas faucibus mollis interdum.")}</li>
</ul>
</div>
<div class="bit">
<h3 class="title-3">${_("What does not transfer from the original course?")}</h3>
<ul class="list-details">
<li class="item-detail">${_("Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Sed posuere consectetur est at lobortis. Maecenas faucibus mollis interdum.")}</li>
</ul>
</div>
</aside><!-- /content-supplementary -->
</section>
</div><!-- /content -->
</div>
</div>
</%block>
\ No newline at end of file
......@@ -36,6 +36,31 @@ from contentstore.utils import reverse_usage_url
% endfor
</%block>
<%block name="page_alert">
%if notification_dismiss_url is not None:
<div class="wrapper wrapper-alert wrapper-alert-announcement is-shown">
<div class="alert announcement has-actions">
<i class="alert-symbol icon-bullhorn"></i>
<div class="copy">
<h2 class="title title-3">This course was created as a re-run. Some manual configuration is needed.</h2>
<p>Be sure to review and reset all dates (the Course Start Date was set to January 1, 2030); set up the course team; review course updates and other assets for dated material; and seed the discussions and wiki.</p>
</div>
<ul class="nav-actions">
<li class="action action-dismiss">
<a href="#" class="button dismiss-button" data-dismiss-link='${notification_dismiss_url}'>
<i class="icon icon-remove-sign"></i>
<span class="button-copy">${_("Dimiss")}</span>
</a>
</li>
</ul>
</div>
</div>
%endif
</%block>
<%block name="content">
<div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle">
......
......@@ -77,7 +77,7 @@ require(["domReady!", "jquery", "jquery.form", "js/index"], function(doc, $) {
% if course_creator_status=='granted':
<div class="wrapper-create-element wrapper-create-course">
<form class="create-course course-info" id="create-course-form" name="create-course-form">
<form class="form-create create-course course-info" id="create-course-form" name="create-course-form">
<div class="wrap-error">
<div id="course_creation_error" name="course_creation_error" class="message message-status message-status error" role="alert">
<p>${_("Please correct the highlighted fields below.")}</p>
......@@ -131,10 +131,104 @@ require(["domReady!", "jquery", "jquery.form", "js/index"], function(doc, $) {
</div>
% endif
<!-- STATE: processing courses -->
%if len(unsucceeded_course_actions) > 0:
<div class="courses courses-processing">
<h3 class="title">Courses Being Processed</h3>
<ul class="list-courses">
%for display_name, org, num, run, state_failed, state_in_progress, dismiss_link in sorted(unsucceeded_course_actions, key=lambda s: s[0].lower() if s[0] is not None else ''):
<!-- STATE: re-run is processing -->
%if state_in_progress:
<li class="wrapper-course has-status">
<div class="course-item course-rerun is-processing">
<div class="course-details" href="#">
<h3 class="course-title">${display_name}</h3>
<div class="course-metadata">
<span class="course-org metadata-item">
<span class="label">${_("Organization:")}</span> <span class="value">${org}</span>
</span>
<span class="course-num metadata-item">
<span class="label">${_("Course Number:")}</span>
<span class="value">${num}</span>
</span>
<span class="course-run metadata-item">
<span class="label">${_("Course Run:")}</span> <span class="value">${run}</span>
</span>
</div>
</div>
<dl class="course-status">
<dt class="label sr">This re-run processing status:</dt>
<dd class="value">
<i class="icon-refresh icon-spin"></i>
<span class="copy">Configuring as re-run</span>
</dd>
</dl>
</div>
<div class="status-message">
<p class="copy">${_("The new course will be added to your course list in 5-10 minutes. Return to this page or refresh it to update the course list. The new course will need some manual configuration.")}</p>
</div>
</li>
%endif
<!-- - - - -->
<!-- STATE: re-run has error -->
%if state_failed:
<li class="wrapper-course has-status">
<div class="course-item course-rerun has-error">
<div class="course-details" href="#">
<h3 class="course-title">${display_name}</h3>
<div class="course-metadata">
<span class="course-org metadata-item">
<span class="label">${_("Organization:")}</span> <span class="value">${org}</span>
</span>
<span class="course-num metadata-item">
<span class="label">${_("Course Number:")}</span>
<span class="value">${num}</span>
</span>
<span class="course-run metadata-item">
<span class="label">${_("Course Run:")}</span> <span class="value">${run}</span>
</span>
</div>
</div>
<dl class="course-status">
<dt class="label sr">This re-run processing status:</dt>
<dd class="value">
<i class="icon-warning-sign"></i>
<span class="copy">Configuration Error</span>
</dd>
</dl>
</div>
<div class="status-message has-actions">
<p class="copy">${_("A system error occurred while your course was being processed. Please go to the original course to try the re-run again, or contact your PM for assistance.")}</p>
<ul class="status-actions">
<li class="action action-dismiss">
<a href="#" class="button dismiss-button" data-dismiss-link="${dismiss_link}">
<i class="icon icon-remove-sign"></i>
<span class="button-copy">${_("Dismiss")}</span>
</a>
</li>
</ul>
</div>
</li>
%endif
%endfor
</ul>
</div>
%endif
%if len(courses) > 0:
<div class="courses">
<ul class="list-courses">
%for course, url, lms_link, org, num, run in sorted(courses, key=lambda s: s[0].lower() if s[0] is not None else ''):
%for course, url, lms_link, rerun_link, org, num, run in sorted(courses, key=lambda s: s[0].lower() if s[0] is not None else ''):
<li class="course-item">
<a class="course-link" href="${url}">
<h3 class="course-title">${course}</h3>
......@@ -154,12 +248,22 @@ require(["domReady!", "jquery", "jquery.form", "js/index"], function(doc, $) {
</a>
<ul class="item-actions course-actions">
<li class="action">
<a href="${lms_link}" rel="external" class="button view-button view-live-button">${_("View Live")}</a>
% if course_creator_status=='granted':
<li class="action action-rerun">
<a href="${rerun_link}" class="button rerun-button">${_("Re-run Course")}</a>
</li>
% endif
<li class="action action-view">
<a href="${lms_link}" rel="external" class="button view-button">${_("View Live")}</a>
</li>
</ul>
</li>
%endfor
% if course_creator_status=='granted':
<script type="text/javascript">
$('.course-item').addClass('can-rerun');
</script>
% endif
</ul>
</div>
......
......@@ -34,7 +34,7 @@
<article class="content-primary" role="main">
%if allow_actions:
<div class="wrapper-create-element animate wrapper-create-user">
<form class="create-user" id="create-user-form" name="create-user-form">
<form class="form-create create-user" id="create-user-form" name="create-user-form">
<div class="wrapper-form">
<h3 class="title">${_("Add a User to Your Course's Team")}</h3>
......@@ -147,7 +147,7 @@
<div class="bit">
<h3 class="title-3">${_("Course Team Roles")}</h3>
<p>${_("Course team members, or staff, are course co-authors. They have full writing and editing privileges on all course content.")}</p>
<p>${_("Admins are course team members who can add and remove other course team members.")}</p>
<p>${_("Admins are course team members who can add and remove other course team members.")}</p>
</div>
% if user_is_instuctor and len(instructors) == 1:
......
......@@ -74,6 +74,7 @@ urlpatterns += patterns(
),
url(r'^course/{}?$'.format(settings.COURSE_KEY_PATTERN), 'course_handler', name='course_handler'),
url(r'^course_notifications/{}/(?P<action_state_id>\d+)?$'.format(settings.COURSE_KEY_PATTERN), 'course_notifications_handler'),
url(r'^course_rerun/{}$'.format(settings.COURSE_KEY_PATTERN), 'course_rerun_handler', name='course_rerun_handler'),
url(r'^container/{}$'.format(settings.USAGE_KEY_PATTERN), 'container_handler'),
url(r'^checklists/{}/(?P<checklist_index>\d+)?$'.format(settings.COURSE_KEY_PATTERN), 'checklists_handler'),
url(r'^orphan/{}$'.format(settings.COURSE_KEY_PATTERN), 'orphan_handler'),
......
......@@ -113,7 +113,7 @@ class CourseRerunUIStateManager(CourseActionUIStateManager):
FAILED = "failed"
SUCCEEDED = "succeeded"
def initiated(self, source_course_key, destination_course_key, user):
def initiated(self, source_course_key, destination_course_key, user, display_name):
"""
To be called when a new rerun is initiated for the given course by the given user.
"""
......@@ -123,6 +123,7 @@ class CourseRerunUIStateManager(CourseActionUIStateManager):
user=user,
allow_not_found=True,
source_course_key=source_course_key,
display_name=display_name,
)
def succeeded(self, course_key):
......
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'CourseRerunState.display_name'
db.add_column('course_action_state_coursererunstate', 'display_name',
self.gf('django.db.models.fields.CharField')(default='', max_length=255),
keep_default=False)
def backwards(self, orm):
# Deleting field 'CourseRerunState.display_name'
db.delete_column('course_action_state_coursererunstate', 'display_name')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'course_action_state.coursererunstate': {
'Meta': {'unique_together': "(('course_key', 'action'),)", 'object_name': 'CourseRerunState'},
'action': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}),
'course_key': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
'created_time': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'created_user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'created_by_user+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['auth.User']"}),
'display_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'message': ('django.db.models.fields.CharField', [], {'max_length': '1000'}),
'should_display': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'source_course_key': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
'state': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
'updated_time': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'updated_user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'updated_by_user+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['auth.User']"})
}
}
complete_apps = ['course_action_state']
\ No newline at end of file
......@@ -109,6 +109,9 @@ class CourseRerunState(CourseActionUIState):
# Original course that is being rerun
source_course_key = CourseKeyField(max_length=255, db_index=True)
# Display name for destination course
display_name = models.CharField(max_length=255, default="")
# MANAGERS
# Override the abstract class' manager with a Rerun-specific manager that inherits from the base class' manager.
objects = CourseRerunUIStateManager()
......@@ -17,10 +17,13 @@ class TestCourseRerunStateManager(TestCase):
self.source_course_key = CourseLocator("source_org", "source_course_num", "source_run")
self.course_key = CourseLocator("test_org", "test_course_num", "test_run")
self.created_user = UserFactory()
self.display_name = "destination course name"
self.expected_rerun_state = {
'created_user': self.created_user,
'updated_user': self.created_user,
'course_key': self.course_key,
'source_course_key': self.source_course_key,
"display_name": self.display_name,
'action': CourseRerunUIStateManager.ACTION,
'should_display': True,
'message': "",
......@@ -53,10 +56,16 @@ class TestCourseRerunStateManager(TestCase):
})
self.verify_rerun_state()
def test_rerun_initiated(self):
def initiate_rerun(self):
CourseRerunState.objects.initiated(
source_course_key=self.source_course_key, destination_course_key=self.course_key, user=self.created_user
source_course_key=self.source_course_key,
destination_course_key=self.course_key,
user=self.created_user,
display_name=self.display_name,
)
def test_rerun_initiated(self):
self.initiate_rerun()
self.expected_rerun_state.update(
{'state': CourseRerunUIStateManager.State.IN_PROGRESS}
)
......@@ -64,9 +73,7 @@ class TestCourseRerunStateManager(TestCase):
def test_rerun_succeeded(self):
# initiate
CourseRerunState.objects.initiated(
source_course_key=self.source_course_key, destination_course_key=self.course_key, user=self.created_user
)
self.initiate_rerun()
# set state to succeed
CourseRerunState.objects.succeeded(course_key=self.course_key)
......@@ -80,9 +87,7 @@ class TestCourseRerunStateManager(TestCase):
def test_rerun_failed(self):
# initiate
CourseRerunState.objects.initiated(
source_course_key=self.source_course_key, destination_course_key=self.course_key, user=self.created_user
)
self.initiate_rerun()
# set state to fail
exception = Exception("failure in rerunning")
......
......@@ -1794,7 +1794,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
xblock_class = self.mixologist.mix(xblock_class)
for field_name, value in fields.iteritems():
if value:
if value is not None:
if isinstance(xblock_class.fields[field_name], Reference):
fields[field_name] = value.block_id
elif isinstance(xblock_class.fields[field_name], ReferenceList):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment