Commit 8573ac39 by chrisndodge

Merge pull request #510 from edx/feature/cdodge/allow-course-run-in-course-create

Feature/cdodge/allow course run in course create
parents 8f976082 b6777118
...@@ -9,6 +9,9 @@ Studio: Send e-mails to new Studio users (on edge only) when their course creato ...@@ -9,6 +9,9 @@ Studio: Send e-mails to new Studio users (on edge only) when their course creato
status has changed. This will not be in use until the course creator table status has changed. This will not be in use until the course creator table
is enabled. is enabled.
Studio: Added improvements to Course Creation: richer error messaging, tip
text, and fourth field for course run.
LMS: Added user preferences (arbitrary user/key/value tuples, for which LMS: Added user preferences (arbitrary user/key/value tuples, for which
which user/key is unique) and a REST API for reading users and which user/key is unique) and a REST API for reading users and
preferences. Access to the REST API is restricted by use of the preferences. Access to the REST API is restricted by use of the
......
...@@ -132,10 +132,12 @@ def create_studio_user( ...@@ -132,10 +132,12 @@ def create_studio_user(
def fill_in_course_info( def fill_in_course_info(
name='Robot Super Course', name='Robot Super Course',
org='MITx', org='MITx',
num='999'): num='101',
run='2013_Spring'):
world.css_fill('.new-course-name', name) world.css_fill('.new-course-name', name)
world.css_fill('.new-course-org', org) world.css_fill('.new-course-org', org)
world.css_fill('.new-course-number', num) world.css_fill('.new-course-number', num)
world.css_fill('.new-course-run', run)
def log_into_studio( def log_into_studio(
......
...@@ -603,6 +603,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -603,6 +603,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
'org': 'MITx', 'org': 'MITx',
'number': '999', 'number': '999',
'display_name': 'Robot Super Course', 'display_name': 'Robot Super Course',
'run': '2013_Spring'
} }
module_store = modulestore('direct') module_store = modulestore('direct')
...@@ -611,12 +612,12 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -611,12 +612,12 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
resp = self.client.post(reverse('create_new_course'), course_data) resp = self.client.post(reverse('create_new_course'), course_data)
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
data = parse_json(resp) data = parse_json(resp)
self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course') self.assertEqual(data['id'], 'i4x://MITx/999/course/2013_Spring')
content_store = contentstore() content_store = contentstore()
source_location = CourseDescriptor.id_to_location('edX/toy/2012_Fall') source_location = CourseDescriptor.id_to_location('edX/toy/2012_Fall')
dest_location = CourseDescriptor.id_to_location('MITx/999/Robot_Super_Course') dest_location = CourseDescriptor.id_to_location('MITx/999/2013_Spring')
clone_course(module_store, content_store, source_location, dest_location) clone_course(module_store, content_store, source_location, dest_location)
...@@ -1015,6 +1016,7 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -1015,6 +1016,7 @@ class ContentStoreTest(ModuleStoreTestCase):
'org': 'MITx', 'org': 'MITx',
'number': '999', 'number': '999',
'display_name': 'Robot Super Course', 'display_name': 'Robot Super Course',
'run': '2013_Spring'
} }
def tearDown(self): def tearDown(self):
...@@ -1026,24 +1028,30 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -1026,24 +1028,30 @@ class ContentStoreTest(ModuleStoreTestCase):
"""Test new course creation - happy path""" """Test new course creation - happy path"""
self.assert_created_course() self.assert_created_course()
def assert_created_course(self): def assert_created_course(self, number_suffix=None):
""" """
Checks that the course was created properly. Checks that the course was created properly.
""" """
resp = self.client.post(reverse('create_new_course'), self.course_data) test_course_data = {}
test_course_data.update(self.course_data)
if number_suffix:
test_course_data['number'] = '{0}_{1}'.format(test_course_data['number'], number_suffix)
resp = self.client.post(reverse('create_new_course'), test_course_data)
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
data = parse_json(resp) data = parse_json(resp)
self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course') self.assertNotIn('ErrMsg', data)
self.assertEqual(data['id'], 'i4x://MITx/{0}/course/2013_Spring'.format(test_course_data['number']))
return test_course_data
def test_create_course_check_forum_seeding(self): def test_create_course_check_forum_seeding(self):
"""Test new course creation and verify forum seeding """ """Test new course creation and verify forum seeding """
self.assert_created_course() test_course_data = self.assert_created_course(number_suffix=uuid4().hex)
self.assertTrue(are_permissions_roles_seeded('MITx/999/Robot_Super_Course')) self.assertTrue(are_permissions_roles_seeded('MITx/{0}/2013_Spring'.format(test_course_data['number'])))
def test_create_course_duplicate_course(self): def test_create_course_duplicate_course(self):
"""Test new course creation - error path""" """Test new course creation - error path"""
self.client.post(reverse('create_new_course'), self.course_data) self.client.post(reverse('create_new_course'), self.course_data)
self.assert_course_creation_failed('There is already a course defined with this name.') self.assert_course_creation_failed('There is already a course defined with the same organization, course number, and course run. Please change either organization or course number to be unique.')
def assert_course_creation_failed(self, error_message): def assert_course_creation_failed(self, error_message):
""" """
...@@ -1058,8 +1066,9 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -1058,8 +1066,9 @@ class ContentStoreTest(ModuleStoreTestCase):
"""Test new course creation - error path""" """Test new course creation - error path"""
self.client.post(reverse('create_new_course'), self.course_data) self.client.post(reverse('create_new_course'), self.course_data)
self.course_data['display_name'] = 'Robot Super Course Two' self.course_data['display_name'] = 'Robot Super Course Two'
self.course_data['run'] = '2013_Summer'
self.assert_course_creation_failed('There is already a course defined with the same organization and course number.') self.assert_course_creation_failed('There is already a course defined with the same organization and course number. Please change at least one field to be unique.')
def test_create_course_with_bad_organization(self): def test_create_course_with_bad_organization(self):
"""Test new course creation - error path for bad organization name""" """Test new course creation - error path for bad organization name"""
......
...@@ -3,6 +3,7 @@ Views related to operations on course objects ...@@ -3,6 +3,7 @@ Views related to operations on course objects
""" """
import json import json
import random import random
from django.utils.translation import ugettext as _
import string # pylint: disable=W0402 import string # pylint: disable=W0402
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
...@@ -101,12 +102,13 @@ def create_new_course(request): ...@@ -101,12 +102,13 @@ def create_new_course(request):
org = request.POST.get('org') org = request.POST.get('org')
number = request.POST.get('number') number = request.POST.get('number')
display_name = request.POST.get('display_name') display_name = request.POST.get('display_name')
run = request.POST.get('run')
try: try:
dest_location = Location('i4x', org, number, 'course', Location.clean(display_name)) dest_location = Location('i4x', org, number, 'course', run)
except InvalidLocationError as error: except InvalidLocationError as error:
return JsonResponse({ return JsonResponse({
"ErrMsg": "Unable to create course '{name}'.\n\n{err}".format( "ErrMsg": _("Unable to create course '{name}'.\n\n{err}").format(
name=display_name, err=error.message)}) name=display_name, err=error.message)})
# see if the course already exists # see if the course already exists
...@@ -116,12 +118,24 @@ def create_new_course(request): ...@@ -116,12 +118,24 @@ def create_new_course(request):
except ItemNotFoundError: except ItemNotFoundError:
pass pass
if existing_course is not None: if existing_course is not None:
return JsonResponse({'ErrMsg': 'There is already a course defined with this name.'}) return JsonResponse(
{
'ErrMsg': _('There is already a course defined with the same organization, course number, and course run. Please change either organization or course number to be unique.'),
'OrgErrMsg': _('Please change either the organization or course number so that it is unique.'),
'CourseErrMsg': _('Please change either the organization or course number so that it is unique.'),
}
)
course_search_location = ['i4x', dest_location.org, dest_location.course, 'course', None] course_search_location = ['i4x', dest_location.org, dest_location.course, 'course', None]
courses = modulestore().get_items(course_search_location) courses = modulestore().get_items(course_search_location)
if len(courses) > 0: if len(courses) > 0:
return JsonResponse({'ErrMsg': 'There is already a course defined with the same organization and course number.'}) return JsonResponse(
{
'ErrMsg': _('There is already a course defined with the same organization and course number. Please change at least one field to be unique.'),
'OrgErrMsg': _('Please change either the organization or course number so that it is unique.'),
'CourseErrMsg': _('Please change either the organization or course number so that it is unique.'),
}
)
# instantiate the CourseDescriptor and then persist it # instantiate the CourseDescriptor and then persist it
# note: no system to pass # note: no system to pass
......
...@@ -597,11 +597,9 @@ function cancelNewSection(e) { ...@@ -597,11 +597,9 @@ function cancelNewSection(e) {
function addNewCourse(e) { function addNewCourse(e) {
e.preventDefault(); e.preventDefault();
$('.new-course-button').addClass('disabled'); $('.new-course-button').addClass('is-disabled');
$(e.target).addClass('disabled'); var $newCourse = $('.wrapper-create-course').addClass('is-shown');
var $newCourse = $($('#new-course-template').html());
var $cancelButton = $newCourse.find('.new-course-cancel'); var $cancelButton = $newCourse.find('.new-course-cancel');
$('.courses').prepend($newCourse);
$newCourse.find('.new-course-name').focus().select(); $newCourse.find('.new-course-name').focus().select();
$newCourse.find('form').bind('submit', saveNewCourse); $newCourse.find('form').bind('submit', saveNewCourse);
$cancelButton.bind('click', cancelNewCourse); $cancelButton.bind('click', cancelNewCourse);
...@@ -613,41 +611,97 @@ function addNewCourse(e) { ...@@ -613,41 +611,97 @@ function addNewCourse(e) {
function saveNewCourse(e) { function saveNewCourse(e) {
e.preventDefault(); e.preventDefault();
var $newCourse = $(this).closest('.new-course'); var $newCourseForm = $(this).closest('#create-course-form');
var org = $newCourse.find('.new-course-org').val(); var display_name = $newCourseForm.find('.new-course-name').val();
var number = $newCourse.find('.new-course-number').val(); var org = $newCourseForm.find('.new-course-org').val();
var display_name = $newCourse.find('.new-course-name').val(); var number = $newCourseForm.find('.new-course-number').val();
var run = $newCourseForm.find('.new-course-run').val();
if (org == '' || number == '' || display_name == '') { var required_field_text = gettext('Required field');
alert(gettext('You must specify all fields in order to create a new course.'));
return; var display_name_errMsg = (display_name === '') ? required_field_text : null;
var org_errMsg = (org === '') ? required_field_text : null;
var number_errMsg = (number === '') ? required_field_text : null;
var run_errMsg = (run === '') ? required_field_text : null;
var bInErr = (display_name_errMsg || org_errMsg || number_errMsg || run_errMsg);
// check for suitable encoding
if (!bInErr) {
var encoding_errMsg = gettext('Please do not use any spaces or special characters in this field.');
if (encodeURIComponent(org) != org)
org_errMsg = encoding_errMsg;
if (encodeURIComponent(number) != number)
number_errMsg = encoding_errMsg;
if (encodeURIComponent(run) != run)
run_errMsg = encoding_errMsg;
bInErr = (org_errMsg || number_errMsg || run_errMsg);
} }
var header_err_msg = (bInErr) ? gettext('Please correct the fields below.') : null;
var setNewCourseErrMsgs = function(header_err_msg, display_name_errMsg, org_errMsg, number_errMsg, run_errMsg) {
if (header_err_msg) {
$('.wrapper-create-course').addClass('has-errors');
$('.wrap-error').addClass('is-shown');
$('#course_creation_error').html('<p>' + header_err_msg + '</p>');
} else {
$('.wrap-error').removeClass('is-shown');
$('#course_creation_error').html('');
}
var setNewCourseFieldInErr = function(el, msg) {
el.children('.tip-error').remove();
if (msg !== null && msg !== '') {
el.addClass('error');
el.append('<span class="tip tip-error">' + msg + '</span>');
} else {
el.removeClass('error');
}
};
setNewCourseFieldInErr($('#field-course-name'), display_name_errMsg);
setNewCourseFieldInErr($('#field-organization'), org_errMsg);
setNewCourseFieldInErr($('#field-course-number'), number_errMsg);
setNewCourseFieldInErr($('#field-course-run'), run_errMsg);
};
setNewCourseErrMsgs(header_err_msg, display_name_errMsg, org_errMsg, number_errMsg, run_errMsg);
if (bInErr)
return;
analytics.track('Created a Course', { analytics.track('Created a Course', {
'org': org, 'org': org,
'number': number, 'number': number,
'display_name': display_name 'display_name': display_name,
'run': run
}); });
$.post('/create_new_course', { $.post('/create_new_course', {
'org': org, 'org': org,
'number': number, 'number': number,
'display_name': display_name 'display_name': display_name,
}, 'run': run
},
function(data) { function(data) {
if (data.id != undefined) { if (data.id !== undefined) {
window.location = '/' + data.id.replace(/.*:\/\//, ''); window.location = '/' + data.id.replace(/.*:\/\//, '');
} else if (data.ErrMsg != undefined) { } else if (data.ErrMsg !== undefined) {
alert(data.ErrMsg); var orgErrMsg = (data.OrgErrMsg !== undefined) ? data.OrgErrMsg : null;
var courseErrMsg = (data.CourseErrMsg !== undefined) ? data.CourseErrMsg : null;
setNewCourseErrMsgs(data.ErrMsg, null, orgErrMsg, courseErrMsg, null);
}
} }
}); );
} }
function cancelNewCourse(e) { function cancelNewCourse(e) {
e.preventDefault(); e.preventDefault();
$('.new-course-button').removeClass('disabled'); $('.new-course-button').removeClass('is-disabled');
$(this).parents('section.new-course').remove(); $('.wrapper-create-course').removeClass('is-shown');
} }
function addNewSubsection(e) { function addNewSubsection(e) {
......
...@@ -23,6 +23,13 @@ body.dashboard { ...@@ -23,6 +23,13 @@ body.dashboard {
} }
// yes we have no boldness today - need to fix the resets
body strong,
body b {
font-weight: 700;
}
// known things to do (paint the fence, sand the floor, wax on/off)
// ==================== // ====================
......
...@@ -93,7 +93,6 @@ form { ...@@ -93,7 +93,6 @@ form {
} }
} }
// ELEM: form wrapper // ELEM: form wrapper
.wrapper-create-element { .wrapper-create-element {
height: 0; height: 0;
...@@ -117,10 +116,6 @@ form { ...@@ -117,10 +116,6 @@ form {
// form styling for creating a new content item (course, user, textbook) // form styling for creating a new content item (course, user, textbook)
form[class^="create-"] { form[class^="create-"] {
@extend .ui-window; @extend .ui-window;
@include box-sizing(border-box);
border-radius: 2px;
width: 100%;
background: $white;
.title { .title {
@extend .t-title4; @extend .t-title4;
...@@ -171,8 +166,8 @@ form[class^="create-"] { ...@@ -171,8 +166,8 @@ form[class^="create-"] {
input, textarea { input, textarea {
@include transition(all $tmg-f2 ease-in-out 0s);
@extend .t-copy-base; @extend .t-copy-base;
@include transition(all $tmg-f2 ease-in-out 0s);
height: 100%; height: 100%;
width: 100%; width: 100%;
padding: ($baseline/2); padding: ($baseline/2);
...@@ -185,6 +180,10 @@ form[class^="create-"] { ...@@ -185,6 +180,10 @@ form[class^="create-"] {
width: 25%; width: 25%;
} }
/*@include placeholder {
color: $gray-l3;
}*/
&:focus { &:focus {
+ .tip { + .tip {
...@@ -295,32 +294,26 @@ form[class^="create-"] { ...@@ -295,32 +294,26 @@ form[class^="create-"] {
padding: ($baseline*0.75) ($baseline*1.5); padding: ($baseline*0.75) ($baseline*1.5);
background: $gray-l6; background: $gray-l6;
.action-primary { .action {
@include blue-button; @include transition(all $tmg-f2 linear 0s);
@extend .t-action2;
@include transition(all .15s);
display: inline-block; display: inline-block;
padding: ($baseline/5) $baseline; padding: ($baseline/5) $baseline;
font-weight: 600; font-weight: 600;
text-transform: uppercase; text-transform: uppercase;
} }
.action-primary {
@include blue-button;
@extend .t-action2;
}
.action-secondary { .action-secondary {
@include grey-button; @include grey-button;
@extend .t-action2; @extend .t-action2;
@include transition(all .15s);
display: inline-block;
padding: ($baseline/5) $baseline;
font-weight: 600;
text-transform: uppercase;
} }
} }
} }
// ==================== // ====================
// forms - grandfathered // forms - grandfathered
......
...@@ -358,22 +358,30 @@ body.dashboard { ...@@ -358,22 +358,30 @@ body.dashboard {
} }
} }
.new-course {
@include clearfix();
padding: ($baseline*0.75) ($baseline*1.25);
margin-top: $baseline;
border-radius: 3px;
border: 1px solid $gray;
background: $white;
box-shadow: 0 1px 2px rgba(0, 0, 0, .1);
.title { // ELEM: new user form
@extend .t-title4; .wrapper-create-course {
font-weight: 600;
margin-bottom: ($baseline/2); // CASE: when form is animating
border-bottom: 1px solid $gray-l3; &.animate {
padding-bottom: ($baseline/2);
} // STATE: shown
&.is-shown {
height: ($baseline*26);
// STATE: errors
&.has-errors {
height: ($baseline*33);
}
}
}
}
// ====================
// course listings
.create-course {
.row { .row {
@include clearfix(); @include clearfix();
...@@ -389,10 +397,6 @@ body.dashboard { ...@@ -389,10 +397,6 @@ body.dashboard {
margin-right: 4%; margin-right: 4%;
} }
.course-info {
width: 600px;
}
label { label {
@extend .t-title7; @extend .t-title7;
display: block; display: block;
...@@ -401,7 +405,8 @@ body.dashboard { ...@@ -401,7 +405,8 @@ body.dashboard {
.new-course-org, .new-course-org,
.new-course-number, .new-course-number,
.new-course-name { .new-course-name,
.new-course-run {
width: 100%; width: 100%;
} }
...@@ -421,5 +426,25 @@ body.dashboard { ...@@ -421,5 +426,25 @@ body.dashboard {
.item-details { .item-details {
padding-bottom: 0; padding-bottom: 0;
} }
.wrap-error {
@include transition(all $tmg-f2 ease 0s);
height: 0;
overflow: hidden;
opacity: 0;
}
.wrap-error.is-shown {
height: 65px;
opacity: 1;
}
.message-status {
display: block;
margin-bottom: 0;
padding: ($baseline*.5) ($baseline*1.5) 8px ($baseline*1.5);
font-weight: bold;
}
} }
} }
...@@ -36,36 +36,6 @@ ...@@ -36,36 +36,6 @@
</script> </script>
</%block> </%block>
<%block name="header_extras">
<script type="text/template" id="new-course-template">
<section class="new-course">
<h3 class="title">${_("Create a New Course:")}</h3>
<div class="item-details">
<form class="course-info">
<div class="row">
<label>${_("Course Name")}</label>
<input type="text" class="new-course-name" />
</div>
<div class="row">
<div class="column">
<label>${_("Organization")}</label>
<input type="text" class="new-course-org" />
</div>
<div class="column">
<label>${_("Course Number")}</label>
<input type="text" class="new-course-number" />
</div>
</div>
<div class="row">
<input type="submit" value="${_('Save')}" class="new-course-save"/>
<input type="button" value="${_('Cancel')}" class="new-course-cancel" />
</div>
</form>
</div>
</section>
</script>
</%block>
<%block name="content"> <%block name="content">
<div class="wrapper-mast wrapper"> <div class="wrapper-mast wrapper">
<header class="mast has-actions"> <header class="mast has-actions">
...@@ -109,6 +79,57 @@ ...@@ -109,6 +79,57 @@
%endif %endif
</div> </div>
% 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">
<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 field-inline 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 tip-stacked">${_("The public display name for your course.")}</span>
</li>
<li class="field field-inline 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. MITX or IMF')}" />
<span class="tip tip-stacked">${_("The name of the organization sponsoring the course")} - <strong>${_("Note: No spaces or special characters are allowed. This cannot be changed.")}</strong></span>
</li>
<li class="field field-inline 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 tip-stacked">${_("The unique number that identifies your course within your organization")} - <strong>${_("Note: No spaces or special characters are allowed. This cannot be changed.")}</strong></span>
</li>
<li class="field field-inline 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. 2013_Spring')}" />
<span class="tip tip-stacked">${_("The term in which your course will run")} - <strong>${_("Note: No spaces or special characters are allowed. This cannot be changed.")}</strong></span>
</li>
</ol>
</fieldset>
</div>
<div class="actions">
<input type="submit" value="${_('Save')}" class="action action-primary new-course-save" />
<input type="button" value="${_('Cancel')}" class="action action-secondary action-cancel new-course-cancel" />
</div>
</form>
</div>
% endif
%if len(courses) > 0: %if len(courses) > 0:
<div class="courses"> <div class="courses">
<ul class="list-courses"> <ul class="list-courses">
......
...@@ -38,6 +38,7 @@ ...@@ -38,6 +38,7 @@
<fieldset class="form-fields"> <fieldset class="form-fields">
<legend class="sr">${_("New Team Member Information")}</legend> <legend class="sr">${_("New Team Member Information")}</legend>
<ol class="list-input"> <ol class="list-input">
<li class="field text required create-user-email"> <li class="field text required create-user-email">
<label for="user-email-input">${_("User's Email Address")}</label> <label for="user-email-input">${_("User's Email Address")}</label>
...@@ -47,6 +48,7 @@ ...@@ -47,6 +48,7 @@
</ol> </ol>
</fieldset> </fieldset>
</div> </div>
<div class="actions"> <div class="actions">
<button class="action action-primary" type="submit">${_("Add User")}</button> <button class="action action-primary" type="submit">${_("Add User")}</button>
<button class="action action-secondary action-cancel">${_("Cancel")}</button> <button class="action action-secondary action-cancel">${_("Cancel")}</button>
......
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