Commit 30fc0441 by Ben McMorran

Merge pull request #4782 from edx/reruns/ui

Reruns/ui
parents fa2154a6 62643a59
......@@ -4,7 +4,10 @@ This file contains celery tasks for contentstore views
from celery.task import task
from django.contrib.auth.models import User
import json
from xmodule.modulestore.django import modulestore
from xmodule.course_module import CourseFields
from xmodule.modulestore.exceptions import DuplicateCourseError, ItemNotFoundError
from course_action_state.models import CourseRerunState
from contentstore.utils import initialize_permissions
......@@ -17,9 +20,10 @@ def rerun_course(source_course_key_string, destination_course_key_string, user_i
Reruns a course in a new celery task.
"""
try:
# deserialize the keys
# deserialize the payload
source_course_key = CourseKey.from_string(source_course_key_string)
destination_course_key = CourseKey.from_string(destination_course_key_string)
fields = deserialize_fields(fields) if fields else None
# use the split modulestore as the store for the rerun course,
# as the Mongo modulestore doesn't support multiple runs of the same course.
......@@ -32,13 +36,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.
......@@ -54,3 +56,10 @@ def rerun_course(source_course_key_string, destination_course_key_string, user_i
pass
return "exception: " + unicode(exc)
def deserialize_fields(json_fields):
fields = json.loads(json_fields)
for field_name, value in fields.iteritems():
fields[field_name] = getattr(CourseFields, field_name).from_json(value)
return fields
"""
Unit tests for cloning a course between the same and different module stores.
"""
import json
from opaque_keys.edx.locator import CourseLocator
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore import ModuleStoreEnum, EdxJSONEncoder
from contentstore.tests.utils import CourseTestCase
from contentstore.tasks import rerun_course
from contentstore.views.access import has_course_access
from course_action_state.models import CourseRerunState
from course_action_state.managers import CourseRerunUIStateManager
from mock import patch, Mock
class CloneCourseTest(CourseTestCase):
......@@ -39,3 +45,46 @@ class CloneCourseTest(CourseTestCase):
)
self.store.clone_course(split_course3_id, split_course4_id, self.user.id)
self.assertCoursesEqual(split_course3_id, split_course4_id)
def test_rerun_course(self):
"""
Unit tests for :meth: `contentstore.tasks.rerun_course`
"""
mongo_course1_id = self.import_and_populate_course()
# rerun from mongo into split
split_course3_id = CourseLocator(
org="edx3", course="split3", run="rerun_test"
)
# Mark the action as initiated
fields = {'display_name': 'rerun'}
CourseRerunState.objects.initiated(mongo_course1_id, split_course3_id, self.user, fields['display_name'])
result = rerun_course.delay(unicode(mongo_course1_id), unicode(split_course3_id), self.user.id,
json.dumps(fields, cls=EdxJSONEncoder))
self.assertEqual(result.get(), "succeeded")
self.assertTrue(has_course_access(self.user, split_course3_id), "Didn't grant access")
rerun_state = CourseRerunState.objects.find_first(course_key=split_course3_id)
self.assertEqual(rerun_state.state, CourseRerunUIStateManager.State.SUCCEEDED)
# try creating rerunning again to same name and ensure it generates error
result = rerun_course.delay(unicode(mongo_course1_id), unicode(split_course3_id), self.user.id)
self.assertEqual(result.get(), "duplicate course")
# the below will raise an exception if the record doesn't exist
CourseRerunState.objects.find_first(
course_key=split_course3_id,
state=CourseRerunUIStateManager.State.FAILED
)
# try to hit the generic exception catch
with patch('xmodule.modulestore.split_mongo.mongo_connection.MongoConnection.insert_course_index', Mock(side_effect=Exception)):
split_course4_id = CourseLocator(org="edx3", course="split3", run="rerun_fail")
fields = {'display_name': 'total failure'}
CourseRerunState.objects.initiated(split_course3_id, split_course4_id, self.user, fields['display_name'])
result = rerun_course.delay(unicode(split_course3_id), unicode(split_course4_id), self.user.id,
json.dumps(fields, cls=EdxJSONEncoder))
self.assertIn("exception: ", result.get())
self.assertIsNone(self.store.get_course(split_course4_id), "Didn't delete course after error")
CourseRerunState.objects.find_first(
course_key=split_course4_id,
state=CourseRerunUIStateManager.State.FAILED
)
......@@ -5,6 +5,7 @@
import copy
import mock
import shutil
import lxml
from datetime import timedelta
from fs.osfs import OSFS
......@@ -47,6 +48,9 @@ from student.roles import CourseCreatorRole, CourseInstructorRole
from opaque_keys import InvalidKeyError
from contentstore.tests.utils import get_url
from course_action_state.models import CourseRerunState, CourseRerunUIStateManager
from unittest import skipIf
from course_action_state.managers import CourseActionStateItemNotFoundError
......@@ -1580,31 +1584,33 @@ 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_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.
return '<div class="unsucceeded-course-action" href="/course/{}"'.format(course_key)
def get_course_listing_elements(self, html, course_key):
"""Returns the elements in the course listing section of html that have the given course_key"""
return html.cssselect('.course-item[data-course-key="{}"]'.format(unicode(course_key)))
def get_unsucceeded_course_action_elements(self, html, course_key):
"""Returns the elements in the unsucceeded course action section that have the given course_key"""
return html.cssselect('.courses-processing li[data-course-key="{}"]'.format(unicode(course_key)))
def assertInCourseListing(self, course_key):
"""
Asserts that the given course key is in the accessible course listing section of the html
and NOT in the unsucceeded course action section of the html.
"""
course_listing_html = self.client.get_html('/course/')
self.assertIn(course_key.run, course_listing_html.content)
self.assertNotIn(self.create_unsucceeded_course_action_html(course_key), course_listing_html.content)
course_listing = lxml.html.fromstring(self.client.get_html('/course/').content)
self.assertEqual(len(self.get_course_listing_elements(course_listing, course_key)), 1)
self.assertEqual(len(self.get_unsucceeded_course_action_elements(course_listing, course_key)), 0)
def assertInUnsucceededCourseActions(self, course_key):
"""
Asserts that the given course key is in the unsucceeded course action section of the html
and NOT in the accessible course listing section of the html.
"""
course_listing_html = self.client.get_html('/course/')
self.assertNotIn(course_key.run, course_listing_html.content)
# TODO Verify the course is in the unsucceeded listing once LMS-11011 is implemented.
course_listing = lxml.html.fromstring(self.client.get_html('/course/').content)
self.assertEqual(len(self.get_course_listing_elements(course_listing, course_key)), 0)
self.assertEqual(len(self.get_unsucceeded_course_action_elements(course_listing, course_key)), 1)
def test_rerun_course_success(self):
......@@ -1615,6 +1621,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,
......@@ -1629,6 +1636,7 @@ class RerunCourseTest(ContentStoreTestCase):
self.assertInCourseListing(source_course.id)
self.assertInCourseListing(destination_course_key)
@skipIf(not settings.FEATURES.get('ALLOW_COURSE_RERUNS', False), "ALLOW_COURSE_RERUNS are not enabled")
def test_rerun_course_fail_no_source_course(self):
existent_course_key = CourseFactory.create().id
non_existent_course_key = CourseLocator("org", "non_existent_course", "non_existent_run")
......@@ -1646,7 +1654,7 @@ class RerunCourseTest(ContentStoreTestCase):
self.assertInCourseListing(existent_course_key)
# Verify that the failed course is NOT in the course listings
self.assertInUnsucceededCourseActions(non_existent_course_key)
self.assertInUnsucceededCourseActions(destination_course_key)
def test_rerun_course_fail_duplicate_course(self):
existent_course_key = CourseFactory.create().id
......
"""
Test view handler for rerun (and eventually create)
"""
from django.test.client import RequestFactory
from opaque_keys.edx.keys import CourseKey
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from student.roles import CourseInstructorRole, CourseStaffRole
from student.tests.factories import UserFactory
from contentstore.tests.utils import AjaxEnabledTestClient, parse_json
from datetime import datetime
from xmodule.course_module import CourseFields
class TestCourseListing(ModuleStoreTestCase):
"""
Unit tests for getting the list of courses for a logged in user
"""
def setUp(self):
"""
Add a user and a course
"""
super(TestCourseListing, self).setUp()
# create and log in a staff user.
# create and log in a non-staff user
self.user = UserFactory()
self.factory = RequestFactory()
self.client = AjaxEnabledTestClient()
self.client.login(username=self.user.username, password='test')
source_course = CourseFactory.create(
org='origin',
number='the_beginning',
run='first',
display_name='the one and only',
start=datetime.utcnow()
)
self.source_course_key = source_course.id
for role in [CourseInstructorRole, CourseStaffRole]:
role(self.source_course_key).add_users(self.user)
def tearDown(self):
"""
Reverse the setup
"""
self.client.logout()
ModuleStoreTestCase.tearDown(self)
def test_rerun(self):
"""
Just testing the functionality the view handler adds over the tasks tested in test_clone_course
"""
response = self.client.ajax_post('/course/', {
'source_course_key': unicode(self.source_course_key),
'org': self.source_course_key.org, 'course': self.source_course_key.course, 'run': 'copy',
'display_name': 'not the same old name',
})
self.assertEqual(response.status_code, 200)
data = parse_json(response)
dest_course_key = CourseKey.from_string(data['destination_course_key'])
self.assertEqual(dest_course_key.run, 'copy')
dest_course = self.store.get_course(dest_course_key)
self.assertEqual(dest_course.start, CourseFields.start.default)
......@@ -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):
......
......@@ -146,7 +146,7 @@ def get_lms_link_for_about_page(course_key):
def course_image_url(course):
"""Returns the image url for the course."""
loc = StaticContent.compute_location(course.location.course_key, course.course_image)
path = loc.to_deprecated_string()
path = StaticContent.serialize_asset_key_with_slash(loc)
return path
......
......@@ -277,7 +277,7 @@ def _get_asset_json(display_name, date, location, thumbnail_location, locked):
"""
Helper method for formatting the asset information to send to client.
"""
asset_url = _add_slash(location.to_deprecated_string())
asset_url = StaticContent.serialize_asset_key_with_slash(location)
external_url = settings.LMS_BASE + asset_url
return {
'display_name': display_name,
......@@ -285,14 +285,8 @@ def _get_asset_json(display_name, date, location, thumbnail_location, locked):
'url': asset_url,
'external_url': external_url,
'portable_url': StaticContent.get_static_path_from_location(location),
'thumbnail': _add_slash(unicode(thumbnail_location)) if thumbnail_location else None,
'thumbnail': StaticContent.serialize_asset_key_with_slash(thumbnail_location) if thumbnail_location else None,
'locked': locked,
# Needed for Backbone delete/update.
'id': unicode(location)
}
def _add_slash(url):
if not url.startswith('/'):
url = '/' + url # TODO - re-address this once LMS-11198 is tackled.
return url
......@@ -159,6 +159,7 @@ DEBUG_TOOLBAR_MONGO_STACKTRACES = False
# Enable URL that shows information about the status of variuous services
FEATURES['ENABLE_SERVICE_STATUS'] = True
FEATURES['ALLOW_COURSE_RERUNS'] = True
############################# SEGMENT-IO ##################################
......
......@@ -40,6 +40,10 @@ FEATURES['ALLOW_ALL_ADVANCED_COMPONENTS'] = True
# By default don't use a worker, execute tasks as if they were local functions
CELERY_ALWAYS_EAGER = True
################################ COURSE RERUNS ################################
FEATURES['ALLOW_COURSE_RERUNS'] = True
################################ DEBUG TOOLBAR ################################
INSTALLED_APPS += ('debug_toolbar', 'debug_toolbar_mongo')
MIDDLEWARE_CLASSES += ('debug_toolbar.middleware.DebugToolbarMiddleware',)
......
......@@ -233,6 +233,8 @@ define([
"js/spec/views/pages/container_subviews_spec",
"js/spec/views/pages/group_configurations_spec",
"js/spec/views/pages/course_outline_spec",
"js/spec/views/pages/course_rerun_spec",
"js/spec/views/pages/index_spec",
"js/spec/views/modals/base_modal_spec",
"js/spec/views/modals/edit_xblock_spec",
......
require(["domReady", "jquery", "underscore", "js/utils/cancel_on_escape"],
function (domReady, $, _, CancelOnEscape) {
define(["domReady", "jquery", "underscore", "js/utils/cancel_on_escape", "js/views/utils/create_course_utils",
"js/views/utils/view_utils"],
function (domReady, $, _, CancelOnEscape, CreateCourseUtilsFactory, ViewUtils) {
var CreateCourseUtils = CreateCourseUtilsFactory({
name: '.new-course-name',
org: '.new-course-org',
number: '.new-course-number',
run: '.new-course-run',
save: '.new-course-save',
errorWrapper: '.wrap-error',
errorMessage: '#course_creation_error',
tipError: 'span.tip-error',
error: '.error',
allowUnicode: '.allow-unicode-course-id'
}, {
shown: 'is-shown',
showing: 'is-showing',
hiding: 'is-hiding',
disabled: 'is-disabled',
error: 'error'
});
var saveNewCourse = function (e) {
e.preventDefault();
// One final check for empty values
var errors = _.reduce(
['.new-course-name', '.new-course-org', '.new-course-number', '.new-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) {
if (CreateCourseUtils.hasInvalidRequiredFields()) {
return;
}
......@@ -25,29 +33,19 @@ require(["domReady", "jquery", "underscore", "js/utils/cancel_on_escape"],
var number = $newCourseForm.find('.new-course-number').val();
var run = $newCourseForm.find('.new-course-run').val();
analytics.track('Created a Course', {
'org': org,
'number': number,
'display_name': display_name,
'run': run
});
course_info = {
org: org,
number: number,
display_name: display_name,
run: run
};
$.postJSON('/course/', {
'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) {
$('.wrap-error').addClass('is-shown');
$('#course_creation_error').html('<p>' + data.ErrMsg + '</p>');
$('.new-course-save').addClass('is-disabled');
}
}
);
analytics.track('Created a Course', course_info);
CreateCourseUtils.createCourse(course_info, function (errorMessage) {
$('.wrap-error').addClass('is-shown');
$('#course_creation_error').html('<p>' + errorMessage + '</p>');
$('.new-course-save').addClass('is-disabled');
});
};
var cancelNewCourse = function (e) {
......@@ -78,91 +76,20 @@ require(["domReady", "jquery", "underscore", "js/utils/cancel_on_escape"],
$cancelButton.bind('click', cancelNewCourse);
CancelOnEscape($cancelButton);
// 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(
['.new-course-org', '.new-course-number', '.new-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>');
$('.new-course-save').addClass('is-disabled');
}
else {
$('.wrap-error').removeClass('is-shown');
}
};
// Handle validation asynchronously
_.each(
['.new-course-org', '.new-course-number', '.new-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('li'), error);
validateTotalCourseItemsLength();
});
}
);
var $name = $('.new-course-name');
$name.on('keyup', function () {
var error = validateRequiredField($name.val());
setNewCourseFieldInErr($name.parent('li'), error);
validateTotalCourseItemsLength();
});
};
var validateRequiredField = function (msg) {
return msg.length === 0 ? gettext('Required field.') : '';
CreateCourseUtils.configureHandlers();
};
var setNewCourseFieldInErr = function (el, msg) {
if(msg) {
el.addClass('error');
el.children('span.tip-error').addClass('is-showing').removeClass('is-hiding').text(msg);
$('.new-course-save').addClass('is-disabled');
}
else {
el.removeClass('error');
el.children('span.tip-error').addClass('is-hiding').removeClass('is-showing');
// One "error" div is always present, but hidden or shown
if($('.error').length === 1) {
$('.new-course-save').removeClass('is-disabled');
}
}
var onReady = function () {
$('.new-course-button').bind('click', addNewCourse);
$('.dismiss-button').bind('click', ViewUtils.deleteNotificationHandler(function () {
ViewUtils.reload();
}));
$('.action-reload').bind('click', ViewUtils.reload);
};
domReady(onReady);
domReady(function () {
$('.new-course-button').bind('click', addNewCourse);
});
return {
onReady: onReady
};
});
......@@ -7,7 +7,8 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
getItemsOfType, getItemHeaders, verifyItemsExpanded, expandItemsAndVerifyState, collapseItemsAndVerifyState,
createMockCourseJSON, createMockSectionJSON, createMockSubsectionJSON, verifyTypePublishable,
mockCourseJSON, mockEmptyCourseJSON, mockSingleSectionCourseJSON, createMockVerticalJSON,
mockOutlinePage = readFixtures('mock/mock-course-outline-page.underscore');
mockOutlinePage = readFixtures('mock/mock-course-outline-page.underscore'),
mockRerunNotification = readFixtures('mock/mock-course-rerun-notification.underscore');
createMockCourseJSON = function(options, children) {
return $.extend(true, {}, {
......@@ -243,6 +244,18 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
});
});
describe("Rerun notification", function () {
it("can be dismissed", function () {
appendSetFixtures(mockRerunNotification);
createCourseOutlinePage(this, mockEmptyCourseJSON);
expect($('.wrapper-alert-announcement')).not.toHaveClass('is-hidden');
$('.dismiss-button').click();
create_sinon.expectJsonRequest(requests, 'DELETE', 'dummy_dismiss_url');
create_sinon.respondToDelete(requests);
expect($('.wrapper-alert-announcement')).toHaveClass('is-hidden');
});
});
describe("Button bar", function() {
it('can add a section', function() {
createCourseOutlinePage(this, mockEmptyCourseJSON);
......
define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers", "js/index",
"js/views/utils/view_utils"],
function ($, create_sinon, view_helpers, IndexUtils, ViewUtils) {
describe("Course listing page", function () {
var mockIndexPageHTML = readFixtures('mock/mock-index-page.underscore'), fillInFields;
var fillInFields = function (org, number, run, name) {
$('.new-course-org').val(org);
$('.new-course-number').val(number);
$('.new-course-run').val(run);
$('.new-course-name').val(name);
};
beforeEach(function () {
view_helpers.installMockAnalytics();
appendSetFixtures(mockIndexPageHTML);
IndexUtils.onReady();
});
afterEach(function () {
view_helpers.removeMockAnalytics();
delete window.source_course_key;
});
it("can dismiss notifications", function () {
var requests = create_sinon.requests(this);
var reloadSpy = spyOn(ViewUtils, 'reload');
$('.dismiss-button').click();
create_sinon.expectJsonRequest(requests, 'DELETE', 'dummy_dismiss_url');
create_sinon.respondToDelete(requests);
expect(reloadSpy).toHaveBeenCalled();
});
it("saves new courses", function () {
var requests = create_sinon.requests(this);
var redirectSpy = spyOn(ViewUtils, 'redirect');
$('.new-course-button').click()
fillInFields('DemoX', 'DM101', '2014', 'Demo course');
$('.new-course-save').click();
create_sinon.expectJsonRequest(requests, 'POST', '/course/', {
org: 'DemoX',
number: 'DM101',
run: '2014',
display_name: 'Demo course'
});
create_sinon.respondWithJson(requests, {
url: 'dummy_test_url'
});
expect(redirectSpy).toHaveBeenCalledWith('dummy_test_url');
});
it("displays an error when saving fails", function () {
var requests = create_sinon.requests(this);
$('.new-course-button').click();
fillInFields('DemoX', 'DM101', '2014', 'Demo course');
$('.new-course-save').click();
create_sinon.respondWithJson(requests, {
ErrMsg: 'error message'
});
expect($('.wrap-error')).toHaveClass('is-shown');
expect($('#course_creation_error')).toContainText('error message');
expect($('.new-course-save')).toHaveClass('is-disabled');
});
});
});
define(["domReady", "jquery", "underscore", "js/views/utils/create_course_utils", "js/views/utils/view_utils"],
function (domReady, $, _, CreateCourseUtilsFactory, ViewUtils) {
var CreateCourseUtils = CreateCourseUtilsFactory({
name: '.rerun-course-name',
org: '.rerun-course-org',
number: '.rerun-course-number',
run: '.rerun-course-run',
save: '.rerun-course-save',
errorWrapper: '.wrapper-error',
errorMessage: '#course_rerun_error',
tipError: 'span.tip-error',
error: '.error',
allowUnicode: '.allow-unicode-course-id'
}, {
shown: 'is-shown',
showing: 'is-showing',
hiding: 'is-hidden',
disabled: 'is-disabled',
error: 'error'
});
var saveRerunCourse = function (e) {
e.preventDefault();
if (CreateCourseUtils.hasInvalidRequiredFields()) {
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();
course_info = {
source_course_key: source_course_key,
org: org,
number: number,
display_name: display_name,
run: run
};
analytics.track('Reran a Course', course_info);
CreateCourseUtils.createCourse(course_info, function (errorMessage) {
$('.wrapper-error').addClass('is-shown').removeClass('is-hidden');
$('#course_rerun_error').html('<p>' + errorMessage + '</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 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');
ViewUtils.redirect('/course/');
};
var onReady = function () {
var $cancelButton = $('.rerun-course-cancel');
var $courseRun = $('.rerun-course-run');
$courseRun.focus().select();
$('.rerun-course-save').on('click', saveRerunCourse);
$cancelButton.bind('click', cancelRerunCourse);
$('.cancel-button').bind('click', cancelRerunCourse);
CreateCourseUtils.configureHandlers();
};
domReady(onReady);
// Return these functions so that they can be tested
return {
saveRerunCourse: saveRerunCourse,
cancelRerunCourse: cancelRerunCourse,
onReady: onReady
};
});
define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/feedback_notification",
"js/utils/cancel_on_escape", "js/utils/date_utils", "js/utils/module"],
"js/utils/cancel_on_escape", "js/utils/date_utils", "js/utils/module", "js/views/utils/view_utils"],
function (domReady, $, ui, _, gettext, NotificationView, CancelOnEscape,
DateUtils, ModuleUtils) {
DateUtils, ModuleUtils, ViewUtils) {
var modalSelector = '.edit-section-publish-settings';
......@@ -222,6 +222,10 @@ 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', ViewUtils.deleteNotificationHandler(function () {
$('.wrapper-alert-announcement').remove();
}));
var $body = $('body');
$body.on('click', '.section-published-date .edit-release-date', editSectionPublishDate);
$body.on('click', '.edit-section-publish-settings .action-save', saveSetSectionScheduleDate);
......
......@@ -2,8 +2,8 @@
* This page is used to show the user an outline of the course.
*/
define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views/utils/xblock_utils",
"js/views/course_outline"],
function ($, _, gettext, BasePage, XBlockViewUtils, CourseOutlineView) {
"js/views/course_outline", "js/views/utils/view_utils"],
function ($, _, gettext, BasePage, XBlockViewUtils, CourseOutlineView, ViewUtils) {
var expandedLocators, CourseOutlinePage;
CourseOutlinePage = BasePage.extend({
......@@ -25,6 +25,9 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
self.outlineView.handleAddEvent(event);
});
this.model.on('change', this.setCollapseExpandVisibility, this);
$('.dismiss-button').bind('click', ViewUtils.deleteNotificationHandler(function () {
$('.wrapper-alert-announcement').removeClass('is-shown').addClass('is-hidden')
}));
},
setCollapseExpandVisibility: function() {
......
/**
* Provides utilities for validating courses during creation, for both new courses and reruns.
*/
define(["jquery", "underscore", "gettext", "js/views/utils/view_utils"],
function ($, _, gettext, ViewUtils) {
return function (selectors, classes) {
var validateRequiredField, validateCourseItemEncoding, validateTotalCourseItemsLength, setNewCourseFieldInErr,
hasInvalidRequiredFields, createCourse, validateFilledFields, configureHandlers;
validateRequiredField = function (msg) {
return msg.length === 0 ? gettext('Required field.') : '';
};
// Check that a course (org, number, run) doesn't use any special characters
validateCourseItemEncoding = function (item) {
var required = validateRequiredField(item);
if (required) {
return required;
}
if ($(selectors.allowUnicode).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.
validateTotalCourseItemsLength = function () {
var totalLength = _.reduce(
[selectors.org, selectors.number, selectors.run],
function (sum, ele) {
return sum + $(ele).val().length;
}, 0
);
if (totalLength > 65) {
$(selectors.errorWrapper).addClass(classes.shown).removeClass(classes.hiding);
$(selectors.errorMessage).html('<p>' + gettext('The combined length of the organization, course number, and course run fields cannot be more than 65 characters.') + '</p>');
$(selectors.save).addClass(classes.disabled);
}
else {
$(selectors.errorWrapper).removeClass(classes.shown).addClass(classes.hiding);
}
};
setNewCourseFieldInErr = function (el, msg) {
if (msg) {
el.addClass(classes.error);
el.children(selectors.tipError).addClass(classes.showing).removeClass(classes.hiding).text(msg);
$(selectors.save).addClass(classes.disabled);
}
else {
el.removeClass(classes.error);
el.children(selectors.tipError).addClass(classes.hiding).removeClass(classes.showing);
// One "error" div is always present, but hidden or shown
if ($(selectors.error).length === 1) {
$(selectors.save).removeClass(classes.disabled);
}
}
};
// One final check for empty values
hasInvalidRequiredFields = function () {
return _.reduce(
[selectors.name, selectors.org, selectors.number, selectors.run],
function (acc, ele) {
var $ele = $(ele);
var error = validateRequiredField($ele.val());
setNewCourseFieldInErr($ele.parent(), error);
return error ? true : acc;
},
false
);
};
createCourse = function (courseInfo, errorHandler) {
$.postJSON(
'/course/',
courseInfo,
function (data) {
if (data.url !== undefined) {
ViewUtils.redirect(data.url);
} else if (data.ErrMsg !== undefined) {
errorHandler(data.ErrMsg);
}
}
);
};
// Ensure that all fields are not empty
validateFilledFields = function () {
return _.reduce(
[selectors.org, selectors.number, selectors.run, selectors.name],
function (acc, ele) {
var $ele = $(ele);
return $ele.val().length !== 0 ? acc : false;
},
true
);
};
// Handle validation asynchronously
configureHandlers = function () {
_.each(
[selectors.org, selectors.number, selectors.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();
if (!validateFilledFields()) {
$(selectors.save).addClass(classes.disabled);
}
});
}
);
var $name = $(selectors.name);
$name.on('keyup', function () {
var error = validateRequiredField($name.val());
setNewCourseFieldInErr($name.parent(), error);
validateTotalCourseItemsLength();
if (!validateFilledFields()) {
$(selectors.save).addClass(classes.disabled);
}
});
};
return {
validateRequiredField: validateRequiredField,
validateCourseItemEncoding: validateCourseItemEncoding,
validateTotalCourseItemsLength: validateTotalCourseItemsLength,
setNewCourseFieldInErr: setNewCourseFieldInErr,
hasInvalidRequiredFields: hasInvalidRequiredFields,
createCourse: createCourse,
validateFilledFields: validateFilledFields,
configureHandlers: configureHandlers
};
};
});
......@@ -5,7 +5,7 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js
function ($, _, gettext, NotificationView, PromptView) {
var toggleExpandCollapse, showLoadingIndicator, hideLoadingIndicator, confirmThenRunOperation,
runOperationShowingMessage, disableElementWhileRunning, getScrollOffset, setScrollOffset,
setScrollTop, redirect, hasChangedAttributes;
setScrollTop, redirect, reload, hasChangedAttributes, deleteNotificationHandler;
/**
* Toggles the expanded state of the current element.
......@@ -95,6 +95,21 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js
};
/**
* Returns a handler that removes a notification, both dismissing it and deleting it from the database.
* @param callback function to call when deletion succeeds
*/
deleteNotificationHandler = function(callback) {
return function (event) {
event.preventDefault();
$.ajax({
url: $(this).data('dismiss-link'),
type: 'DELETE',
success: callback
});
};
};
/**
* Performs an animated scroll so that the window has the specified scroll top.
* @param scrollTop The desired scroll top for the window.
*/
......@@ -133,6 +148,13 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js
};
/**
* Reloads the page. This is broken out as its own function for unit testing.
*/
reload = function() {
window.location.reload();
};
/**
* Returns true if a model has changes to at least one of the specified attributes.
* @param model The model in question.
* @param attributes The list of attributes to be compared.
......@@ -158,10 +180,12 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js
'confirmThenRunOperation': confirmThenRunOperation,
'runOperationShowingMessage': runOperationShowingMessage,
'disableElementWhileRunning': disableElementWhileRunning,
'deleteNotificationHandler': deleteNotificationHandler,
'setScrollTop': setScrollTop,
'getScrollOffset': getScrollOffset,
'setScrollOffset': setScrollOffset,
'redirect': redirect,
'reload': reload,
'hasChangedAttributes': hasChangedAttributes
};
});
......@@ -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 {
......
......@@ -138,28 +138,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 {
......@@ -254,12 +236,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;
......@@ -366,7 +355,6 @@ form[class^="create-"] {
}
}
// form - inline xblock name edit on unit, container, outline
// TOOD: abstract this out into a Sass placeholder
......@@ -403,6 +391,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
......
......@@ -141,6 +141,26 @@
}
}
// 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 {
......
......@@ -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,29 @@
background: $gray-d1;
}
}
// with dismiss (to sunset action-alert-clos)
.action-dismiss {
.button {
@extend %btn-secondary-white;
padding:($baseline/4) ($baseline/2);
}
.icon,.button-copy {
display: inline-block;
vertical-align: middle;
}
.icon {
@extend %t-icon4;
margin-right: ($baseline/4);
}
.button-copy {
@extend %t-copy-sub1;
}
}
}
// ====================
......
......@@ -243,6 +243,12 @@
}
}
// learn more (aka external help button)
.external-help-button {
@extend %ui-btn-flat-outline;
@extend %sizing;
}
// actions
.list-actions {
@extend %cont-no-list;
......
......@@ -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;
.icon {
display: inline-block;
vertical-align: middle;
margin-right: ($baseline/4);
}
}
.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;
}
}
}
}
......@@ -236,7 +236,7 @@
@extend %t-strong;
margin: ($baseline/2);
[class^="icon-"] {
.icon {
margin-right: ($baseline/4);
}
}
......@@ -294,120 +294,307 @@
// 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;
}
.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, .icon {
display: inline-block;
vertical-align: middle;
}
.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;
padding:($baseline/4) ($baseline/2);
}
}
.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 +681,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>
......
<%inherit file="base.html" />
<%def name="online_help_token()"><% return "course_rerun" %></%def>
<%! 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>
<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>
<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="${allow_unicode_course_id}" 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>
<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">${_("The new course is set to start on January 1, 2030 at midnight (UTC).")}</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">${_("The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.")}</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">${_("You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.")}</li>
</ul>
</div>
<div class="bit external-help">
<a href="${get_online_help_info(online_help_token())['doc_url']}" target="_blank" class="button external-help-button">${_("Learn more about Course Re-runs")}</a>
</div>
</aside>
</section>
</div>
</div>
</div>
</%block>
......@@ -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 has been changed to a future date); 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>
......@@ -123,7 +123,7 @@ require(["domReady!", "jquery", "jquery.form", "js/index"], function(doc, $) {
</div>
<div class="actions">
<input type="hidden" value="${ allow_unicode_course_id }" class="allow-unicode-course-id" />
<input type="hidden" value="${allow_unicode_course_id}" class="allow-unicode-course-id" />
<input type="submit" value="${_('Create')}" class="action action-primary new-course-save" />
<input type="button" value="${_('Cancel')}" class="action action-secondary action-cancel new-course-cancel" />
</div>
......@@ -131,35 +131,139 @@ require(["domReady!", "jquery", "jquery.form", "js/index"], function(doc, $) {
</div>
% endif
<!-- STATE: processing courses -->
%if allow_course_reruns and rerun_creator_status and len(in_process_course_actions) > 0:
<div class="courses courses-processing">
<h3 class="title">Courses Being Processed</h3>
<ul class="list-courses">
%for course_info in sorted(in_process_course_actions, key=lambda s: s['display_name'].lower() if s['display_name'] is not None else ''):
<!-- STATE: re-run is processing -->
%if course_info['is_in_progress']:
<li class="wrapper-course has-status" data-course-key="${course_info['course_key']}">
<div class="course-item course-rerun is-processing">
<div class="course-details" href="#">
<h3 class="course-title">${course_info['display_name']}</h3>
<div class="course-metadata">
<span class="course-org metadata-item">
<span class="label">${_("Organization:")}</span> <span class="value">${course_info['org']}</span>
</span>
<span class="course-num metadata-item">
<span class="label">${_("Course Number:")}</span>
<span class="value">${course_info['number']}</span>
</span>
<span class="course-run metadata-item">
<span class="label">${_("Course Run:")}</span> <span class="value">${course_info['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 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 <a href="#" class="action-reload">refresh it</a> to update the course list. The new course will need some manual configuration.')}</p>
</div>
</li>
%endif
<!-- - - - -->
<!-- STATE: re-run has error -->
%if course_info['is_failed']:
<li class="wrapper-course has-status" data-course-key="${course_info['course_key']}">
<div class="course-item course-rerun has-error">
<div class="course-details" href="#">
<h3 class="course-title">${course_info['display_name']}</h3>
<div class="course-metadata">
<span class="course-org metadata-item">
<span class="label">${_("Organization:")}</span> <span class="value">${course_info['org']}</span>
</span>
<span class="course-num metadata-item">
<span class="label">${_("Course Number:")}</span>
<span class="value">${course_info['number']}</span>
</span>
<span class="course-run metadata-item">
<span class="label">${_("Course Run:")}</span> <span class="value">${course_info['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 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="${course_info['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 ''):
<li class="course-item">
<a class="course-link" href="${url}">
<h3 class="course-title">${course}</h3>
%for course_info in sorted(courses, key=lambda s: s['display_name'].lower() if s['display_name'] is not None else ''):
<li class="course-item" data-course-key="${course_info['course_key']}">
<a class="course-link" href="${course_info['url']}">
<h3 class="course-title">${course_info['display_name']}</h3>
<div class="course-metadata">
<span class="course-org metadata-item">
<span class="label">${_("Organization:")}</span> <span class="value">${org}</span>
<span class="label">${_("Organization:")}</span> <span class="value">${course_info['org']}</span>
</span>
<span class="course-num metadata-item">
<span class="label">${_("Course Number:")}</span>
<span class="value">${num}</span>
<span class="value">${course_info['number']}</span>
</span>
<span class="course-run metadata-item">
<span class="label">${_("Course Run:")}</span> <span class="value">${run}</span>
<span class="label">${_("Course Run:")}</span> <span class="value">${course_info['run']}</span>
</span>
</div>
</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 allow_course_reruns and rerun_creator_status and course_creator_status=='granted':
<li class="action action-rerun">
<a href="${course_info['rerun_link']}" class="button rerun-button">${_("Re-run Course")}</a>
</li>
% endif
<li class="action action-view">
<a href="${course_info['lms_link']}" rel="external" class="button view-button">${_("View Live")}</a>
</li>
</ul>
</li>
%endfor
% if allow_course_reruns and rerun_creator_status and course_creator_status=='granted':
<script type="text/javascript">
$('.course-item').addClass('can-rerun');
</script>
% endif
</ul>
</div>
......
<div id="page-alert">
<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 has been changed to a future date); 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="dummy_dismiss_url">
<i class="icon icon-remove-sign"></i>
<span class="button-copy">Dimiss</span>
</a>
</li>
</ul>
</div>
</div>
</div>
\ No newline at end of file
<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">edX Open_DemoX 2014_T1</span>
<span class="course-original-title">edX Demonstration Course</span>
</h2>
</header>
</div>
<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>
<div class="wrapper-rerun-course">
<form class="form-create rerun-course course-info" id="rerun-course-form"
name="rerun-course-form">
<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="edX Demonstration Course"
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="edX"
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="Open_DemoX" 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="false" class="allow-unicode-course-id"/>
</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>
</section>
</div>
</div>
</div>
\ No newline at end of file
<div class="wrapper-mast wrapper">
<header class="mast has-actions">
<h1 class="page-header">My Courses</h1>
<nav class="nav-actions">
<h3 class="sr">Page Actions</h3>
<ul>
<li class="nav-item">
<a href="#" class="button new-button new-course-button"><i class="icon-plus icon-inline"></i>
New Course</a>
</li>
</ul>
</nav>
</header>
</div>
<div class="wrapper-content wrapper">
<section class="content">
<article class="content-primary" role="main">
<div class="introduction">
<h2 class="title">Welcome, user!</h2>
<div class="copy">
<p>Here are all of the courses you currently have access to in Studio:</p>
</div>
</div>
<div class="wrapper-create-element wrapper-create-course">
<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>
</div>
</div>
<div class="wrapper-form">
<h3 class="title">Create a New Course</h3>
<fieldset>
<legend class="sr">Required Information to Create a New Course</legend>
<ol class="list-input">
<li class="field text required" id="field-course-name">
<label for="new-course-name">Course Name</label>
<input class="new-course-name" id="new-course-name" type="text" name="new-course-name" aria-required="true" placeholder="e.g. Introduction to Computer Science" />
<span class="tip">The public display name for your course. This cannot be changed, but you can set a different display name in Advanced Settings later.</span>
<span class="tip tip-error is-hiding"></span>
</li>
<li class="field text required" id="field-organization">
<label for="new-course-org">Organization</label>
<input class="new-course-org" id="new-course-org" type="text" name="new-course-org" aria-required="true" placeholder="e.g. UniversityX or OrganizationX" />
<span class="tip">The name of the organization sponsoring the course. <strong>Note: This is part of your course URL, so no spaces or special characters are allowed.</strong> This cannot be changed, but you can set a different display name in Advanced Settings later.</span>
<span class="tip tip-error is-hiding"></span>
</li>
<li class="field text required" id="field-course-number">
<label for="new-course-number">Course Number</label>
<input class="new-course-number" id="new-course-number" type="text" name="new-course-number" aria-required="true" placeholder="e.g. CS101" />
<span class="tip">The unique number that identifies your course within your organization. <strong>Note: This is part of your course URL, so no spaces or special characters are allowed and it cannot be changed.</strong></span>
<span class="tip tip-error is-hiding"></span>
</li>
<li class="field text required" id="field-course-run">
<label for="new-course-run">Course Run</label>
<input class="new-course-run" id="new-course-run" type="text" name="new-course-run" aria-required="true"placeholder="e.g. 2014_T1" />
<span class="tip">The term in which your course will run. <strong>Note: This is part of your course URL, so no spaces or special characters are allowed and it cannot be changed.</strong></span>
<span class="tip tip-error is-hiding"></span>
</li>
</ol>
</fieldset>
</div>
<div class="actions">
<input type="hidden" value="true" class="allow-unicode-course-id" />
<input type="submit" value="Create" class="action action-primary new-course-save" />
<input type="button" value="Cancel" class="action action-secondary action-cancel new-course-cancel" />
</div>
</form>
</div>
<!-- STATE: processing courses -->
<div class="courses courses-processing">
<h3 class="title">Courses Being Processed</h3>
<ul class="list-courses">
<!-- STATE: re-run is processing -->
<li class="wrapper-course has-status" data-test-unsucceeded="edX/DM101/2014">
<div class="course-item course-rerun is-processing">
<div class="course-details" href="#">
<h3 class="course-title">Demo Course</h3>
<div class="course-metadata">
<span class="course-org metadata-item">
<span class="label">Organization:</span> <span class="value">edX</span>
</span>
<span class="course-num metadata-item">
<span class="label">Course Number:</span>
<span class="value">DM101</span>
</span>
<span class="course-run metadata-item">
<span class="label">Course Run:</span> <span class="value">2014</span>
</span>
</div>
</div>
<dl class="course-status">
<dt class="label sr">This re-run processing status:</dt>
<dd class="value">
<i class="icon 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>
<!-- - - - -->
<!-- STATE: re-run has error -->
<li class="wrapper-course has-status" data-test-unsucceeded="edX/DM102/2014">
<div class="course-item course-rerun has-error">
<div class="course-details" href="#">
<h3 class="course-title">Demo Course 2</h3>
<div class="course-metadata">
<span class="course-org metadata-item">
<span class="label">Organization:</span> <span class="value">edX</span>
</span>
<span class="course-num metadata-item">
<span class="label">Course Number:</span>
<span class="value">DM102</span>
</span>
<span class="course-run metadata-item">
<span class="label">Course Run:</span> <span class="value">2014</span>
</span>
</div>
</div>
<dl class="course-status">
<dt class="label sr">This re-run processing status:</dt>
<dd class="value">
<i class="icon 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="dummy_dismiss_url">
<i class="icon icon-remove-sign"></i>
<span class="button-copy">Dismiss</span>
</a>
</li>
</ul>
</div>
</li>
</ul>
</div>
</article>
</section>
</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:
......
......@@ -75,7 +75,10 @@ require(["js/models/section", "js/collections/textbook", "js/views/list_textbook
<div class="bit">
<h3 class="title-3">${_("What if my book isn't divided into chapters?")}</h3>
<p>${_("If your textbook doesn't have individual chapters, you can upload the entire text as a single chapter and enter a name of your choice in the Chapter Name field.")}</p>
<p><a href="${get_online_help_info(online_help_token())['doc_url']}" target="_blank">${_("Learn More")}</a></p>
</div>
<div class="bit external-help">
<a href="${get_online_help_info(online_help_token())['doc_url']}" target="_blank" class="button external-help-button">${_("Learn More")}</a>
</div>
</aside>
</section>
......
......@@ -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, blank=True),
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']
......@@ -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="", blank=True)
# 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")
......
......@@ -97,7 +97,7 @@ def replace_static_urls(text, data_directory, course_id=None, static_asset_path=
Replace /static/$stuff urls either with their correct url as generated by collectstatic,
(/static/$md5_hashed_stuff) or by the course-specific content static url
/static/$course_data_dir/$stuff, or, if course_namespace is not None, by the
correct url in the contentstore (c4x://)
correct url in the contentstore (/c4x/.. or /asset-loc:..)
text: The source text to do the substitution in
data_directory: The directory in which course data is stored
......
......@@ -185,7 +185,7 @@ def add_staff_markup(user, has_instructor_access, block, view, frag, context):
# TODO: make this more general, eg use an XModule attribute instead
if isinstance(block, VerticalModule) and (not context or not context.get('child_of_vertical', False)):
# check that the course is a mongo backed Studio course before doing work
is_mongo_course = modulestore().get_modulestore_type(block.location.course_key) == ModuleStoreEnum.Type.mongo
is_mongo_course = modulestore().get_modulestore_type(block.location.course_key) != ModuleStoreEnum.Type.xml
is_studio_course = block.course_edit_method == "Studio"
if is_studio_course and is_mongo_course:
......
......@@ -64,9 +64,6 @@ class StaticContent(object):
def get_id(self):
return self.location
def get_url_path(self):
return self.location.to_deprecated_string()
@property
def data(self):
return self._data
......@@ -108,7 +105,9 @@ class StaticContent(object):
assert(isinstance(course_key, CourseKey))
placeholder_id = uuid.uuid4().hex
# create a dummy asset location with a fake but unique name. strip off the name, and return it
url_path = unicode(course_key.make_asset_key('asset', placeholder_id).for_branch(None))
url_path = StaticContent.serialize_asset_key_with_slash(
course_key.make_asset_key('asset', placeholder_id).for_branch(None)
)
return url_path.replace(placeholder_id, '')
@staticmethod
......@@ -133,7 +132,7 @@ class StaticContent(object):
# Generate url of urlparse.path component
scheme, netloc, orig_path, params, query, fragment = urlparse(path)
loc = StaticContent.compute_location(course_id, orig_path)
loc_url = loc.to_deprecated_string()
loc_url = StaticContent.serialize_asset_key_with_slash(loc)
# parse the query params for "^/static/" and replace with the location url
orig_query = parse_qsl(query)
......@@ -144,7 +143,7 @@ class StaticContent(object):
course_id,
query_value[len('/static/'):],
)
new_query_url = new_query.to_deprecated_string()
new_query_url = StaticContent.serialize_asset_key_with_slash(new_query)
new_query_list.append((query_name, new_query_url))
else:
new_query_list.append((query_name, query_value))
......@@ -155,6 +154,17 @@ class StaticContent(object):
def stream_data(self):
yield self._data
@staticmethod
def serialize_asset_key_with_slash(asset_key):
"""
Legacy code expects the serialized asset key to start w/ a slash; so, do that in one place
:param asset_key:
"""
url = unicode(asset_key)
if not url.startswith('/'):
url = '/' + url # TODO - re-address this once LMS-11198 is tackled.
return url
class StaticContentStream(StaticContent):
def __init__(self, loc, name, content_type, stream, last_modified_at=None, thumbnail_location=None, import_path=None,
......
......@@ -66,7 +66,7 @@ class MongoContentStore(ContentStore):
self.delete(content_id) # delete is a noop if the entry doesn't exist; so, don't waste time checking
thumbnail_location = content.thumbnail_location.to_deprecated_list_repr() if content.thumbnail_location else None
with self.fs.new_file(_id=content_id, filename=content.get_url_path(), content_type=content.content_type,
with self.fs.new_file(_id=content_id, filename=unicode(content.location), content_type=content.content_type,
displayname=content.name, content_son=content_son,
thumbnail_location=thumbnail_location,
import_path=content.import_path,
......
......@@ -393,15 +393,13 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
if source_modulestore == dest_modulestore:
return source_modulestore.clone_course(source_course_id, dest_course_id, user_id, fields, **kwargs)
# ensure super's only called once. The delegation above probably calls it; so, don't move
# the invocation above the delegation call
super(MixedModuleStore, self).clone_course(source_course_id, dest_course_id, user_id, fields, **kwargs)
if dest_modulestore.get_modulestore_type() == ModuleStoreEnum.Type.split:
split_migrator = SplitMigrator(dest_modulestore, source_modulestore)
split_migrator.migrate_mongo_course(
source_course_id, user_id, dest_course_id.org, dest_course_id.course, dest_course_id.run, fields, **kwargs
)
# the super handles assets and any other necessities
super(MixedModuleStore, self).clone_course(source_course_id, dest_course_id, user_id, fields, **kwargs)
@strip_key
def create_item(self, user_id, course_key, block_type, block_id=None, fields=None, **kwargs):
......
......@@ -952,11 +952,10 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
In split, other than copying the assets, this is cheap as it merely creates a new version of the
existing course.
"""
super(SplitMongoModuleStore, self).clone_course(source_course_id, dest_course_id, user_id, fields, **kwargs)
source_index = self.get_course_index_info(source_course_id)
if source_index is None:
raise ItemNotFoundError("Cannot find a course at {0}. Aborting".format(source_course_id))
return self.create_course(
new_course = self.create_course(
dest_course_id.org, dest_course_id.course, dest_course_id.run,
user_id,
fields=fields,
......@@ -965,6 +964,9 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
skip_auto_publish=True,
**kwargs
)
# don't copy assets until we create the course in case something's awry
super(SplitMongoModuleStore, self).clone_course(source_course_id, dest_course_id, user_id, fields, **kwargs)
return new_course
DEFAULT_ROOT_BLOCK_ID = 'course'
def create_course(
......@@ -1794,7 +1796,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):
......
......@@ -118,7 +118,7 @@ def course_image_url(course):
url += '/images/course_image.jpg'
else:
loc = StaticContent.compute_location(course.id, course.course_image)
url = loc.to_deprecated_string()
url = StaticContent.serialize_asset_key_with_slash(loc)
return url
......@@ -360,14 +360,15 @@ def get_cms_block_link(block, page):
return u"//{}/{}/{}".format(settings.CMS_BASE, page, block.location)
def get_studio_url(course_key, page):
def get_studio_url(course, page):
"""
Get the Studio URL of the page that is passed in.
Args:
course (CourseDescriptor)
"""
assert(isinstance(course_key, CourseKey))
course = get_course_by_id(course_key)
is_studio_course = course.course_edit_method == "Studio"
is_mongo_course = modulestore().get_modulestore_type(course_key) == ModuleStoreEnum.Type.mongo
is_mongo_course = modulestore().get_modulestore_type(course.id) != ModuleStoreEnum.Type.xml
studio_link = None
if is_studio_course and is_mongo_course:
studio_link = get_cms_course_link(course, page)
......
......@@ -311,7 +311,7 @@ def index(request, course_id, chapter=None, section=None,
u' far, should have gotten a course module for this user')
return redirect(reverse('about_course', args=[course_key.to_deprecated_string()]))
studio_url = get_studio_url(course_key, 'course')
studio_url = get_studio_url(course, 'course')
context = {
'csrf': csrf(request)['csrf_token'],
......@@ -419,7 +419,7 @@ def index(request, course_id, chapter=None, section=None,
context['section_title'] = section_descriptor.display_name_with_default
else:
# section is none, so display a message
studio_url = get_studio_url(course_key, 'course')
studio_url = get_studio_url(course, 'course')
prev_section = get_current_child(chapter_module)
if prev_section is None:
# Something went wrong -- perhaps this chapter has no sections visible to the user
......@@ -553,7 +553,7 @@ def course_info(request, course_id):
staff_access = has_access(request.user, 'staff', course)
masq = setup_masquerade(request, staff_access) # allow staff to toggle masquerade on info page
reverifications = fetch_reverify_banner_info(request, course_key)
studio_url = get_studio_url(course_key, 'course_info')
studio_url = get_studio_url(course, 'course_info')
context = {
'request': request,
......@@ -655,7 +655,7 @@ def course_about(request, course_id):
course = get_course_with_access(request.user, 'see_exists', course_key)
registered = registered_for_course(course, request.user)
staff_access = has_access(request.user, 'staff', course)
studio_url = get_studio_url(course_key, 'settings/details')
studio_url = get_studio_url(course, 'settings/details')
if has_access(request.user, 'load', course):
course_target = reverse('info', args=[course.id.to_deprecated_string()])
......@@ -812,7 +812,7 @@ def _progress(request, course_key, student_id):
student = User.objects.prefetch_related("groups").get(id=student.id)
courseware_summary = grades.progress_summary(student, request, course)
studio_url = get_studio_url(course_key, 'settings/grading')
studio_url = get_studio_url(course, 'settings/grading')
grade_summary = grades.grade(student, request, course)
if courseware_summary is None:
......
......@@ -180,7 +180,7 @@ if settings.WIKI_ENABLED:
# never be returned by a reverse() so they come after the other url patterns
url(r'^courses/{}/course_wiki/?$'.format(settings.COURSE_ID_PATTERN),
'course_wiki.views.course_wiki_redirect', name="course_wiki"),
url(r'^courses/(?:[^/]+/[^/]+/[^/]+)/wiki/', include(wiki_pattern())),
url(r'^courses/{}/wiki/'.format(settings.COURSE_ID_PATTERN), include(wiki_pattern())),
)
if settings.COURSEWARE_ENABLED:
......
......@@ -10,6 +10,7 @@ bleach==1.4
html5lib==0.999
boto==2.13.3
celery==3.0.19
cssselect==0.9.1
dealer==0.2.3
distribute>=0.6.28, <0.7
django-babel-underscore==0.1.0
......
......@@ -18,7 +18,7 @@
-e git+https://github.com/jazkarta/edx-jsme.git@813079fd5218ed275248d2a1fcae2fcbf20a0838#egg=edx-jsme
# Our libraries:
-e git+https://github.com/edx/XBlock.git@f0e53538be7ce90584a03cc7dd3f06bd43e12ac2#egg=XBlock
-e git+https://github.com/edx/XBlock.git@de7fde7f27b1f4a0bb7b6ea9041cc893021be287#egg=XBlock
-e git+https://github.com/edx/codejail.git@71f5c5616e2a73ae8cecd1ff2362774a773d3665#egg=codejail
-e git+https://github.com/edx/diff-cover.git@v0.5.0#egg=diff_cover
-e git+https://github.com/edx/js-test-tool.git@v0.1.5#egg=js_test_tool
......
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