Commit 07b9d635 by Chris Dodge

Merge branch 'master' of github.com:MITx/mitx into fix/cdodge/studio-forum-improvements

parents 5b7d882e 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]
......
......@@ -21,8 +21,7 @@ Feature: Advanced (manual) course policy
Scenario: Test editing key value
Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key
And I press the "Save" notification button
When I edit the value of a policy key and save
Then the policy key value is changed
And I reload the page
Then the policy key value is changed
......
......@@ -51,6 +51,11 @@ def edit_the_value_of_a_policy_key(step):
e._element.send_keys(Keys.TAB, Keys.END, Keys.ARROW_LEFT, ' ', 'X')
@step(u'I edit the value of a policy key and save$')
def edit_the_value_of_a_policy_key(step):
change_display_name_value(step, '"foo"')
@step('I create a JSON object as a value$')
def create_JSON_object(step):
change_display_name_value(step, '{"key": "value", "key_2": "value_2"}')
......@@ -96,7 +101,7 @@ def the_policy_key_value_is_unchanged(step):
@step(u'the policy key value is changed$')
def the_policy_key_value_is_changed(step):
assert_equal(get_display_name_value(), '"Robot Super Course X"')
assert_equal(get_display_name_value(), '"foo"')
############# HELPERS ###############
......
......@@ -18,8 +18,8 @@ COURSE_END_TIME_CSS = "#course-end-time"
ENROLLMENT_START_TIME_CSS = "#course-enrollment-start-time"
ENROLLMENT_END_TIME_CSS = "#course-enrollment-end-time"
DUMMY_TIME = "3:30pm"
DEFAULT_TIME = "12:00am"
DUMMY_TIME = "15:30"
DEFAULT_TIME = "00:00"
############### ACTIONS ####################
......
......@@ -38,7 +38,7 @@ def i_click_the_edit_link_for_the_release_date(step):
@step('I save a new section release date$')
def i_save_a_new_section_release_date(step):
set_date_and_time('input.start-date.date.hasDatepicker', '12/25/2013',
'input.start-time.time.ui-timepicker-input', '12:00am')
'input.start-time.time.ui-timepicker-input', '00:00')
world.browser.click_link_by_text('Save')
......@@ -105,7 +105,7 @@ def the_section_release_date_picker_not_visible(step):
def the_section_release_date_is_updated(step):
css = 'span.published-status'
status_text = world.css_text(css)
assert_equal(status_text, 'Will Release: 12/25/2013 at 12:00am')
assert_equal(status_text, 'Will Release: 12/25/2013 at 00:00 UTC')
############ HELPER METHODS ###################
......
......@@ -57,18 +57,18 @@ def i_see_complete_subsection_name_with_quote_in_editor(step):
@step('I have set a release date and due date in different years$')
def test_have_set_dates_in_different_years(step):
set_date_and_time('input#start_date', '12/25/2011', 'input#start_time', '3:00am')
set_date_and_time('input#start_date', '12/25/2011', 'input#start_time', '03:00')
world.css_click('.set-date')
# Use a year in the past so that current year will always be different.
set_date_and_time('input#due_date', '01/02/2012', 'input#due_time', '4:00am')
set_date_and_time('input#due_date', '01/02/2012', 'input#due_time', '04:00')
@step('I see the correct dates$')
def i_see_the_correct_dates(step):
assert_equal('12/25/2011', world.css_find('input#start_date').first.value)
assert_equal('3:00am', world.css_find('input#start_time').first.value)
assert_equal('03:00', world.css_find('input#start_time').first.value)
assert_equal('01/02/2012', world.css_find('input#due_date').first.value)
assert_equal('4:00am', world.css_find('input#due_time').first.value)
assert_equal('04:00', world.css_find('input#due_time').first.value)
@step('I mark it as Homework$')
......
......@@ -93,6 +93,69 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
return cnt
def test_draft_metadata(self):
'''
This verifies a bug we had where inherited metadata was getting written to the
module as 'own-metadata' when publishing. Also verifies the metadata inheritance is
properly computed
'''
store = modulestore()
draft_store = modulestore('draft')
import_from_xml(store, 'common/test/data/', ['simple'])
course = draft_store.get_item(Location(['i4x', 'edX', 'simple',
'course', '2012_Fall', None]), depth=None)
html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None])
self.assertEqual(html_module.lms.graceperiod, course.lms.graceperiod)
self.assertNotIn('graceperiod', own_metadata(html_module))
draft_store.clone_item(html_module.location, html_module.location)
# refetch to check metadata
html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None])
self.assertEqual(html_module.lms.graceperiod, course.lms.graceperiod)
self.assertNotIn('graceperiod', own_metadata(html_module))
# publish module
draft_store.publish(html_module.location, 0)
# refetch to check metadata
html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None])
self.assertEqual(html_module.lms.graceperiod, course.lms.graceperiod)
self.assertNotIn('graceperiod', own_metadata(html_module))
# put back in draft and change metadata and see if it's now marked as 'own_metadata'
draft_store.clone_item(html_module.location, html_module.location)
html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None])
new_graceperiod = timedelta(**{'hours': 1})
self.assertNotIn('graceperiod', own_metadata(html_module))
html_module.lms.graceperiod = new_graceperiod
self.assertIn('graceperiod', own_metadata(html_module))
self.assertEqual(html_module.lms.graceperiod, new_graceperiod)
draft_store.update_metadata(html_module.location, own_metadata(html_module))
# read back to make sure it reads as 'own-metadata'
html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None])
self.assertIn('graceperiod', own_metadata(html_module))
self.assertEqual(html_module.lms.graceperiod, new_graceperiod)
# republish
draft_store.publish(html_module.location, 0)
# and re-read and verify 'own-metadata'
draft_store.clone_item(html_module.location, html_module.location)
html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None])
self.assertIn('graceperiod', own_metadata(html_module))
self.assertEqual(html_module.lms.graceperiod, new_graceperiod)
def test_get_depth_with_drafts(self):
import_from_xml(modulestore(), 'common/test/data/', ['simple'])
......@@ -566,6 +629,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')
......
......@@ -121,6 +121,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>');
......@@ -826,7 +846,7 @@ function saveSetSectionScheduleDate(e) {
data: JSON.stringify({ 'id': id, 'metadata': {'start': start}})
}).success(function () {
var $thisSection = $('.courseware-section[data-id="' + id + '"]');
$thisSection.find('.section-published-date').html('<span class="published-status"><strong>Will Release:</strong> ' + input_date + ' at ' + input_time + '</span><a href="#" class="edit-button" data-date="' + input_date + '" data-time="' + input_time + '" data-id="' + id + '">Edit</a>');
$thisSection.find('.section-published-date').html('<span class="published-status"><strong>Will Release:</strong> ' + input_date + ' at ' + input_time + ' UTC</span><a href="#" class="edit-button" data-date="' + input_date + '" data-time="' + input_time + '" data-id="' + id + '">Edit</a>');
$thisSection.find('.section-published-date').animate({
'background-color': 'rgb(182,37,104)'
}, 300).animate({
......@@ -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
});
......@@ -110,7 +110,7 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
};
// instrument as date and time pickers
timefield.timepicker();
timefield.timepicker({'timeFormat' : 'H:i'});
datefield.datepicker();
// Using the change event causes savefield to be triggered twice, but it is necessary
......
......@@ -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; }
......@@ -62,6 +62,12 @@ table {
border-spacing: 0;
}
abbr[title] {
border-bottom: none;
text-decoration: none;
cursor: help;
}
// ====================
// grandfathered styles
......
......@@ -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 {
......
......@@ -604,13 +604,39 @@ body.course.outline {
}
.picker {
@include clearfix();
margin: 30px 0 65px;
.field {
float: left;
margin-right: ($baseline/2);
&:first-child {
margin-left: ($baseline*5);
}
&:last-child {
margin-right: 0;
}
label, input {
display: block;
text-align: left;
}
label {
@include font-size(14);
margin-bottom: ($baseline/4);
}
}
}
.description {
float: left;
margin-top: 30px;
font-size: 14px;
line-height: 20px;
width: 100%;
}
strong {
......
......@@ -3,11 +3,41 @@
body.course.subsection {
.main-wrapper {
margin-top: ($baseline*2);
}
.unit-settings {
.window-contents {
padding: 10px 20px;
}
.datepair {
.field {
display: inline-block;
margin-right: ($baseline/4);
width: 45%;
&:last-child {
margin-right: 0;
}
label, input {
display: block;
text-align: left;
}
input {
width: 100%;
}
label {
margin-bottom: ($baseline/4);
}
}
}
.unit-actions {
border-bottom: none;
padding-bottom: 0;
......@@ -74,7 +104,7 @@ body.course.subsection {
}
.window-contents {
display: none;
display: none;
}
}
......@@ -232,6 +262,7 @@ body.course.subsection {
.remove-date {
display: block;
margin-top: ($baseline/4);
}
}
......@@ -259,7 +290,7 @@ body.course.subsection {
background-position: 0 -50px;
.hidden {
background-position: 0 -5px;
background-position: 0 -5px;
}
}
}
......@@ -369,4 +400,4 @@ body.course.subsection {
}
}
}
}
\ No newline at end of file
}
......@@ -3,9 +3,8 @@
body.course.unit {
.unit .main-wrapper {
@include clearfix();
margin: 40px;
.main-wrapper {
margin-top: ($baseline*2);
}
//Problem Selector tab menu requirements
......@@ -31,7 +30,7 @@ body.course.unit {
}
.unit-body {
.unit-name-input {
padding: 20px 40px;
......@@ -44,7 +43,7 @@ body.course.unit {
font-size: 20px;
}
}
.breadcrumbs {
border-radius: 3px 3px 0 0;
border-bottom: 1px solid #cbd1db;
......@@ -189,10 +188,10 @@ body.course.unit {
@include clearfix;
a {
position: relative;
position: relative;
border: 1px solid $darkGreen;
background: tint($green,20%);
color: #fff;
color: #fff;
&:hover {
background: $brightGreen;
......@@ -254,8 +253,8 @@ body.course.unit {
@include transition (none);
&:hover {
background: tint($green,30%);
color: #fff;
background: tint($green,30%);
color: #fff;
@include transition(background-color .15s);
}
}
......@@ -263,7 +262,7 @@ body.course.unit {
li {
border:none;
border-bottom: 1px dashed $lightGrey;
color: #fff;
color: #fff;
}
li:first-child {
......@@ -326,7 +325,7 @@ body.course.unit {
}
}
// specific editor types
// specific editor types
.empty {
a {
......@@ -337,20 +336,20 @@ body.course.unit {
&:hover {
background: tint($green,30%);
background: tint($green,30%);
color: #fff;
}
}
}
}
.new-component {
.new-component {
text-align: center;
h5 {
color: $darkGreen;
}
}
}
}
......@@ -374,7 +373,7 @@ body.course.unit {
&.editing {
border: 1px solid $lightBluishGrey2;
z-index: auto;
.drag-handle,
.component-actions {
display: none;
......@@ -434,7 +433,7 @@ body.course.unit {
label {
display: inline-block;
margin-right: 10px;
margin-right: 10px;
}
}
......@@ -528,7 +527,7 @@ body.course.unit {
}
.window-contents {
display: none;
display: none;
}
}
......@@ -678,4 +677,4 @@ body.unit {
padding-top: 0;
}
}
}
\ No newline at end of file
}
......@@ -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>
......
......@@ -33,17 +33,22 @@
<h4 class="header">Subsection Settings</h4>
<div class="window-contents">
<div class="scheduled-date-input row">
<label>Release date:<!-- <span class="description">Determines when this subsection and the units within it will be released publicly.</span>--></label>
<div class="datepair" data-language="javascript">
<input type="text" id="start_date" name="start_date" value="${get_time_struct_display(subsection.lms.start, '%m/%d/%Y')}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
<input type="text" id="start_time" name="start_time" value="${get_time_struct_display(subsection.lms.start, '%H:%M')}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
<div class="field field-start-date">
<label for="start_date">Release Day</label>
<input type="text" id="start_date" name="start_date" value="${get_time_struct_display(subsection.lms.start, '%m/%d/%Y')}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
</div>
<div class="field field-start-time">
<label for="start_time">Release Time (<abbr title="Coordinated Universal Time">UTC</abbr>)</label>
<input type="text" id="start_time" name="start_time" value="${get_time_struct_display(subsection.lms.start, '%H:%M')}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
</div>
</div>
% if subsection.lms.start != parent_item.lms.start and subsection.lms.start:
% if parent_item.lms.start is None:
<p class="notice">The date above differs from the release date of ${parent_item.display_name_with_default}, which is unset.
% else:
<p class="notice">The date above differs from the release date of ${parent_item.display_name_with_default} –
${get_time_struct_display(parent_item.lms.start, '%m/%d/%Y at %I:%M %p')}.
${get_time_struct_display(parent_item.lms.start, '%m/%d/%Y at %H:%M UTC')}.
% endif
<a href="#" class="sync-date no-spinner">Sync to ${parent_item.display_name_with_default}.</a></p>
% endif
......@@ -56,14 +61,17 @@
</div>
<div class="due-date-input row">
<label>Due date:</label>
<a href="#" class="set-date">Set a due date</a>
<div class="datepair date-setter">
<p class="date-description">
<div class="field field-start-date">
<label for="due_date">Due Day</label>
<input type="text" id="due_date" name="due_date" value="${get_time_struct_display(subsection.lms.due, '%m/%d/%Y')}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
</div>
<div class="field field-start-time">
<label for="due_time">Due Time (<abbr title="Coordinated Universal Time">UTC</abbr>)</label>
<input type="text" id="due_time" name="due_time" value="${get_time_struct_display(subsection.lms.due, '%H:%M')}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
<a href="#" class="remove-date">Remove due date</a>
</p>
</div>
<a href="#" class="remove-date">Remove due date</a>
</div>
</div>
<div class="row unit-actions">
......
......@@ -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) {
......@@ -148,13 +148,13 @@
<div class="section-published-date">
<%
start_date_str = get_time_struct_display(section.lms.start, '%m/%d/%Y')
start_time_str = get_time_struct_display(section.lms.start, '%I:%M %p')
start_time_str = get_time_struct_display(section.lms.start, '%H:%M')
%>
%if section.lms.start is None:
<span class="published-status">This section has not been released.</span>
<a href="#" class="schedule-button" data-date="" data-time="" data-id="${section.location}">Schedule</a>
%else:
<span class="published-status"><strong>Will Release:</strong> ${start_date_str} at ${start_time_str}</span>
<span class="published-status"><strong>Will Release:</strong> ${get_time_struct_display(section.lms.start, '%m/%d/%Y at %H:%M UTC')}</span>
<a href="#" class="edit-button" data-date="${start_date_str}" data-time="${start_time_str}" data-id="${section.location}">Edit</a>
%endif
</div>
......@@ -202,13 +202,20 @@
</div>
</div>
<footer></footer>
<div class="edit-subsection-publish-settings">
<div class="settings">
<h3>Section Release Date</h3>
<div class="picker datepair">
<input class="start-date date" type="text" name="start_date" value="" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
<input class="start-time time" type="text" name="start_time" value="" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
<div class="field field-start-date">
<label for="">Release Day</label>
<input class="start-date date" type="text" name="start_date" value="" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
</div>
<div class="field field-start-time">
<label for="">Release Time (<abbr title="Coordinated Universal Time">UTC</abbr>)</label>
<input class="start-time time" type="text" name="start_time" value="" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
</div>
<div class="description">
<p>On the date set above, this section – <strong class="section-name"></strong> – will be released to students. Any units marked private will only be visible to admins.</p>
</div>
......
......@@ -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:
......
......@@ -108,6 +108,22 @@ def add_histogram(get_html, module, user):
histogram = grade_histogram(module_id)
render_histogram = len(histogram) > 0
if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'):
[filepath, filename] = getattr(module.descriptor, 'xml_attributes', {}).get('filename', ['', None])
osfs = module.system.filestore
if filename is not None and osfs.exists(filename):
# if original, unmangled filename exists then use it (github
# doesn't like symlinks)
filepath = filename
data_dir = osfs.root_path.rsplit('/')[-1]
giturl = getattr(module.lms, 'giturl', '') or 'https://github.com/MITx'
edit_link = "%s/%s/tree/master/%s" % (giturl, data_dir, filepath)
else:
edit_link = False
# Need to define all the variables that are about to be used
giturl = ""
data_dir = ""
source_file = module.lms.source_file # source used to generate the problem XML, eg latex or word
# useful to indicate to staff if problem has been released or not
......@@ -121,12 +137,15 @@ def add_histogram(get_html, module, user):
staff_context = {'fields': [(field.name, getattr(module, field.name)) for field in module.fields],
'lms_fields': [(field.name, getattr(module.lms, field.name)) for field in module.lms.fields],
'xml_attributes' : getattr(module.descriptor, 'xml_attributes', {}),
'location': module.location,
'xqa_key': module.lms.xqa_key,
'source_file': source_file,
'source_url': '%s/%s/tree/master/%s' % (giturl, data_dir, source_file),
'category': str(module.__class__.__name__),
# Template uses element_id in js function names, so can't allow dashes
'element_id': module.location.html_id().replace('-', '_'),
'edit_link': edit_link,
'user': user,
'xqa_server': settings.MITX_FEATURES.get('USE_XQA_SERVER', 'http://xqa:server@content-qa.mitx.mit.edu/xqa'),
'histogram': json.dumps(histogram),
......
......@@ -180,6 +180,7 @@ class CourseFields(object):
has_children = True
checklists = List(scope=Scope.settings)
info_sidebar_name = String(scope=Scope.settings, default='Course Handouts')
show_timezone = Boolean(help="True if timezones should be shown on dates in the courseware", scope=Scope.settings, default=True)
# An extra property is used rather than the wiki_slug/number because
# there are courses that change the number for different runs. This allows
......
......@@ -2,6 +2,7 @@ from datetime import datetime
from . import ModuleStoreBase, Location, namedtuple_to_son
from .exceptions import ItemNotFoundError
from .inheritance import own_metadata
import logging
DRAFT = 'draft'
......@@ -181,7 +182,7 @@ class DraftModuleStore(ModuleStoreBase):
draft.cms.published_by = published_by_id
super(DraftModuleStore, self).update_item(location, draft._model_data._kvs._data)
super(DraftModuleStore, self).update_children(location, draft._model_data._kvs._children)
super(DraftModuleStore, self).update_metadata(location, draft._model_data._kvs._metadata)
super(DraftModuleStore, self).update_metadata(location, own_metadata(draft))
self.delete_item(location)
def unpublish(self, location):
......
......@@ -8,7 +8,8 @@ INHERITABLE_METADATA = (
# How many days early to show a course element to beta testers (float)
# intended to be set per-course, but can be overridden in for specific
# elements. Can be a float.
'days_early_for_beta'
'days_early_for_beta',
'giturl' # for git edit link
)
def compute_inherited_metadata(descriptor):
......
......@@ -176,7 +176,10 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
model_data = DbModel(kvs, class_, None, MongoUsage(self.course_id, location))
module = class_(self, location, model_data)
if self.cached_metadata is not None:
metadata_to_inherit = self.cached_metadata.get(location.url(), {})
# parent container pointers don't differentiate between draft and non-draft
# so when we do the lookup, we should do so with a non-draft location
non_draft_loc = location._replace(revision=None)
metadata_to_inherit = self.cached_metadata.get(non_draft_loc.url(), {})
inherit_metadata(module, metadata_to_inherit)
return module
except:
......@@ -260,11 +263,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}
......@@ -282,6 +285,17 @@ class MongoModuleStore(ModuleStoreBase):
# now go through the results and order them by the location url
for result in resultset:
location = Location(result['_id'])
# We need to collate between draft and non-draft
# i.e. draft verticals can have children which are not in non-draft versions
location = location._replace(revision=None)
location_url = location.url()
if location_url in results_by_url:
existing_children = results_by_url[location_url].get('definition', {}).get('children', [])
additional_children = result.get('definition', {}).get('children', [])
total_children = existing_children + additional_children
if 'definition' not in results_by_url[location_url]:
results_by_url[location_url]['definition'] = {}
results_by_url[location_url]['definition']['children'] = total_children
results_by_url[location.url()] = result
if location.category == 'course':
root = location.url()
......@@ -293,17 +307,12 @@ class MongoModuleStore(ModuleStoreBase):
"""
Helper method for computing inherited metadata for a specific location url
"""
my_metadata = {}
# check for presence of metadata key. Note that a given module may not yet be fully formed.
# example: update_item -> update_children -> update_metadata sequence on new item create
# if we get called here without update_metadata called first then 'metadata' hasn't been set
# as we're not fully transactional at the DB layer. Same comment applies to below key name
# check
my_metadata = results_by_url[url].get('metadata', {})
for key in my_metadata.keys():
if key not in INHERITABLE_METADATA:
del my_metadata[key]
results_by_url[url]['metadata'] = my_metadata
# go through all the children and recurse, but only if we have
# in the result set. Remember results will not contain leaf nodes
......
......@@ -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
......
......@@ -5,6 +5,7 @@ from xmodule.util import date_utils
import datetime
import time
def test_get_time_struct_display():
assert_equals("", date_utils.get_time_struct_display(None, ""))
test_time = time.struct_time((1992, 3, 12, 15, 3, 30, 1, 71, 0))
......@@ -15,12 +16,20 @@ def test_get_time_struct_display():
def test_get_default_time_display():
assert_equals("", date_utils.get_default_time_display(None))
test_time = time.struct_time((1992, 3, 12, 15, 3, 30, 1, 71, 0))
assert_equals("Mar 12, 1992 at 03:03 PM",
assert_equals(
"Mar 12, 1992 at 15:03 UTC",
date_utils.get_default_time_display(test_time))
assert_equals(
"Mar 12, 1992 at 15:03 UTC",
date_utils.get_default_time_display(test_time, True))
assert_equals(
"Mar 12, 1992 at 15:03",
date_utils.get_default_time_display(test_time, False))
def test_time_to_datetime():
assert_equals(None, date_utils.time_to_datetime(None))
test_time = time.struct_time((1992, 3, 12, 15, 3, 30, 1, 71, 0))
assert_equals(datetime.datetime(1992, 3, 12, 15, 3, 30),
assert_equals(
datetime.datetime(1992, 3, 12, 15, 3, 30),
date_utils.time_to_datetime(test_time))
......@@ -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)
......
......@@ -2,15 +2,18 @@ import time
import datetime
def get_default_time_display(time_struct):
def get_default_time_display(time_struct, show_timezone=True):
"""
Converts a time struct to a string representation. This is the default
representation used in Studio and LMS.
It is of the form "Apr 09, 2013 at 04:00 PM".
It is of the form "Apr 09, 2013 at 16:00" or "Apr 09, 2013 at 16:00 UTC",
depending on the value of show_timezone.
If None is passed in, an empty string will be returned.
If None is passed in for time_struct, an empty string will be returned.
The default value of show_timezone is True.
"""
return get_time_struct_display(time_struct, "%b %d, %Y at %I:%M %p")
timezone = "" if time_struct is None or not show_timezone else " UTC"
return get_time_struct_display(time_struct, "%b %d, %Y at %H:%M") + timezone
def get_time_struct_display(time_struct, format):
......
......@@ -102,6 +102,7 @@ class XmlDescriptor(XModuleDescriptor):
'start', 'due', 'graded', 'display_name', 'url_name', 'hide_from_toc',
'ispublic', # if True, then course is listed for all users; see
'xqa_key', # for xqaa server access
'giturl', # url of git server for origin of file
# information about testcenter exams is a dict (of dicts), not a string,
# so it cannot be easily exportable as a course element's attribute.
'testcenter_info',
......@@ -222,6 +223,7 @@ class XmlDescriptor(XModuleDescriptor):
definition, children = cls.definition_from_xml(definition_xml, system)
if definition_metadata:
definition['definition_metadata'] = definition_metadata
definition['filename'] = [ filepath, filename ]
return definition, children
......@@ -315,6 +317,7 @@ class XmlDescriptor(XModuleDescriptor):
model_data['children'] = children
model_data['xml_attributes'] = {}
model_data['xml_attributes']['filename'] = definition.get('filename', ['', None]) # for git link
for key, value in metadata.items():
if key not in set(f.name for f in cls.fields + cls.lms.fields):
model_data['xml_attributes'][key] = value
......
......@@ -24,7 +24,7 @@ $(function() {
$('.datepair input.time').each(function() {
var $this = $(this);
var opts = { 'showDuration': true, 'timeFormat': 'g:ia', 'scrollDefaultNow': true };
var opts = { 'showDuration': true, 'timeFormat': 'H:i', 'scrollDefaultNow': true };
if ($this.hasClass('start') || $this.hasClass('end')) {
opts.onSelect = doDatepair;
......
......@@ -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,13 +12,20 @@
</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" url_name='test_sequence'>
<vertical url_name='test_vertical'>
<html url_name='test_html'>
Foobar
</html>
</vertical>
</sequential>
</chapter>
</course>
......@@ -12,7 +12,6 @@ from django.contrib.auth.decorators import login_required
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.shortcuts import redirect
from mitxmako.shortcuts import render_to_response, render_to_string
#from django.views.decorators.csrf import ensure_csrf_cookie
from django_future.csrf import ensure_csrf_cookie
from django.views.decorators.cache import cache_control
......@@ -67,9 +66,9 @@ def user_groups(user):
@ensure_csrf_cookie
@cache_if_anonymous
def courses(request):
'''
"""
Render "find courses" page. The course selection work is done in courseware.courses.
'''
"""
courses = get_courses(request.user, request.META.get('HTTP_HOST'))
courses = sort_by_announcement(courses)
......@@ -77,14 +76,16 @@ def courses(request):
def render_accordion(request, course, chapter, section, model_data_cache):
''' Draws navigation bar. Takes current position in accordion as
parameter.
"""
Draws navigation bar. Takes current position in accordion as
parameter.
If chapter and section are '' or None, renders a default accordion.
If chapter and section are '' or None, renders a default accordion.
course, chapter, and section are the url_names.
course, chapter, and section are the url_names.
Returns the html string'''
Returns the html string
"""
# grab the table of contents
user = User.objects.prefetch_related("groups").get(id=request.user.id)
......@@ -92,7 +93,8 @@ def render_accordion(request, course, chapter, section, model_data_cache):
context = dict([('toc', toc),
('course_id', course.id),
('csrf', csrf(request)['csrf_token'])] + template_imports.items())
('csrf', csrf(request)['csrf_token']),
('show_timezone', course.show_timezone)] + template_imports.items())
return render_to_string('courseware/accordion.html', context)
......@@ -166,10 +168,10 @@ def save_child_position(seq_module, child_name):
def check_for_active_timelimit_module(request, course_id, course):
'''
"""
Looks for a timing module for the given user and course that is currently active.
If found, returns a context dict with timer-related values to enable display of time remaining.
'''
"""
context = {}
# TODO (cpennington): Once we can query the course structure, replace this with such a query
......@@ -201,11 +203,11 @@ def check_for_active_timelimit_module(request, course_id, course):
def update_timelimit_module(user, course_id, model_data_cache, timelimit_descriptor, timelimit_module):
'''
"""
Updates the state of the provided timing module, starting it if it hasn't begun.
Returns dict with timer-related values to enable display of time remaining.
Returns 'timer_expiration_duration' in dict if timer is still active, and not if timer has expired.
'''
"""
context = {}
# determine where to go when the exam ends:
if timelimit_descriptor.time_expired_redirect_url is None:
......@@ -391,14 +393,14 @@ def index(request, course_id, chapter=None, section=None,
@ensure_csrf_cookie
def jump_to(request, course_id, location):
'''
"""
Show the page that contains a specific location.
If the location is invalid or not in any class, return a 404.
Otherwise, delegates to the index view to figure out whether this user
has access, and what they should see.
'''
"""
# Complain if the location isn't valid
try:
location = Location(location)
......@@ -486,7 +488,9 @@ def syllabus(request, course_id):
def registered_for_course(course, user):
'''Return CourseEnrollment if user is registered for course, else False'''
"""
Return CourseEnrollment if user is registered for course, else False
"""
if user is None:
return False
if user.is_authenticated():
......
......@@ -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)
......@@ -93,13 +88,13 @@ def instructor_dashboard(request, course_id):
data += compute_course_stats(course).items()
if request.user.is_staff:
for field in course.fields:
if getattr(field.scope, 'student', False):
if getattr(field.scope, 'user', False):
continue
data.append([field.name, json.dumps(field.read_json(course))])
for namespace in course.namespaces:
for field in getattr(course, namespace).fields:
if getattr(field.scope, 'student', False):
if getattr(field.scope, 'user', False):
continue
data.append(["{}.{}".format(namespace, field.name), json.dumps(field.read_json(course))])
......@@ -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,13 +1147,13 @@ 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)
for sec in gsvals:
s = sec['section_descriptor']
format = getattr(s, 'format', None)
format = getattr(s.lms, 'format', None)
aname = ''
if format in graders:
g = graders[format]
......
......@@ -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,
......
......@@ -11,7 +11,7 @@
<li class="${'active' if 'active' in section and section['active'] else ''} ${'graded' if 'graded' in section and section['graded'] else ''}">
<a href="${reverse('courseware_section', args=[course_id, chapter['url_name'], section['url_name']])}">
<p>${section['display_name']}</p>
<p class="subtitle">${section['format']} ${"due " + get_default_time_display(section['due']) if section.get('due') is not None else ''}</p>
<p class="subtitle">${section['format']} ${"due " + get_default_time_display(section['due'], show_timezone) if section.get('due') is not None else ''}</p>
</a>
</li>
% endfor
......
......@@ -64,7 +64,7 @@ ${progress_graph.body(grade_summary, course.grade_cutoffs, "grade-detail-graph",
%if section.get('due') is not None:
<em>
due ${get_default_time_display(section['due'])}
due ${get_default_time_display(section['due'], course.show_timezone)}
</em>
%endif
</p>
......
## The JS for this is defined in xqa_interface.html
${module_content}
%if location.category in ['problem','video','html']:
%if location.category in ['problem','video','html','combinedopenended']:
% if edit_link:
<div>
<a href="${edit_link}">Edit</a> /
<a href="#${element_id}_xqa-modal" onclick="javascript:getlog('${element_id}', {
<a href="${edit_link}">Edit</a>
% if xqa_key:
/ <a href="#${element_id}_xqa-modal" onclick="javascript:getlog('${element_id}', {
'location': '${location}',
'xqa_key': '${xqa_key}',
'category': '${category}',
'user': '${user}'
})" id="${element_id}_xqa_log">QA</a>
% endif
</div>
% endif
<div><a href="#${element_id}_debug" id="${element_id}_trig">Staff Debug Info</a></div>
......@@ -61,6 +63,12 @@ location = ${location | h}
<tr><td>${name}</td><td><pre style="display:inline-block; margin: 0;">${field | h}</pre></td></tr>
%endfor
</table>
<table>
<tr><th>XML attributes</th></tr>
%for name, field in xml_attributes.items():
<tr><td>${name}</td><td><pre style="display:inline-block; margin: 0;">${field | h}</pre></td></tr>
%endfor
</table>
category = ${category | h}
</div>
%if render_histogram:
......
......@@ -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>
......
......@@ -52,7 +52,8 @@ class LmsNamespace(Namespace):
start = Date(help="Start time when this module is visible", scope=Scope.settings)
due = Date(help="Date that this problem is due by", scope=Scope.settings)
source_file = String(help="DO NOT USE", scope=Scope.settings)
source_file = String(help="source file name (eg for latex)", scope=Scope.settings)
giturl = String(help="url root for course data git repository", scope=Scope.settings)
xqa_key = String(help="DO NOT USE", scope=Scope.settings)
ispublic = Boolean(help="Whether this course is open to the public, or only to admins", scope=Scope.settings)
graceperiod = Timedelta(
......
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