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 ...@@ -5,6 +5,7 @@ This file contains celery tasks for contentstore views
from celery.task import task from celery.task import task
from django.contrib.auth.models import User from django.contrib.auth.models import User
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import DuplicateCourseError, ItemNotFoundError from xmodule.modulestore.exceptions import DuplicateCourseError, ItemNotFoundError
from course_action_state.models import CourseRerunState from course_action_state.models import CourseRerunState
from contentstore.utils import initialize_permissions from contentstore.utils import initialize_permissions
...@@ -32,13 +33,11 @@ def rerun_course(source_course_key_string, destination_course_key_string, user_i ...@@ -32,13 +33,11 @@ def rerun_course(source_course_key_string, destination_course_key_string, user_i
# update state: Succeeded # update state: Succeeded
CourseRerunState.objects.succeeded(course_key=destination_course_key) CourseRerunState.objects.succeeded(course_key=destination_course_key)
return "succeeded" return "succeeded"
except DuplicateCourseError as exc: except DuplicateCourseError as exc:
# do NOT delete the original course, only update the status # do NOT delete the original course, only update the status
CourseRerunState.objects.failed(course_key=destination_course_key, exception=exc) CourseRerunState.objects.failed(course_key=destination_course_key, exception=exc)
return "duplicate course" return "duplicate course"
# catch all exceptions so we can update the state and properly cleanup the course. # catch all exceptions so we can update the state and properly cleanup the course.
......
...@@ -1580,9 +1580,12 @@ class RerunCourseTest(ContentStoreTestCase): ...@@ -1580,9 +1580,12 @@ class RerunCourseTest(ContentStoreTestCase):
json_resp = parse_json(response) json_resp = parse_json(response)
self.assertNotIn('ErrMsg', json_resp) self.assertNotIn('ErrMsg', json_resp)
destination_course_key = CourseKey.from_string(json_resp['destination_course_key']) destination_course_key = CourseKey.from_string(json_resp['destination_course_key'])
return 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): 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""" """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. # TODO Update this once the Rerun UI LMS-11011 is implemented.
...@@ -1615,6 +1618,7 @@ class RerunCourseTest(ContentStoreTestCase): ...@@ -1615,6 +1618,7 @@ class RerunCourseTest(ContentStoreTestCase):
rerun_state = CourseRerunState.objects.find_first(course_key=destination_course_key) rerun_state = CourseRerunState.objects.find_first(course_key=destination_course_key)
expected_states = { expected_states = {
'state': CourseRerunUIStateManager.State.SUCCEEDED, 'state': CourseRerunUIStateManager.State.SUCCEEDED,
'display_name': self.destination_course_data['display_name'],
'source_course_key': source_course.id, 'source_course_key': source_course.id,
'course_key': destination_course_key, 'course_key': destination_course_key,
'should_display': True, 'should_display': True,
......
...@@ -329,7 +329,9 @@ class TestCourseListing(ModuleStoreTestCase): ...@@ -329,7 +329,9 @@ class TestCourseListing(ModuleStoreTestCase):
# simulate initiation of course actions # simulate initiation of course actions
for course in courses_in_progress: 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 # verify return values
for method in (_accessible_courses_list_from_groups, _accessible_courses_list): for method in (_accessible_courses_list_from_groups, _accessible_courses_list):
......
...@@ -5,11 +5,9 @@ import json ...@@ -5,11 +5,9 @@ import json
import random import random
import string # pylint: disable=W0402 import string # pylint: disable=W0402
import logging import logging
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
import django.utils import django.utils
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django_future.csrf import ensure_csrf_cookie
from django.conf import settings from django.conf import settings
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
...@@ -25,11 +23,14 @@ from xmodule.modulestore.django import modulestore ...@@ -25,11 +23,14 @@ from xmodule.modulestore.django import modulestore
from xmodule.contentstore.content import StaticContent from xmodule.contentstore.content import StaticContent
from xmodule.tabs import PDFTextbookTabs from xmodule.tabs import PDFTextbookTabs
from xmodule.partitions.partitions import UserPartition, Group from xmodule.partitions.partitions import UserPartition, Group
from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateCourseError from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateCourseError
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
from opaque_keys.edx.locations import Location 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.course_info_model import get_course_updates, update_course_updates, delete_course_update
from contentstore.utils import ( from contentstore.utils import (
add_instructor, add_instructor,
...@@ -47,7 +48,6 @@ from models.settings.course_grading import CourseGradingModel ...@@ -47,7 +48,6 @@ from models.settings.course_grading import CourseGradingModel
from models.settings.course_metadata import CourseMetadata from models.settings.course_metadata import CourseMetadata
from util.json_request import expect_json from util.json_request import expect_json
from util.string_utils import _has_non_ascii_characters from util.string_utils import _has_non_ascii_characters
from .access import has_course_access from .access import has_course_access
from .component import ( from .component import (
OPEN_ENDED_COMPONENT_TYPES, OPEN_ENDED_COMPONENT_TYPES,
...@@ -56,10 +56,8 @@ from .component import ( ...@@ -56,10 +56,8 @@ from .component import (
SPLIT_TEST_COMPONENT_TYPE, SPLIT_TEST_COMPONENT_TYPE,
ADVANCED_COMPONENT_TYPES, ADVANCED_COMPONENT_TYPES,
) )
from .tasks import rerun_course from contentstore.tasks import rerun_course
from .item import create_xblock_info 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 course_creators.views import get_course_creator_status, add_user_with_status_unrequested
from contentstore import utils from contentstore import utils
from student.roles import ( from student.roles import (
...@@ -68,11 +66,11 @@ from student.roles import ( ...@@ -68,11 +66,11 @@ from student.roles import (
from student import auth from student import auth
from course_action_state.models import CourseRerunState, CourseRerunUIStateManager from course_action_state.models import CourseRerunState, CourseRerunUIStateManager
from course_action_state.managers import CourseActionStateItemNotFoundError from course_action_state.managers import CourseActionStateItemNotFoundError
from microsite_configuration import microsite from microsite_configuration import microsite
__all__ = ['course_info_handler', 'course_handler', 'course_info_update_handler', __all__ = ['course_info_handler', 'course_handler', 'course_info_update_handler',
'course_rerun_handler',
'settings_handler', 'settings_handler',
'grading_handler', 'grading_handler',
'advanced_settings_handler', 'advanced_settings_handler',
...@@ -233,6 +231,25 @@ def course_handler(request, course_key_string=None): ...@@ -233,6 +231,25 @@ def course_handler(request, course_key_string=None):
else: else:
return HttpResponseNotFound() 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): def _course_outline_json(request, course_module):
""" """
...@@ -340,9 +357,26 @@ def course_listing(request): ...@@ -340,9 +357,26 @@ def course_listing(request):
course.display_name, course.display_name,
reverse_course_url('course_handler', course.id), reverse_course_url('course_handler', course.id),
get_lms_link_for_item(course.location), get_lms_link_for_item(course.location),
_get_rerun_link_for_item(course.id),
course.display_org_with_default, course.display_org_with_default,
course.display_number_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 # remove any courses in courses that are also in the unsucceeded_course_actions list
...@@ -353,6 +387,8 @@ def course_listing(request): ...@@ -353,6 +387,8 @@ def course_listing(request):
if not isinstance(c, ErrorDescriptor) and (c.id not in unsucceeded_action_course_keys) 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', { return render_to_response('index.html', {
'courses': courses, 'courses': courses,
'unsucceeded_course_actions': unsucceeded_course_actions, 'unsucceeded_course_actions': unsucceeded_course_actions,
...@@ -363,6 +399,10 @@ def course_listing(request): ...@@ -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 @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def course_index(request, course_key): def course_index(request, course_key):
...@@ -398,6 +438,9 @@ 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, 'rerun_notification_id': current_action.id if current_action else None,
'course_release_date': course_release_date, 'course_release_date': course_release_date,
'settings_url': settings_url, '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): ...@@ -554,7 +597,7 @@ def _rerun_course(request, org, number, run, fields):
add_instructor(destination_course_key, request.user, request.user) add_instructor(destination_course_key, request.user, request.user)
# Mark the action as initiated # 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 the course as a new celery task
rerun_course.delay(unicode(source_course_key), unicode(destination_course_key), request.user.id, fields) 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 ...@@ -38,8 +38,15 @@ FEATURES['ALLOW_ALL_ADVANCED_COMPONENTS'] = True
################################# CELERY ###################################### ################################# CELERY ######################################
# By default don't use a worker, execute tasks as if they were local functions # 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 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 ################################ ################################ DEBUG TOOLBAR ################################
INSTALLED_APPS += ('debug_toolbar', 'debug_toolbar_mongo') INSTALLED_APPS += ('debug_toolbar', 'debug_toolbar_mongo')
MIDDLEWARE_CLASSES += ('debug_toolbar.middleware.DebugToolbarMiddleware',) MIDDLEWARE_CLASSES += ('debug_toolbar.middleware.DebugToolbarMiddleware',)
......
require(["domReady", "jquery", "underscore", "js/utils/cancel_on_escape"], require(["domReady", "jquery", "underscore", "js/utils/cancel_on_escape"],
function (domReady, $, _, CancelOnEscape) { 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) { var saveNewCourse = function (e) {
e.preventDefault(); e.preventDefault();
...@@ -164,5 +175,6 @@ require(["domReady", "jquery", "underscore", "js/utils/cancel_on_escape"], ...@@ -164,5 +175,6 @@ require(["domReady", "jquery", "underscore", "js/utils/cancel_on_escape"],
domReady(function () { domReady(function () {
$('.new-course-button').bind('click', addNewCourse); $('.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 ...@@ -5,6 +5,17 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
var modalSelector = '.edit-section-publish-settings'; 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) { var toggleSections = function(e) {
e.preventDefault(); e.preventDefault();
...@@ -222,6 +233,8 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe ...@@ -222,6 +233,8 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
$('.toggle-button-sections').bind('click', toggleSections); $('.toggle-button-sections').bind('click', toggleSections);
$('.expand-collapse').bind('click', toggleSubmodules); $('.expand-collapse').bind('click', toggleSubmodules);
$('.dismiss-button').bind('click', dismissNotification);
var $body = $('body'); var $body = $('body');
$body.on('click', '.section-published-date .edit-release-date', editSectionPublishDate); $body.on('click', '.section-published-date .edit-release-date', editSectionPublishDate);
$body.on('click', '.edit-section-publish-settings .action-save', saveSetSectionScheduleDate); $body.on('click', '.edit-section-publish-settings .action-save', saveSetSectionScheduleDate);
......
...@@ -240,6 +240,282 @@ p, ul, ol, dl { ...@@ -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 @@ ...@@ -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 // green secondary button
%btn-secondary-green { %btn-secondary-green {
@extend %ui-btn-secondary; @extend %ui-btn-secondary;
...@@ -213,17 +234,6 @@ ...@@ -213,17 +234,6 @@
// ==================== // ====================
// calls-to-action
// ====================
// specific buttons - view live
%view-live-button {
@extend %t-action4;
}
// ====================
// UI: element actions list // UI: element actions list
%actions-list { %actions-list {
......
...@@ -137,28 +137,10 @@ form { ...@@ -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 // ELEM: form
// form styling for creating a new content item (course, user, textbook) // 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; @extend %ui-window;
.title { .title {
...@@ -253,12 +235,19 @@ form[class^="create-"] { ...@@ -253,12 +235,19 @@ form[class^="create-"] {
.tip { .tip {
@extend %t-copy-sub2; @extend %t-copy-sub2;
@include transition(color, 0.15s, ease-in-out); @include transition(color 0.15s ease-in-out);
display: block; display: block;
margin-top: ($baseline/4); margin-top: ($baseline/4);
color: $gray-l3; color: $gray-l3;
} }
.tip-note {
display: block;
margin-top: ($baseline/4);
}
.tip-error { .tip-error {
display: none; display: none;
float: none; float: none;
...@@ -365,7 +354,6 @@ form[class^="create-"] { ...@@ -365,7 +354,6 @@ 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 // TOOD: abstract this out into a Sass placeholder
...@@ -402,6 +390,25 @@ form[class^="create-"] { ...@@ -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 // forms - grandfathered
......
...@@ -527,7 +527,7 @@ ...@@ -527,7 +527,7 @@
&.wrapper-alert-warning { &.wrapper-alert-warning {
box-shadow: 0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $orange; box-shadow: 0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $orange;
[class^="icon"] { .alert-symbol {
color: $orange; color: $orange;
} }
} }
...@@ -535,7 +535,7 @@ ...@@ -535,7 +535,7 @@
&.wrapper-alert-error { &.wrapper-alert-error {
box-shadow: 0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $red-l1; 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; color: $red-l1;
} }
} }
...@@ -543,7 +543,7 @@ ...@@ -543,7 +543,7 @@
&.wrapper-alert-confirmation { &.wrapper-alert-confirmation {
box-shadow: 0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $green; box-shadow: 0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $green;
[class^="icon"] { .alert-symbol {
color: $green; color: $green;
} }
} }
...@@ -551,7 +551,7 @@ ...@@ -551,7 +551,7 @@
&.wrapper-alert-announcement { &.wrapper-alert-announcement {
box-shadow: 0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $blue; box-shadow: 0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $blue;
[class^="icon"] { .alert-symbol {
color: $blue; color: $blue;
} }
} }
...@@ -559,7 +559,7 @@ ...@@ -559,7 +559,7 @@
&.wrapper-alert-step-required { &.wrapper-alert-step-required {
box-shadow: 0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $pink; box-shadow: 0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $pink;
[class^="icon"] { .alert-symbol {
color: $pink; color: $pink;
} }
} }
...@@ -579,11 +579,11 @@ ...@@ -579,11 +579,11 @@
@extend %t-strong; @extend %t-strong;
} }
[class^="icon"], .copy { .alert-symbol, .copy {
float: left; float: left;
} }
[class^="icon"] { .alert-symbol {
@include transition (color 0.50s ease-in-out 0s); @include transition (color 0.50s ease-in-out 0s);
@extend %t-icon3; @extend %t-icon3;
width: flex-grid(1, 12); width: flex-grid(1, 12);
...@@ -605,7 +605,7 @@ ...@@ -605,7 +605,7 @@
// with actions // with actions
&.has-actions { &.has-actions {
[class^="icon"] { .alert-symbol {
width: flex-grid(1, 12); width: flex-grid(1, 12);
} }
...@@ -667,6 +667,28 @@ ...@@ -667,6 +667,28 @@
background: $gray-d1; 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 @@ ...@@ -38,6 +38,7 @@
@import 'views/dashboard'; @import 'views/dashboard';
@import 'views/export'; @import 'views/export';
@import 'views/index'; @import 'views/index';
@import 'views/course-create';
@import 'views/import'; @import 'views/import';
@import 'views/outline'; @import 'views/outline';
@import 'views/settings'; @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 @@ ...@@ -294,120 +294,298 @@
// ELEM: course listings // ELEM: course listings
.courses { .courses {
margin: $baseline 0; 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 { .list-courses {
margin-top: $baseline; margin-top: $baseline;
border-radius: 3px; border-radius: 3px;
border: 1px solid $gray; border: 1px solid $gray-l2;
background: $white; background: $white;
box-shadow: 0 1px 2px $shadow-l1; box-shadow: 0 1px 1px $shadow-l1;
.course-item { li:last-child {
@include box-sizing(border-box); margin-bottom: 0;
width: flex-grid(9, 9); }
position: relative; }
border-bottom: 1px solid $gray-l1;
padding: $baseline;
// STATE: hover/focus
&:hover {
background: $paleYellow;
.course-actions .view-live-button {
opacity: 1.0;
pointer-events: auto;
}
.course-title { // UI: course wrappers (needed for status messages)
color: $orange-d1; .wrapper-course {
}
.course-metadata { // CASE: has status
opacity: 1.0; &.has-status {
}
}
.course-link, .course-actions { .course-status {
@include box-sizing(border-box); @include box-sizing(border-box);
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
} width: flex-grid(3, 9);
padding-right: ($baseline/2);
text-align: right;
// encompassing course link .value {
.course-link {
@extend %ui-depth2;
width: flex-grid(7, 9);
margin-right: flex-gutter();
}
// course title .copy, *[class^="icon"] {
.course-title { display: inline-block;
@extend %t-title4; vertical-align: middle;
@extend %t-light; }
margin: 0 ($baseline*2) ($baseline/4) 0;
*[class^="icon"] {
@extend %t-icon4;
margin-right: ($baseline/2);
}
.copy {
@extend %t-copy-sub1;
}
}
} }
// course metadata .status-message {
.course-metadata {
@extend %t-copy-sub1; @extend %t-copy-sub1;
@include transition(opacity $tmg-f1 ease-in-out 0); background-color: $gray-l5;
color: $gray; box-shadow: 0 2px 2px 0 $shadow inset;
opacity: 0.75; padding: ($baseline*0.75) $baseline;
.metadata-item { &.has-actions {
display: inline-block;
&:after { .copy, .status-actions {
content: "/"; display: inline-block;
margin-left: ($baseline/10); vertical-align: middle;
margin-right: ($baseline/10); }
color: $gray-l4;
.copy {
width: 65%;
margin: 0 $baseline 0 0;
} }
&:last-child { .status-actions {
width: 30%;
text-align: right;
&:after { .button {
content: ""; @extend %btn-secondary-white;
margin-left: 0;
margin-right: 0;
} }
}
.label { .icon,.button-copy {
@extend %cont-text-sr; 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 { .course-actions {
@extend %ui-depth3; opacity: 1.0;
position: static; pointer-events: auto;
width: flex-grid(2, 9); }
text-align: right;
// view live button .course-title {
.view-live-button { color: $orange-d1;
@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;
&:hover { .course-metadata {
opacity: 1.0; opacity: 1.0;
pointer-events: auto; }
}
.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 { &: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 // ELEM: new user form
.wrapper-create-course { .wrapper-create-course {
...@@ -494,6 +672,5 @@ ...@@ -494,6 +672,5 @@
margin-bottom: 0; margin-bottom: 0;
padding: ($baseline*.5) ($baseline*1.5) 8px ($baseline*1.5); padding: ($baseline*.5) ($baseline*1.5) 8px ($baseline*1.5);
} }
} }
} }
...@@ -343,7 +343,9 @@ ...@@ -343,7 +343,9 @@
<% online_help_token = self.online_help_token() if hasattr(self, 'online_help_token') else None %> <% 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" /> <%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"> <div id="content">
<%block name="content"></%block> <%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 ...@@ -36,6 +36,31 @@ from contentstore.utils import reverse_usage_url
% endfor % endfor
</%block> </%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"> <%block name="content">
<div class="wrapper-mast wrapper"> <div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle"> <header class="mast has-actions has-subtitle">
......
...@@ -77,7 +77,7 @@ require(["domReady!", "jquery", "jquery.form", "js/index"], function(doc, $) { ...@@ -77,7 +77,7 @@ require(["domReady!", "jquery", "jquery.form", "js/index"], function(doc, $) {
% if course_creator_status=='granted': % if course_creator_status=='granted':
<div class="wrapper-create-element wrapper-create-course"> <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 class="wrap-error">
<div id="course_creation_error" name="course_creation_error" class="message message-status message-status error" role="alert"> <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> <p>${_("Please correct the highlighted fields below.")}</p>
...@@ -131,10 +131,104 @@ require(["domReady!", "jquery", "jquery.form", "js/index"], function(doc, $) { ...@@ -131,10 +131,104 @@ require(["domReady!", "jquery", "jquery.form", "js/index"], function(doc, $) {
</div> </div>
% endif % 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: %if len(courses) > 0:
<div class="courses"> <div class="courses">
<ul class="list-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"> <li class="course-item">
<a class="course-link" href="${url}"> <a class="course-link" href="${url}">
<h3 class="course-title">${course}</h3> <h3 class="course-title">${course}</h3>
...@@ -154,12 +248,22 @@ require(["domReady!", "jquery", "jquery.form", "js/index"], function(doc, $) { ...@@ -154,12 +248,22 @@ require(["domReady!", "jquery", "jquery.form", "js/index"], function(doc, $) {
</a> </a>
<ul class="item-actions course-actions"> <ul class="item-actions course-actions">
<li class="action"> % if course_creator_status=='granted':
<a href="${lms_link}" rel="external" class="button view-button view-live-button">${_("View Live")}</a> <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> </li>
</ul> </ul>
</li> </li>
%endfor %endfor
% if course_creator_status=='granted':
<script type="text/javascript">
$('.course-item').addClass('can-rerun');
</script>
% endif
</ul> </ul>
</div> </div>
......
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
<article class="content-primary" role="main"> <article class="content-primary" role="main">
%if allow_actions: %if allow_actions:
<div class="wrapper-create-element animate wrapper-create-user"> <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"> <div class="wrapper-form">
<h3 class="title">${_("Add a User to Your Course's Team")}</h3> <h3 class="title">${_("Add a User to Your Course's Team")}</h3>
...@@ -147,7 +147,7 @@ ...@@ -147,7 +147,7 @@
<div class="bit"> <div class="bit">
<h3 class="title-3">${_("Course Team Roles")}</h3> <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>${_("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> </div>
% if user_is_instuctor and len(instructors) == 1: % if user_is_instuctor and len(instructors) == 1:
......
...@@ -74,6 +74,7 @@ urlpatterns += patterns( ...@@ -74,6 +74,7 @@ urlpatterns += patterns(
), ),
url(r'^course/{}?$'.format(settings.COURSE_KEY_PATTERN), 'course_handler', name='course_handler'), 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_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'^container/{}$'.format(settings.USAGE_KEY_PATTERN), 'container_handler'),
url(r'^checklists/{}/(?P<checklist_index>\d+)?$'.format(settings.COURSE_KEY_PATTERN), 'checklists_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'), url(r'^orphan/{}$'.format(settings.COURSE_KEY_PATTERN), 'orphan_handler'),
......
...@@ -113,7 +113,7 @@ class CourseRerunUIStateManager(CourseActionUIStateManager): ...@@ -113,7 +113,7 @@ class CourseRerunUIStateManager(CourseActionUIStateManager):
FAILED = "failed" FAILED = "failed"
SUCCEEDED = "succeeded" 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. To be called when a new rerun is initiated for the given course by the given user.
""" """
...@@ -123,6 +123,7 @@ class CourseRerunUIStateManager(CourseActionUIStateManager): ...@@ -123,6 +123,7 @@ class CourseRerunUIStateManager(CourseActionUIStateManager):
user=user, user=user,
allow_not_found=True, allow_not_found=True,
source_course_key=source_course_key, source_course_key=source_course_key,
display_name=display_name,
) )
def succeeded(self, course_key): 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): ...@@ -109,6 +109,9 @@ class CourseRerunState(CourseActionUIState):
# Original course that is being rerun # Original course that is being rerun
source_course_key = CourseKeyField(max_length=255, db_index=True) source_course_key = CourseKeyField(max_length=255, db_index=True)
# Display name for destination course
display_name = models.CharField(max_length=255, default="")
# MANAGERS # MANAGERS
# Override the abstract class' manager with a Rerun-specific manager that inherits from the base class' manager. # Override the abstract class' manager with a Rerun-specific manager that inherits from the base class' manager.
objects = CourseRerunUIStateManager() objects = CourseRerunUIStateManager()
...@@ -17,10 +17,13 @@ class TestCourseRerunStateManager(TestCase): ...@@ -17,10 +17,13 @@ class TestCourseRerunStateManager(TestCase):
self.source_course_key = CourseLocator("source_org", "source_course_num", "source_run") self.source_course_key = CourseLocator("source_org", "source_course_num", "source_run")
self.course_key = CourseLocator("test_org", "test_course_num", "test_run") self.course_key = CourseLocator("test_org", "test_course_num", "test_run")
self.created_user = UserFactory() self.created_user = UserFactory()
self.display_name = "destination course name"
self.expected_rerun_state = { self.expected_rerun_state = {
'created_user': self.created_user, 'created_user': self.created_user,
'updated_user': self.created_user, 'updated_user': self.created_user,
'course_key': self.course_key, 'course_key': self.course_key,
'source_course_key': self.source_course_key,
"display_name": self.display_name,
'action': CourseRerunUIStateManager.ACTION, 'action': CourseRerunUIStateManager.ACTION,
'should_display': True, 'should_display': True,
'message': "", 'message': "",
...@@ -53,10 +56,16 @@ class TestCourseRerunStateManager(TestCase): ...@@ -53,10 +56,16 @@ class TestCourseRerunStateManager(TestCase):
}) })
self.verify_rerun_state() self.verify_rerun_state()
def test_rerun_initiated(self): def initiate_rerun(self):
CourseRerunState.objects.initiated( 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( self.expected_rerun_state.update(
{'state': CourseRerunUIStateManager.State.IN_PROGRESS} {'state': CourseRerunUIStateManager.State.IN_PROGRESS}
) )
...@@ -64,9 +73,7 @@ class TestCourseRerunStateManager(TestCase): ...@@ -64,9 +73,7 @@ class TestCourseRerunStateManager(TestCase):
def test_rerun_succeeded(self): def test_rerun_succeeded(self):
# initiate # initiate
CourseRerunState.objects.initiated( self.initiate_rerun()
source_course_key=self.source_course_key, destination_course_key=self.course_key, user=self.created_user
)
# set state to succeed # set state to succeed
CourseRerunState.objects.succeeded(course_key=self.course_key) CourseRerunState.objects.succeeded(course_key=self.course_key)
...@@ -80,9 +87,7 @@ class TestCourseRerunStateManager(TestCase): ...@@ -80,9 +87,7 @@ class TestCourseRerunStateManager(TestCase):
def test_rerun_failed(self): def test_rerun_failed(self):
# initiate # initiate
CourseRerunState.objects.initiated( self.initiate_rerun()
source_course_key=self.source_course_key, destination_course_key=self.course_key, user=self.created_user
)
# set state to fail # set state to fail
exception = Exception("failure in rerunning") exception = Exception("failure in rerunning")
......
...@@ -1794,7 +1794,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -1794,7 +1794,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
xblock_class = self.mixologist.mix(xblock_class) xblock_class = self.mixologist.mix(xblock_class)
for field_name, value in fields.iteritems(): for field_name, value in fields.iteritems():
if value: if value is not None:
if isinstance(xblock_class.fields[field_name], Reference): if isinstance(xblock_class.fields[field_name], Reference):
fields[field_name] = value.block_id fields[field_name] = value.block_id
elif isinstance(xblock_class.fields[field_name], ReferenceList): 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