Commit 97e893b2 by Brian Talbot

Merge branch 'master' into feature/btalbot/studio-courselink

parents 01b2fefd 81f94fc4
......@@ -35,6 +35,7 @@ load-plugins=
# it should appear only once).
disable=
# C0301: Line too long
# C0302: Too many lines in module
# W0141: Used builtin function 'map'
# W0142: Used * or ** magic
# R0201: Method could be a function
......@@ -42,8 +43,11 @@ disable=
# R0902: Too many instance attributes
# R0903: Too few public methods (1/2)
# R0904: Too many public methods
# R0911: Too many return statements
# R0912: Too many branches
# R0913: Too many arguments
C0301,W0141,W0142,R0201,R0901,R0902,R0903,R0904,R0913
# R0914: Too many local variables
C0301,C0302,W0141,W0142,R0201,R0901,R0902,R0903,R0904,R0911,R0912,R0913,R0914
[REPORTS]
......@@ -92,7 +96,7 @@ zope=no
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E0201 when accessed. Python regular
# expressions are accepted.
generated-members=REQUEST,acl_users,aq_parent,objects,DoesNotExist,can_read,can_write,get_url,size
generated-members=REQUEST,acl_users,aq_parent,objects,DoesNotExist,can_read,can_write,get_url,size,content
[BASIC]
......
......@@ -628,6 +628,113 @@ class ContentStoreTest(ModuleStoreTestCase):
self.assertIn('markdown', context, "markdown is missing from context")
self.assertNotIn('markdown', problem.editable_metadata_fields, "Markdown slipped into the editable metadata fields")
def test_cms_imported_course_walkthrough(self):
"""
Import and walk through some common URL endpoints. This just verifies non-500 and no other
correct behavior, so it is not a deep test
"""
import_from_xml(modulestore(), 'common/test/data/', ['simple'])
loc = Location(['i4x', 'edX', 'simple', 'course', '2012_Fall', None])
resp = self.client.get(reverse('course_index',
kwargs={'org': loc.org,
'course': loc.course,
'name': loc.name}))
self.assertEqual(200, resp.status_code)
self.assertContains(resp, 'Chapter 2')
# go to various pages
# import page
resp = self.client.get(reverse('import_course',
kwargs={'org': loc.org,
'course': loc.course,
'name': loc.name}))
self.assertEqual(200, resp.status_code)
# export page
resp = self.client.get(reverse('export_course',
kwargs={'org': loc.org,
'course': loc.course,
'name': loc.name}))
self.assertEqual(200, resp.status_code)
# manage users
resp = self.client.get(reverse('manage_users',
kwargs={'location': loc.url()}))
self.assertEqual(200, resp.status_code)
# course info
resp = self.client.get(reverse('course_info',
kwargs={'org': loc.org,
'course': loc.course,
'name': loc.name}))
self.assertEqual(200, resp.status_code)
# settings_details
resp = self.client.get(reverse('settings_details',
kwargs={'org': loc.org,
'course': loc.course,
'name': loc.name}))
self.assertEqual(200, resp.status_code)
# settings_details
resp = self.client.get(reverse('settings_grading',
kwargs={'org': loc.org,
'course': loc.course,
'name': loc.name}))
self.assertEqual(200, resp.status_code)
# static_pages
resp = self.client.get(reverse('static_pages',
kwargs={'org': loc.org,
'course': loc.course,
'coursename': loc.name}))
self.assertEqual(200, resp.status_code)
# static_pages
resp = self.client.get(reverse('asset_index',
kwargs={'org': loc.org,
'course': loc.course,
'name': loc.name}))
self.assertEqual(200, resp.status_code)
# go look at a subsection page
subsection_location = loc._replace(category='sequential', name='test_sequence')
resp = self.client.get(reverse('edit_subsection',
kwargs={'location': subsection_location.url()}))
self.assertEqual(200, resp.status_code)
# go look at the Edit page
unit_location = loc._replace(category='vertical', name='test_vertical')
resp = self.client.get(reverse('edit_unit',
kwargs={'location': unit_location.url()}))
self.assertEqual(200, resp.status_code)
# delete a component
del_loc = loc._replace(category='html', name='test_html')
resp = self.client.post(reverse('delete_item'),
json.dumps({'id': del_loc.url()}), "application/json")
self.assertEqual(200, resp.status_code)
# delete a unit
del_loc = loc._replace(category='vertical', name='test_vertical')
resp = self.client.post(reverse('delete_item'),
json.dumps({'id': del_loc.url()}), "application/json")
self.assertEqual(200, resp.status_code)
# delete a unit
del_loc = loc._replace(category='sequential', name='test_sequence')
resp = self.client.post(reverse('delete_item'),
json.dumps({'id': del_loc.url()}), "application/json")
self.assertEqual(200, resp.status_code)
# delete a chapter
del_loc = loc._replace(category='chapter', name='chapter_2')
resp = self.client.post(reverse('delete_item'),
json.dumps({'id': del_loc.url()}), "application/json")
self.assertEqual(200, resp.status_code)
def test_import_metadata_with_attempts_empty_string(self):
import_from_xml(modulestore(), 'common/test/data/', ['simple'])
module_store = modulestore('direct')
......
......@@ -120,6 +120,12 @@ def howitworks(request):
else:
return render_to_response('howitworks.html', {})
# static/proof-of-concept views
def ux_alerts(request):
return render_to_response('ux-alerts.html', {})
# ==== Views for any logged-in user ==================================
......
if (!window.CmsUtils) window.CmsUtils = {};
var $body;
var $modal;
var $modalCover;
......@@ -48,6 +50,10 @@ $(document).ready(function () {
(e).preventDefault();
});
// alerts/notifications - manual close
$('.action-alert-close, .alert.has-actions .nav-actions a').bind('click', hideAlert);
$('.action-notification-close').bind('click', hideNotification);
// nav - dropdown related
$body.click(function (e) {
$('.nav-dropdown .nav-item .wrapper-nav-sub').removeClass('is-shown');
......@@ -87,7 +93,7 @@ $(document).ready(function () {
$('a[rel*="view"][href^="#"]').bind('click', smoothScrollLink);
// tender feedback window scrolling
$('a.show-tender').bind('click', smoothScrollTop);
$('a.show-tender').bind('click', window.CmsUtils.smoothScrollTop);
// toggling footer additional support
$('.cta-show-sock').bind('click', toggleSock);
......@@ -159,21 +165,24 @@ $(document).ready(function () {
function smoothScrollLink(e) {
(e).preventDefault();
$.smoothScroll({
offset: -200,
easing: 'swing',
$.smoothScroll({
offset: -200,
easing: 'swing',
speed: 1000,
scrollElement: null,
scrollTarget: $(this).attr('href')
});
}
function smoothScrollTop(e) {
// On AWS instances, this base.js gets wrapped in a separate scope as part of Django static
// pipelining (note, this doesn't happen on local runtimes). So if we set it on window,
// when we can access it from other scopes (namely Course Advanced Settings).
window.CmsUtils.smoothScrollTop = function (e) {
(e).preventDefault();
$.smoothScroll({
offset: -200,
easing: 'swing',
$.smoothScroll({
offset: -200,
easing: 'swing',
speed: 1000,
scrollElement: null,
scrollTarget: $('#view-top')
......@@ -483,9 +492,9 @@ function toggleSock(e) {
$sock.toggleClass('is-shown');
$sockContent.toggle('fast');
$.smoothScroll({
offset: -200,
easing: 'swing',
$.smoothScroll({
offset: -200,
easing: 'swing',
speed: 1000,
scrollElement: null,
scrollTarget: $sock
......@@ -538,6 +547,17 @@ function removeDateSetter(e) {
$block.find('.time').val('');
}
function hideNotification(e) {
(e).preventDefault();
$(this).closest('.wrapper-notification').removeClass('is-shown').addClass('is-hiding').attr('aria-hidden','true');
}
function hideAlert(e) {
(e).preventDefault();
$(this).closest('.wrapper-alert').removeClass('is-shown');
}
function showToastMessage(message, $button, lifespan) {
var $toast = $('<div class="toast-notification"></div>');
var $closeBtn = $('<a href="#" class="close-button">×</a>');
......@@ -839,4 +859,4 @@ function saveSetSectionScheduleDate(e) {
hideModal();
});
}
\ No newline at end of file
}
......@@ -32,7 +32,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
var listEle$ = this.$el.find('.course-advanced-policy-list');
listEle$.empty();
// b/c we've deleted all old fields, clear the map and repopulate
this.fieldToSelectorMap = {};
this.selectorToField = {};
......@@ -101,13 +101,13 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
});
},
showMessage: function (type) {
this.$el.find(".message-status").removeClass("is-shown");
$(".wrapper-alert").removeClass("is-shown");
if (type) {
if (type === this.error_saving) {
this.$el.find(".message-status.error").addClass("is-shown");
$(".wrapper-alert-error").addClass("is-shown").attr('aria-hidden','false');
}
else if (type === this.successful_changes) {
this.$el.find(".message-status.confirm").addClass("is-shown");
$(".wrapper-alert-confirmation").addClass("is-shown").attr('aria-hidden','false');
this.hideSaveCancelButtons();
}
}
......@@ -117,17 +117,20 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
}
},
showSaveCancelButtons: function(event) {
if (!this.buttonsVisible) {
if (!this.notificationBarShowing) {
this.$el.find(".message-status").removeClass("is-shown");
$('.wrapper-notification').addClass('is-shown');
this.buttonsVisible = true;
$('.wrapper-notification').removeClass('is-hiding').addClass('is-shown').attr('aria-hidden','false');
this.notificationBarShowing = true;
}
},
hideSaveCancelButtons: function() {
$('.wrapper-notification').removeClass('is-shown');
this.buttonsVisible = false;
if (this.notificationBarShowing) {
$('.wrapper-notification').removeClass('is-shown').addClass('is-hiding').attr('aria-hidden','true');
this.notificationBarShowing = false;
}
},
saveView : function(event) {
window.CmsUtils.smoothScrollTop(event);
// TODO one last verification scan:
// call validateKey on each to ensure proper format
// check for dupes
......@@ -146,6 +149,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
});
},
revertView : function(event) {
event.preventDefault();
var self = event.data;
self.model.deleteKeys = [];
self.model.clear({silent : true});
......@@ -158,7 +162,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
var newKeyId = _.uniqueId('policy_key_'),
newEle = this.template({ key : key, value : JSON.stringify(value, null, 4),
keyUniqueId: newKeyId, valueUniqueId: _.uniqueId('policy_value_')});
this.fieldToSelectorMap[key] = newKeyId;
this.selectorToField[newKeyId] = key;
return newEle;
......@@ -169,4 +173,4 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
blurInput : function(event) {
$(event.target).prev().removeClass("is-focused");
}
});
\ No newline at end of file
});
......@@ -25,7 +25,7 @@ a {
@include transition(color 0.25s ease-in-out);
&:hover {
color: #cb9c40;
color: $orange-d1;
}
}
......@@ -50,11 +50,72 @@ h1 {
// ====================
// typography - basic
.title-1, .title-2, .title-3, .title-4, .title-5, .title-6 {
font-weight: 600;
color: $gray-d3;
margin: 0;
padding: 0;
}
.title-1 {
@include font-size(32);
margin-bottom: ($baseline*1.5);
}
.title-2 {
@include font-size(24);
margin-bottom: $baseline;
}
.title-3 {
@include font-size(18);
margin-bottom: ($baseline/2);
}
.title-4 {
@include font-size(14);
margin-bottom: $baseline;
font-weight: 500
}
.title-5 {
@include font-size(14);
color: $gray-l1;
margin-bottom: $baseline;
font-weight: 500
}
.title-6 {
@include font-size(14);
color: $gray-l2;
margin-bottom: $baseline;
font-weight: 500
}
p, ul, ol, dl {
margin-bottom: ($baseline/2);
&:last-child {
margin-bottom: 0;
}
}
// ====================
// 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();
@include font-size(16);
......@@ -62,7 +123,7 @@ h1 {
max-width: $fg-max-width;
min-width: $fg-min-width;
width: flex-grid(12);
margin: ($baseline*1.5) auto $baseline auto;
margin: 0 auto $baseline auto;
color: $gray-d2;
}
......@@ -284,18 +345,33 @@ h1 {
margin: 0 0 ($baseline/2) 0;
}
.title-4 {
}
header {
@include clearfix();
.title-5 {
.title-2 {
width: flex-grid(5, 12);
margin: 0 flex-gutter() 0 0;
float: left;
}
.tip {
@include font-size(13);
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;
}
.bit {
@include font-size(13);
margin: 0 0 $baseline 0;
......@@ -761,10 +837,10 @@ body.js {
// ====================
// works in progress
// works in progress & testing
body.hide-wip {
.wip-box {
display: none;
}
}
\ No newline at end of file
}
......@@ -15,17 +15,17 @@
// mixins - grandfathered
@mixin button {
display: inline-block;
padding: 4px 20px 6px;
font-size: 14px;
padding: ($baseline/5) $baseline ($baseline/4);
@include font-size(14);
font-weight: 700;
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset, 0 0 0 rgba(0, 0, 0, 0));
@include transition(background-color .15s, box-shadow .15s);
&.disabled {
border: 1px solid $lightGrey !important;
border: 1px solid $gray-l1 !important;
border-radius: 3px !important;
background: $lightGrey !important;
color: $darkGrey !important;
background: $gray-l1 !important;
color: $gray-d1 !important;
pointer-events: none;
cursor: none;
&:hover {
......@@ -38,32 +38,111 @@
}
}
@mixin green-button {
@include button;
border: 1px solid $green-d1;
border-radius: 3px;
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
background-color: $green;
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset);
color: $white;
&:hover {
background-color: $green-s1;
color: $white;
}
&.disabled {
border: 1px solid $green-l3 !important;
background: $green-l3 !important;
color: $white !important;
@include box-shadow(none);
}
}
@mixin blue-button {
@include button;
border: 1px solid #437fbf;
border: 1px solid $blue-d1;
border-radius: 3px;
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
background-color: $blue;
color: #fff;
color: $white;
&:hover, &.active {
background-color: #62aaf5;
color: #fff;
background-color: $blue-s2;
color: $white;
}
&.disabled {
border: 1px solid $blue-l3 !important;
background: $blue-l3 !important;
color: $white !important;
@include box-shadow(none);
}
}
@mixin green-button {
@include button;
border: 1px solid #0d7011;
border-radius: 3px;
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
background-color: $green;
color: #fff;
@mixin red-button {
@include button;
border: 1px solid $red-d1;
border-radius: 3px;
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
background-color: $red;
color: $white;
&:hover {
background-color: #129416;
color: #fff;
}
&:hover, &.active {
background-color: $red-s1;
color: $white;
}
&.disabled {
border: 1px solid $red-l3 !important;
background: $red-l3 !important;
color: $white !important;
@include box-shadow(none);
}
}
@mixin pink-button {
@include button;
border: 1px solid $pink-d1;
border-radius: 3px;
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
background-color: $pink;
color: $white;
&:hover, &.active {
background-color: $pink-s1;
color: $white;
}
&.disabled {
border: 1px solid $pink-l3 !important;
background: $pink-l3 !important;
color: $white !important;
@include box-shadow(none);
}
}
@mixin orange-button {
@include button;
border: 1px solid $orange-d1;
border-radius: 3px;
@include linear-gradient(top, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0) 60%);
background-color: $orange;
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset);
color: $gray-d2;
&:hover {
background-color: $orange-s2;
color: $gray-d2;
}
&.disabled {
border: 1px solid $orange-l3 !important;
background: $orange-l2 !important;
color: $gray-l1 !important;
@include box-shadow(none);
}
}
@mixin white-button {
......@@ -82,24 +161,9 @@
}
}
@mixin orange-button {
@include button;
border: 1px solid #bda046;
border-radius: 3px;
@include linear-gradient(top, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0) 60%);
background-color: #edbd3c;
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset);
color: #3c3c3c;
&:hover {
background-color: #ffcd46;
color: #3c3c3c;
}
}
@mixin grey-button {
@include button;
border: 1px solid $darkGrey;
border: 1px solid $gray-d2;
border-radius: 3px;
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
background-color: #d1dae3;
......@@ -127,39 +191,17 @@
}
}
@mixin green-button {
@include button;
border: 1px solid $darkGreen;
border-radius: 3px;
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
background-color: $green;
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset);
color: #fff;
&:hover {
background-color: $brightGreen;
color: #fff;
}
&.disabled {
border: 1px solid $disabledGreen !important;
background: $disabledGreen !important;
color: #fff !important;
@include box-shadow(none);
}
}
@mixin dark-grey-button {
@include button;
border: 1px solid #1c1e20;
border: 1px solid $gray-d2;
border-radius: 3px;
background: -webkit-linear-gradient(top, rgba(255, 255, 255, .2), rgba(255, 255, 255, 0)) $extraDarkGrey;
background: -webkit-linear-gradient(top, rgba(255, 255, 255, .2), rgba(255, 255, 255, 0)) $gray-d1;
box-shadow: 0 1px 0 rgba(255, 255, 255, .2) inset;
color: #fff;
color: $white;
&:hover {
background-color: #595f64;
color: #fff;
background-color: $gray-d4;
color: $white;
}
}
......@@ -180,7 +222,7 @@
}
textarea {
min-height: 80px;
min-height: 80px;
}
h5 {
......@@ -225,7 +267,7 @@
.section-item {
position: relative;
display: block;
padding: 6px 8px 8px 16px;
padding: 6px 8px 8px 16px;
background: #edf1f5;
font-size: 13px;
......@@ -296,6 +338,9 @@
}
}
// ====================
// sunsetted mixins
@mixin active {
@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
background-color: rgba(255, 255, 255, .3);
......@@ -389,4 +434,4 @@
.depth2 { z-index: 100; }
.depth3 { z-index: 1000; }
.depth4 { z-index: 10000; }
.depth5 { z-index: 100000; }
\ No newline at end of file
.depth5 { z-index: 100000; }
......@@ -113,7 +113,7 @@ $green-u1: desaturate($green,15%);
$green-u2: desaturate($green,30%);
$green-u3: desaturate($green,45%);
$yellow: rgb(231, 214, 143);
$yellow: rgb(237, 189, 60);
$yellow-l1: tint($yellow,20%);
$yellow-l2: tint($yellow,40%);
$yellow-l3: tint($yellow,60%);
......@@ -149,8 +149,13 @@ $orange-u3: desaturate($orange,45%);
$shadow: rgba(0,0,0,0.2);
$shadow-l1: rgba(0,0,0,0.1);
$shadow-l2: rgba(0,0,0,0.05);
$shadow-d1: rgba(0,0,0,0.4);
// specific UI
$notification-height: ($baseline*10);
// colors - inherited
$baseFontColor: $gray-d2;
$offBlack: #3c3c3c;
......@@ -167,4 +172,4 @@ $disabledGreen: rgb(124, 206, 153);
$darkGreen: rgb(52, 133, 76);
$lightBluishGrey: rgb(197, 207, 223);
$lightBluishGrey2: rgb(213, 220, 228);
$error-red: rgb(253, 87, 87);
\ No newline at end of file
$error-red: rgb(253, 87, 87);
@mixin bounce-in {
// studio animations & keyframes
// ====================
// rotate clockwise
@mixin rotateClockwise {
0% {
@include transform(rotate(0deg));
}
100% {
@include transform(rotate(360deg));
}
}
@-moz-keyframes rotateClockwise { @include rotateClockwise(); }
@-webkit-keyframes rotateClockwise { @include rotateClockwise(); }
@-o-keyframes rotateClockwise { @include rotateClockwise(); }
@keyframes rotateClockwise { @include rotateClockwise();}
@mixin anim-rotateClockwise($duration, $timing: ease-in-out, $count: 1, $delay: 0) {
@include animation-name(rotateClockwise);
@include animation-duration($duration);
@include animation-delay($delay);
@include animation-timing-function($timing);
@include animation-iteration-count($count);
@include animation-fill-mode(both);
}
// ====================
// notifications slide up
@mixin notificationsSlideUp {
0% {
@include transform(translateY(0));
}
90% {
@include transform(translateY(-($notification-height)));
}
100% {
@include transform(translateY(-($notification-height*0.99)));
}
}
@-moz-keyframes notificationsSlideUp { @include notificationsSlideUp(); }
@-webkit-keyframes notificationsSlideUp { @include notificationsSlideUp(); }
@-o-keyframes notificationsSlideUp { @include notificationsSlideUp(); }
@keyframes notificationsSlideUp { @include notificationsSlideUp();}
@mixin anim-notificationsSlideUp($duration, $timing: ease-in-out, $count: 1, $delay: 0) {
@include animation-name(notificationsSlideUp);
@include animation-duration($duration);
@include animation-delay($delay);
@include animation-timing-function($timing);
@include animation-iteration-count($count);
@include animation-fill-mode(both);
}
// ====================
// notifications slide down
@mixin notificationsSlideDown {
0% {
@include transform(translateY(-($notification-height*0.99)));
}
10% {
@include transform(translateY(-($notification-height)));
}
100% {
@include transform(translateY(0));
}
}
@-moz-keyframes notificationsSlideDown { @include notificationsSlideDown(); }
@-webkit-keyframes notificationsSlideDown { @include notificationsSlideDown(); }
@-o-keyframes notificationsSlideDown { @include notificationsSlideDown(); }
@keyframes notificationsSlideDown { @include notificationsSlideDown();}
@mixin anim-notificationsSlideDown($duration, $timing: ease-in-out, $count: 1, $delay: 0) {
@include animation-name(notificationsSlideDown);
@include animation-duration($duration);
@include animation-delay($delay);
@include animation-timing-function($timing);
@include animation-iteration-count($count);
@include animation-fill-mode(both);
}
// ====================
// notifications slide up then down
@mixin notificationsSlideUpDown {
0%, 100% {
@include transform(translateY(0));
}
15%, 85% {
@include transform(translateY(-($notification-height)));
}
20%, 80% {
@include transform(translateY(-($notification-height*0.99)));
}
}
@-moz-keyframes notificationsSlideUpDown { @include notificationsSlideUpDown(); }
@-webkit-keyframes notificationsSlideUpDown { @include notificationsSlideUpDown(); }
@-o-keyframes notificationsSlideUpDown { @include notificationsSlideUpDown(); }
@keyframes notificationsSlideUpDown { @include notificationsSlideUpDown();}
@mixin anim-notificationsSlideUpDown($duration, $timing: ease-in-out, $count: 1, $delay: 0) {
@include animation-name(notificationsSlideUpDown);
@include animation-duration($duration);
@include animation-delay($delay);
@include animation-timing-function($timing);
@include animation-iteration-count($count);
@include animation-fill-mode(both);
}
// ====================
// bounce in
@mixin bounceIn {
0% {
opacity: 0;
@include transform(scale(.3));
@include transform(scale(0.3));
}
50% {
......@@ -14,14 +140,63 @@
}
}
@-moz-keyframes bounce-in { @include bounce-in(); }
@-webkit-keyframes bounce-in { @include bounce-in(); }
@-o-keyframes bounce-in { @include bounce-in(); }
@keyframes bounce-in { @include bounce-in();}
@-moz-keyframes bounceIn { @include bounceIn(); }
@-webkit-keyframes bounceIn { @include bounceIn(); }
@-o-keyframes bounceIn { @include bounceIn(); }
@keyframes bounceIn { @include bounceIn();}
@mixin bounce-in-animation($duration, $timing: ease-in-out) {
@include animation-name(bounce-in);
@mixin anim-bounceIn($duration, $timing: ease-in-out, $count: 1, $delay: 0) {
@include animation-name(bounceIn);
@include animation-duration($duration);
@include animation-delay($delay);
@include animation-timing-function($timing);
@include animation-iteration-count($count);
@include animation-fill-mode(both);
}
// ====================
// bounce in
@mixin bounceOut {
0% {
opacity: 0;
@include transform(scale(0.3));
}
50% {
opacity: 1;
@include transform(scale(1.05));
}
100% {
@include transform(scale(1));
}
0% {
@include transform(scale(1));
}
50% {
opacity: 1;
@include transform(scale(1.05));
}
100% {
opacity: 0;
@include transform(scale(0.3));
}
}
@-moz-keyframes bounceOut { @include bounceOut(); }
@-webkit-keyframes bounceOut { @include bounceOut(); }
@-o-keyframes bounceOut { @include bounceOut(); }
@keyframes bounceOut { @include bounceOut();}
@mixin anim-bounceOut($duration, $timing: ease-in-out, $count: 1, $delay: 0) {
@include animation-name(bounceOut);
@include animation-duration($duration);
@include animation-delay($delay);
@include animation-timing-function($timing);
@include animation-iteration-count($count);
@include animation-fill-mode(both);
}
\ No newline at end of file
......@@ -4,6 +4,7 @@
// bourbon libs and resets
@import 'bourbon/bourbon';
@import 'bourbon/addons/button';
@import "variables";
@import 'vendor/normalize';
@import 'reset';
......
......@@ -5,12 +5,12 @@
margin: 0;
padding: $baseline;
border-bottom: 1px solid $gray;
@include box-shadow(0 1px 5px 0 rgba(0,0,0, 0.1));
@include box-shadow(0 1px 5px 0 rgba(0,0,0, 0.2));
background: $white;
height: 76px;
position: relative;
width: 100%;
z-index: 10;
z-index: 1000;
a {
color: $baseFontColor;
......
......@@ -4,7 +4,7 @@
body.signup, body.signin {
.wrapper-content {
margin: 0;
margin: ($baseline*1.5) 0 0 0;
padding: 0 $baseline;
position: relative;
width: 100%;
......@@ -18,7 +18,7 @@ body.signup, body.signin {
width: flex-grid(12);
margin: 0 auto;
color: $gray-d2;
header {
position: relative;
margin-bottom: $baseline;
......@@ -121,7 +121,7 @@ body.signup, body.signin {
@include font-size(16);
height: 100%;
width: 100%;
padding: ($baseline/2);
padding: ($baseline/2);
&.long {
width: 100%;
......@@ -136,15 +136,15 @@ body.signup, body.signin {
}
:-moz-placeholder {
color: $gray-l3;
color: $gray-l3;
}
::-moz-placeholder {
color: $gray-l3;
color: $gray-l3;
}
:-ms-input-placeholder {
color: $gray-l3;
:-ms-input-placeholder {
color: $gray-l3;
}
&:focus {
......
......@@ -49,20 +49,30 @@
<script type="text/javascript" src="${static.url('js/vendor/CodeMirror/htmlmixed.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/CodeMirror/css.js')}"></script>
<script type="text/javascript">
document.write('\x3Cscript type="text/javascript" src="' +
document.location.protocol + '//www.youtube.com/player_api">\x3C/script>');
document.write('\x3Cscript type="text/javascript" src="' +
document.location.protocol + '//www.youtube.com/player_api">\x3C/script>');
</script>
<%include file="widgets/header.html" />
<%block name="content"></%block>
% if user.is_authenticated():
<%include file="widgets/sock.html" />
% endif
<%include file="widgets/footer.html" />
<%include file="widgets/tender.html" />
<!-- view -->
<div class="wrapper wrapper-view">
<%include file="widgets/header.html" />
<%block name="view_alerts"></%block>
<%block name="view_banners"></%block>
<%block name="content"></%block>
% if user.is_authenticated():
<%include file="widgets/sock.html" />
% endif
<%include file="widgets/footer.html" />
<%include file="widgets/tender.html" />
<%block name="view_notifications"></%block>
</div>
<%block name="view_prompts"></%block>
<%block name="jsextra"></%block>
</body>
......
......@@ -69,7 +69,7 @@
<article class="my-classes">
% if user.is_active:
<ul class="class-list">
%for course, url, lms_link in sorted(courses, key=lambda s: s[0].lower()):
%for course, url, lms_link in sorted(courses, key=lambda s: s[0].lower() if s[0] is not None else ''):
<li>
<a class="class-link" href="${url}" class="class-name">
<span class="class-name">${course}</span>
......
......@@ -26,9 +26,9 @@
// I believe that current (New Section/New Subsection) cause full page reloads which means these aren't needed globally
// but we really should change that behavior.
if (!window.graderTypes) {
window.graderTypes = new CMS.Models.Settings.CourseGraderCollection();
window.graderTypes.course_location = new CMS.Models.Location('${parent_location}');
window.graderTypes.reset(${course_graders|n});
window.graderTypes = new CMS.Models.Settings.CourseGraderCollection();
window.graderTypes.course_location = new CMS.Models.Location('${parent_location}');
window.graderTypes.reset(${course_graders|n});
}
$(".gradable-status").each(function(index, ele) {
......
......@@ -42,13 +42,17 @@ editor.render();
</%block>
<%block name="content">
<div class="wrapper-content wrapper">
<section class="content">
<header class="page">
<div class="wrapper-mast wrapper">
<header class="mast has-subtitle">
<div class="title">
<span class="title-sub">Settings</span>
<h1 class="title-1">Advanced Settings</h1>
</header>
</div>
</header>
</div>
<div class="wrapper-content wrapper">
<section class="content">
<article class="content-primary" role="main">
<form id="settings_advanced" class="settings-advanced" method="post" action="">
......@@ -69,7 +73,7 @@ editor.render();
<p class="instructions"><strong>Warning</strong>: Do not modify these policies unless you are familiar with their purpose.</p>
<ul class="list-input course-advanced-policy-list enum">
</ul>
</section>
</form>
......@@ -100,23 +104,61 @@ editor.render();
</aside>
</section>
</div>
</%block>
<%block name="view_notifications">
<!-- notification: change has been made and a save is needed -->
<div class="wrapper wrapper-notification wrapper-notification-warning">
<div class="notification warning">
<div class="copy">
<i class="ss-icon ss-symbolicons-block icon icon-warning">&#x26A0;</i>
<div class="wrapper wrapper-notification wrapper-notification-warning" aria-hidden="true" role="dialog" aria-labelledby="notification-changesMade-title" aria-describedby="notification-changesMade-description">
<div class="notification warning has-actions">
<i class="ss-icon ss-symbolicons-block icon icon-warning">&#x26A0;</i>
<p><strong>Note: </strong>Your changes will not take effect until you <strong>save your
progress</strong>. Take care with policy value formatting, as validation is <strong>not implemented</strong>.</p>
<div class="copy">
<h2 class="title title-3" id="notification-changesMade-title">You've Made Some Changes</h2>
<p id="notification-changesMade-description">Your changes will not take effect until you <strong>save your progress</strong>. Take care with key and value formatting, as validation is <strong>not implemented</strong>.</p>
</div>
<div class="actions">
<nav class="nav-actions">
<h3 class="sr">Notification Actions</h3>
<ul>
<li><a href="#" class="save-button">Save</a></li>
<li><a href="#" class="cancel-button">Cancel</a></li>
<li class="nav-item">
<a href="" class="action-primary save-button">Save Changes</a>
</li>
<li class="nav-item">
<a href="" class="action-secondary cancel-button">Cancel</a>
</li>
</ul>
</nav>
</div>
</div>
</%block>
<%block name="view_alerts">
<!-- alert: save confirmed with close -->
<div class="wrapper wrapper-alert wrapper-alert-confirmation" role="status">
<div class="alert confirmation">
<i class="ss-icon ss-symbolicons-standard icon icon-confirmation">&#x2713;</i>
<div class="copy">
<h2 class="title title-3">Your policy changes have been saved.</h2>
<p>Please note that validation of your policy key and value pairs is not currently in place yet. If you are having difficulties, please review your policy pairs.</p>
</div>
<a href="" rel="view" class="action action-alert-close">
<i class="ss-icon ss-symbolicons-block icon icon-close">&#x2421;</i>
<span class="label">close alert</span>
</a>
</div>
</div>
<!-- alert: error -->
<div class="wrapper wrapper-alert wrapper-alert-error" role="status">
<div class="alert error">
<i class="ss-icon ss-symbolicons-block icon icon-error">&#x26A0;</i>
<div class="copy">
<h2 class="title title-3">There was an error saving your information</h2>
<p>Please see the error below and correct it to ensure there are no problems in rendering your course.</p>
</div>
</div>
</div>
</%block>
\ No newline at end of file
</%block>
......@@ -16,7 +16,6 @@
});
$(document).ready(function() {
$('body').addClass('js');
// tabs
$('.tab-group').tabs();
......
......@@ -5,20 +5,20 @@
<a href="#sock" class="cta cta-show-sock"><i class="ss-icon ss-symbolicons-block icon icon-inline icon-help">&#x2753;</i> <span class="copy">Looking for Help with Studio?</span></a>
</li>
</ul>
<div class="wrapper-inner wrapper">
<section class="sock" id="sock">
<header>
<h2 class="title sr"><i class="ss-icon ss-symbolicons-block icon icon-inline icon-help">&#x2753;</i> edX Studio Help</h2>
</header>
<div class="support">
<h3 class="title">Studio Support</h3>
<div class="copy">
<p>Need help with Studio? Creating a course is complex, so we're here to help? Take advantage of our documentation, help center, as well as our edX101 introduction course for course authors.</p>
<p>Need help with Studio? Creating a course is complex, so we're here to help. Take advantage of our documentation, help center, as well as our edX101 introduction course for course authors.</p>
</div>
<ul class="list-actions">
<li class="action-item">
<a href="http://files.edx.org/Getting_Started_with_Studio.pdf" class="action action-primary" title="This is a PDF Document"><i class="ss-icon icon ss-symbolicons-block icon icon-inline icon-pdf">&#xEC00;</i> Download Studio Documentation</a>
......@@ -34,21 +34,21 @@
</li>
</ul>
</div>
<div class="feedback">
<h3 class="title">Contact us about Studio</h3>
<div class="copy">
<p>Have problems, questions, or suggestions about Studio? We're here to help and listen to any feedback you want to share.</p>
<p>Have problems, questions, or suggestions about Studio? We're also here to listen to any feedback you want to share.</p>
</div>
<ul class="list-actions">
<li class="action-item">
<a href="http://help.edge.edx.org/discussion/new" class="action action-primary show-tender" title="Use our feedback tool, Tender, to share your feedback"><i class="ss-icon ss-symbolicons-block icon icon-inline icon-feedback">&#xE398;</i> Contact Us</a>
</li>
</ul>
</div>
</section>
</div>
</div>
\ No newline at end of file
</div>
</div>
......@@ -116,6 +116,8 @@ urlpatterns += (
url(r'^logout$', 'student.views.logout_user', name='logout'),
# static/proof-of-concept views
url(r'^ux-alerts$', 'contentstore.views.ux_alerts', name='ux-alerts')
)
if settings.ENABLE_JASMINE:
......
......@@ -258,11 +258,11 @@ class MongoModuleStore(ModuleStoreBase):
# get all collections in the course, this query should not return any leaf nodes
# note this is a bit ugly as when we add new categories of containers, we have to add it here
query = {
'_id.org': location.org,
'_id.course': location.course,
'_id.category': {'$in': ['course', 'chapter', 'sequential', 'vertical']}
}
query = {'_id.org': location.org,
'_id.course': location.course,
'_id.category': {'$in': ['course', 'chapter', 'sequential', 'vertical',
'wrapper', 'problemset', 'conditional']}
}
# we just want the Location, children, and inheritable metadata
record_filter = {'_id': 1, 'definition.children': 1}
......
......@@ -284,7 +284,7 @@ def import_from_xml(store, data_dir, course_dirs=None,
except KeyError:
# Ignore any missing keys in _model_data
pass
if 'data' in content:
module_data = content['data']
......@@ -301,16 +301,18 @@ def import_from_xml(store, data_dir, course_dirs=None,
# Note the dropped element closing tag. This causes the LMS to fail when rendering modules - that's
# no good, so we have to do this kludge
if isinstance(module_data, str) or isinstance(module_data, unicode): # some module 'data' fields are non strings which blows up the link traversal code
lxml_rewrite_links(module_data, lambda link: verify_content_links(module, course_data_path,
static_content_store, link, remap_dict))
lxml_rewrite_links(module_data,
lambda link: verify_content_links(module, course_data_path, static_content_store, link, remap_dict))
for key in remap_dict.keys():
module_data = module_data.replace(key, remap_dict[key])
except Exception, e:
except Exception:
logging.exception("failed to rewrite links on {0}. Continuing...".format(module.location))
else:
module_data = content
store.update_item(module.location, content)
store.update_item(module.location, module_data)
if hasattr(module, 'children') and module.children != []:
store.update_children(module.location, module.children)
......
......@@ -199,8 +199,8 @@ class PeerGradingModule(PeerGradingFields, XModule):
self.student_data_for_location = response
score_dict = {
'score': int(count_graded >= count_required),
'total': self.max_grade,
'score': int(count_graded >= count_required and count_graded>0) * int(self.weight),
'total': self.max_grade * int(self.weight),
}
return score_dict
......
......@@ -24,6 +24,11 @@ def strip_filenames(descriptor):
"""
print "strip filename from {desc}".format(desc=descriptor.location.url())
descriptor._model_data.pop('filename', None)
if hasattr(descriptor, 'xml_attributes'):
if 'filename' in descriptor.xml_attributes:
del descriptor.xml_attributes['filename']
for d in descriptor.get_children():
strip_filenames(d)
......
......@@ -11,11 +11,15 @@
font-size: ($sizeValue/10) + rem;
}
// ====================
// line-height
@function lh($amount: 1) {
@return $body-line-height * $amount;
}
// ====================
// image-replacement hidden text
@mixin text-hide() {
text-indent: 100%;
......@@ -35,6 +39,8 @@
width: 1px;
}
// ====================
// vertical and horizontal centering
@mixin vertically-and-horizontally-centered ($height, $width) {
left: 50%;
......@@ -46,6 +52,8 @@
top: 150px;
}
// ====================
// sizing
@mixin size($width: $baseline, $height: $baseline) {
height: $height;
......@@ -56,6 +64,8 @@
@include size($size);
}
// ====================
// placeholder styling
@mixin placeholder($color) {
:-moz-placeholder {
......
......@@ -12,15 +12,15 @@
</sequential>
</section>
</chapter>
<chapter name="Chapter 2">
<chapter name="Chapter 2" url_name='chapter_2'>
<section name="Problem Set 1">
<sequential>
<problem type="lecture" showanswer="attempted" rerandomize="true" display_name="A simple coding problem" name="Simple coding problem" filename="ps01-simple" url_name="ps01-simple"/>
</sequential>
</section>
<video name="Lost Video" youtube="1.0:TBvX7HzxexQ"/>
<sequential format="Lecture Sequence">
<vertical url_name='test_verical'>
<sequential format="Lecture Sequence" url_name='test_sequence'>
<vertical url_name='test_vertical'>
<html url_name='test_html'>
Foobar
</html>
......
......@@ -3,17 +3,12 @@
# django management command: dump grades to csv files
# for use by batch processes
import os
import sys
import string
import datetime
import json
import csv
from instructor.views import *
from instructor.views import get_student_grade_summary_data
from courseware.courses import get_course_by_id
from xmodule.modulestore.django import modulestore
from django.conf import settings
from django.core.management.base import BaseCommand
......@@ -45,7 +40,7 @@ class Command(BaseCommand):
request = self.DummyRequest()
try:
course = get_course_by_id(course_id)
except Exception as err:
except Exception:
if course_id in modulestore().courses:
course = modulestore().courses[course_id]
else:
......
......@@ -11,7 +11,6 @@ import requests
from requests.status_codes import codes
import urllib
from collections import OrderedDict
import json
from StringIO import StringIO
......@@ -21,7 +20,6 @@ from django.http import HttpResponse
from django_future.csrf import ensure_csrf_cookie
from django.views.decorators.cache import cache_control
from mitxmako.shortcuts import render_to_response
import requests
from django.core.urlresolvers import reverse
from courseware import grades
......@@ -36,11 +34,7 @@ from django_comment_client.models import (Role,
from django_comment_client.utils import has_forum_access
from psychometrics import psychoanalyze
from student.models import CourseEnrollment, CourseEnrollmentAllowed
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem
from xmodule.modulestore.search import path_to_location
import xmodule.graders as xmgraders
import track.views
......@@ -48,14 +42,15 @@ from .offline_gradecalc import student_grades, offline_grades_available
log = logging.getLogger(__name__)
template_imports = {'urllib': urllib}
# internal commands for managing forum roles:
FORUM_ROLE_ADD = 'add'
FORUM_ROLE_REMOVE = 'remove'
def split_by_comma_and_whitespace(s):
"""
Return string s, split by , or whitespace
"""
return re.split(r'[\s,]', s)
......@@ -141,7 +136,7 @@ def instructor_dashboard(request, course_id):
# 'beta', so adding it to get_access_group_name doesn't really make
# sense.
name = course_beta_test_group_name(course.location)
(group, created) = Group.objects.get_or_create(name=name)
(group, _) = Group.objects.get_or_create(name=name)
return group
# process actions from form POST
......@@ -237,13 +232,13 @@ def instructor_dashboard(request, course_id):
if '/' not in problem_to_reset: # allow state of modules other than problem to be reset
problem_to_reset = "problem/" + problem_to_reset # but problem is the default
try:
(org, course_name, run) = course_id.split("/")
(org, course_name, _) = course_id.split("/")
module_state_key = "i4x://" + org + "/" + course_name + "/" + problem_to_reset
module_to_reset = StudentModule.objects.get(student_id=student_to_reset.id,
course_id=course_id,
module_state_key=module_state_key)
msg += "Found module to reset. "
except Exception as e:
except Exception:
msg += "<font color='red'>Couldn't find module with that urlname. </font>"
if "Delete student state for problem" in action:
......@@ -352,7 +347,7 @@ def instructor_dashboard(request, course_id):
return_csv('', datatable, fp=fp)
fp.seek(0)
files = {'datafile': fp}
msg2, dataset = _do_remote_gradebook(request.user, course, 'post-grades', files=files)
msg2, _ = _do_remote_gradebook(request.user, course, 'post-grades', files=files)
msg += msg2
......@@ -423,7 +418,7 @@ def instructor_dashboard(request, course_id):
datatable = {'header': ['username', 'email'] + profkeys}
def getdat(u):
p = u.profile
return [u.username, u.email] + [getattr(p,x,'') for x in profkeys]
return [u.username, u.email] + [getattr(p, x, '') for x in profkeys]
datatable['data'] = [getdat(u) for u in enrolled_students]
datatable['title'] = 'Student profile data for course %s' % course_id
......@@ -433,17 +428,17 @@ def instructor_dashboard(request, course_id):
elif 'Download CSV of all responses to problem' in action:
problem_to_dump = request.POST.get('problem_to_dump','')
if problem_to_dump[-4:]==".xml":
problem_to_dump=problem_to_dump[:-4]
if problem_to_dump[-4:] == ".xml":
problem_to_dump = problem_to_dump[:-4]
try:
(org, course_name, run)=course_id.split("/")
module_state_key="i4x://"+org+"/"+course_name+"/problem/"+problem_to_dump
(org, course_name, run) = course_id.split("/")
module_state_key = "i4x://" + org + "/" + course_name + "/problem/" + problem_to_dump
smdat = StudentModule.objects.filter(course_id=course_id,
module_state_key=module_state_key)
smdat = smdat.order_by('student')
msg += "Found %d records to dump " % len(smdat)
except Exception as err:
msg+="<font color='red'>Couldn't find module with that urlname. </font>"
msg += "<font color='red'>Couldn't find module with that urlname. </font>"
msg += "<pre>%s</pre>" % escape(err)
smdat = []
......@@ -741,7 +736,7 @@ def _list_course_forum_members(course_id, rolename, datatable):
# make sure datatable is set up properly for display first, before checking for errors
datatable['header'] = ['Username', 'Full name', 'Roles']
datatable['title'] = 'List of Forum {0}s in course {1}'.format(rolename, course_id)
datatable['data'] = [];
datatable['data'] = []
try:
role = Role.objects.get(name=rolename, course_id=course_id)
except Role.DoesNotExist:
......@@ -923,7 +918,7 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True,
datarow = [student.id, student.username, student.profile.name, student.email]
try:
datarow.append(student.externalauthmap.external_email)
except: # ExternalAuthMap.DoesNotExist
except: # ExternalAuthMap.DoesNotExist
datarow.append('')
if get_grades:
......@@ -1040,7 +1035,8 @@ def _do_enroll_students(course, course_id, students, overload=False):
datatable['data'] = [[x, status[x]] for x in status]
datatable['title'] = 'Enrollment of students'
def sf(stat): return [x for x in status if status[x] == stat]
def sf(stat):
return [x for x in status if status[x] == stat]
data = dict(added=sf('added'), rejected=sf('rejected') + sf('exists'),
deleted=sf('deleted'), datatable=datatable)
......@@ -1136,7 +1132,7 @@ def dump_grading_context(course):
'''
msg = "-----------------------------------------------------------------------------\n"
msg += "Course grader:\n"
msg += '%s\n' % course.grader.__class__
graders = {}
if isinstance(course.grader, xmgraders.WeightedSubsectionsGrader):
......@@ -1151,7 +1147,7 @@ def dump_grading_context(course):
gc = course.grading_context
msg += "graded sections:\n"
msg += '%s\n' % gc['graded_sections'].keys()
for (gs, gsvals) in gc['graded_sections'].items():
msg += "--> Section %s:\n" % (gs)
......
......@@ -27,8 +27,6 @@ from mitxmako.shortcuts import render_to_string
log = logging.getLogger(__name__)
template_imports = {'urllib': urllib}
system = ModuleSystem(
ajax_url=None,
track_function=None,
......
......@@ -75,8 +75,8 @@
<article id="director-of-education-services" class="job">
<div class="inner-wrapper">
<h3><strong>DIRECTOR OF EDUCATIONAL SERVICES</strong></h3>
<p>The edX Director of Education Services reporting to the VP of Engineering and Educational Services is responsible for:</p>
<h3><strong>VICE PRESIDENT/DIRECTOR OF EDUCATIONAL SERVICES</strong></h3>
<p>The edX VP/Director of Education Services reporting to the VP of Engineering and Educational Services is responsible for:</p>
<ol>
<li>Delivering 20 new courses in 2013 in collaboration with the partner Universities
<ul>
......@@ -419,13 +419,76 @@
</div>
</article>
<article id="sales-engineer" class="job">
<div class="inner-wrapper">
<h3><strong>SALES ENGINEER, BUSINESS DEVELOPMENT TEAM</strong></h3>
<p>A great relationship with edX begins long before the first student signs up. We are
looking for some talented, self-motivated people to help set a solid foundation for
our emerging corporate customers, NGO’s, and governmental partners. As the Sales
Engineer you will be expected to provide oversight if requested over more junior
staff. This may include skills development, knowledge transfer, sharing technical
expertise, and review of demos prior to presentation. The Sales Engineer should
have familiarity with instructional design and competency in that area is a plus.</p>
<p>Experience teaching and mentoring, needs assessment and prior management
responsibility, and LMS experience, also a plus. This is a team atmosphere with
many constituencies working to develop a new global perspective regarding higher
education and online learning. Respect and patience, along with knowledge and
understanding of the development process is critical to success in order to maintain
strong bonds between development teams, sales team, and prospect/client
implementation teams. In addition the Sales Engineer may also work with our
xUniversity partners and the affiliated professors joining the edX movement. This
position requires customer facing skills, comfort in demonstrating the product, and
ability to code ‘demos’ as required. Additionally you will be contributing to
proposals, so clear documentation and writing skills are critical. The job will
require travel to client sites around the US upon occasion, and possibly
internationally as well. Job also requires good speaking skills, and a willingness and
ability to communicate clearly and respond quickly to prospect and customer
requests. This is a salaried position and will on occasion require work and
responsiveness to both the edX team and customers ‘after hours’. This position
reports to the VP, Business Development and will be dotted lined to the
development and program management teams.</p>
<p><strong>Responsibilities:</strong></p>
<ul>
<li>Can code demos and evaluate demos of others</li>
<li>Prepare and deliver standard and custom demonstrations</li>
<li>Handle all pre-sales technical issues professionally and efficiently</li>
<li>Maintain in-depth knowledge of products and pending new releases</li>
<li>Maintain a working knowledge of documentation and training</li>
<li>Maintain a working knowledge of workflow systems</li>
<li>Respond to technical questions from universities looking to expand their on-line offerings</li>
<li>Provide feedback to Product Development regarding new features, improving product performance, and eliminating bugs in the product</li>
<li>Prepare Professional Services for efficient onboarding – professionally managing the transition from pre-sales to post-sales</li>
<li>Deliver high-level presentation and associated ‘click-thru’ demonstrations</li>
<li>and be able to customize to prospect’s requirements</li>
<li>Understand and articulate the underlying technology concepts</li>
<li>Understand and articulate how all products components fit together technically as well as how they integrate and work with external technologies and cross functional applications found within clients organizations.</li>
<li>Build relationships with our prospects and universities, to be viewed as a trusted training partner.</li>
</ul>
<p><strong>Qualifications:</strong></p>
<ul>
<li>Minimum of 5 years of experience working closely with relationship based sales organizations, preferably in an educational technology organization.</li>
<li>Excellent interpersonal skills including proven presentation and facilitation skills.</li>
<li>Strong oral and written communication skills.</li>
<li>Flexibility to work on a variety of initiatives; prior startup experience preferred.</li>
<li>Outstanding work ethic, results-oriented, and creative/innovative style.</li>
<li>Proactive, optimistic approach to problem solving.</li>
<li>Commitment to constant personal and organizational improvement.</li>
<li>Willingness to travel to partner sites as needed.</li>
<li>Lean and Agile thinking and training. Experienced in Scrum or kanban.</li>
<li>Bachelors or Master’s in Education, organizational learning, or other related field preferred. But we're all about education, so let us know how you gained what you need to succeed in this role: projects after completing 6.00x or CS50x, Xbox cheevos, on-line guilds led, large scale innovations championed.</li>
</ul>
<p>If you are interested in this position, please send an email to <a href="mailto:jobs@edx.org">jobs@edx.org</a>.</p>
</div>
</article>
</section>
<section class="jobs-sidebar">
<h2>Positions</h2>
<nav>
<a href="#director-of-education-services">Director of Education Services</a>
<a href="#director-of-education-services">Vice President/Director of Education Services</a>
<a href="#manager-of-training-services">Manager of Training Services</a>
<a href="#trainer">Trainer</a>
<a href="#instructional-designer">Instructional Designer</a>
......@@ -435,6 +498,7 @@
<a href="#software-engineer">Software Engineer</a>
<a href="#devops-engineer-systems-administrator">Devops Engineer - Systems Administrator</a>
<a href="#learning-sciences-engineer">Learning Sciences Engineer</a>
<a href="#sales-engineer">Sales Engineer, Business Development Team</a>
</nav>
<h2>How to Apply</h2>
<p>E-mail your resume, cover letter and any other materials to <a href="mailto:jobs@edx.org">jobs@edx.org</a></p>
......
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