diff --git a/README b/README
deleted file mode 100644
index 2ed50ba063..0000000000
--- a/README
+++ /dev/null
@@ -1 +0,0 @@
-See doc/ for documentation.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000..ec17d7c9a4
--- /dev/null
+++ b/README.md
@@ -0,0 +1,148 @@
+This is edX, a platform for online course delivery. The project is primarily
+written in [Python](http://python.org/), using the
+[Django](https://www.djangoproject.com/) framework. We also use some
+[Ruby](http://www.ruby-lang.org/) and some [NodeJS](http://nodejs.org/).
+
+Installation
+============
+The installation process is a bit messy at the moment. Here's a high-level
+overview of what you should do to get started.
+
+**TLDR:** There is a `create-dev-env.sh` script that will attempt to set all
+of this up for you. If you're in a hurry, run that script. Otherwise, I suggest
+that you understand what the script is doing, and why, by reading this document.
+
+Directory Hierarchy
+-------------------
+This code assumes that it is checked out in a directory that has three sibling
+directories: `data` (used for XML course data), `db` (used to hold a
+[sqlite](https://sqlite.org/) database), and `log` (used to hold logs). If you
+clone the repository into a directory called `edx` inside of a directory
+called `dev`, here's an example of how the directory hierarchy should look:
+
+ * dev
+ \
+ * data
+ * db
+ * log
+ * edx
+ \
+ README.md
+
+Language Runtimes
+-----------------
+You'll need to be sure that you have Python 2.7, Ruby 1.9.3, and NodeJS
+(latest stable) installed on your system. Some of these you can install
+using your system's package manager: [homebrew](http://mxcl.github.io/homebrew/)
+for Mac, [apt](http://wiki.debian.org/Apt) for Debian-based systems
+(including Ubuntu), [rpm](http://www.rpm.org/) or [yum](http://yum.baseurl.org/)
+for Red Hat based systems (including CentOS).
+
+If your system's package manager gives you the wrong version of a language
+runtime, then you'll need to use a versioning tool to install the correct version.
+Usually, you'll need to do this for Ruby: you can use
+[`rbenv`](https://github.com/sstephenson/rbenv) or [`rvm`](https://rvm.io/), but
+typically `rbenv` is simpler. For Python, you can use
+[`pythonz`](http://saghul.github.io/pythonz/),
+and for Node, you can use [`nvm`](https://github.com/creationix/nvm).
+
+Virtual Environments
+--------------------
+Often, different projects will have conflicting dependencies: for example, two
+projects depending on two different, incompatible versions of a library. Clearly,
+you can't have both versions installed and used on your machine simultaneously.
+Virtual environments were created to solve this problem: by installing libraries
+into an isolated environment, only projects that live inside the environment
+will be able to see and use those libraries. Got incompatible dependencies? Use
+different virtual environments, and your problem is solved.
+
+Remember, each language has a different implementation. Python has
+[`virtualenv`](http://www.virtualenv.org/), Ruby has
+[`bundler`](http://gembundler.com/), and Node's virtual environment support
+is built into [`npm`](https://npmjs.org/), its library management tool.
+For each language, decide if you want to use a virtual environment, or if you
+want to install all the language dependencies globally (and risk conflicts).
+I suggest you start with installing things globally until and unless things
+break; you can always switch over to a virtual environment later on.
+
+Language Packages
+-----------------
+The Python libraries we use are listed in `requirements.txt`. The Ruby libraries
+we use are listed in `Gemfile`. The Node libraries we use are listed in
+`packages.json`. Python has a library installer called
+[`pip`](http://www.pip-installer.org/), Ruby has a library installer called
+[`gem`](https://rubygems.org/) (or `bundle` if you're using a virtual
+environment), and Node has a library installer called
+[`npm`](https://npmjs.org/).
+Once you've got your languages and virtual environments set up, install
+the libraries like so:
+
+ $ pip install -r pre-requirements.txt
+ $ pip install -r requirements.txt
+ $ bundle install
+ $ npm install
+
+Other Dependencies
+------------------
+You'll also need to install [MongoDB](http://www.mongodb.org/), since our
+application uses it in addition to sqlite. You can install it through your
+system package manager, and I suggest that you configure it to start
+automatically when you boot up your system, so that you never have to worry
+about it again. For Mac, use
+[`launchd`](https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man8/launchd.8.html)
+(running `brew info mongodb` will give you some commands you can copy-paste.)
+For Linux, you can use [`upstart`](http://upstart.ubuntu.com/), `chkconfig`,
+or any other process management tool.
+
+Configuring Your Project
+------------------------
+We use [`rake`](http://rake.rubyforge.org/) to execute common tasks in our
+project. The `rake` tasks are defined in the `rakefile`, or you can run `rake -T`
+to view a summary.
+
+Before you run your project, you need to create a sqlite database, create
+tables in that database, run database migrations, and populate templates for
+CMS templates. Fortunately, `rake` will do all of this for you! Just run:
+
+ $ rake django-admin[syncdb]
+ $ rake django-admin[migrate]
+ $ rake django-admin[update_templates]
+
+If you are running these commands using the [`zsh`](http://www.zsh.org/) shell,
+zsh will assume that you are doing
+[shell globbing](https://en.wikipedia.org/wiki/Glob_(programming)), search for
+a file in your directory named `django-adminsyncdb` or `django-adminmigrate`,
+and fail. To fix this, just surround the argument with quotation marks, so that
+you're running `rake "django-admin[syncdb]"`.
+
+Run Your Project
+----------------
+edX has two components: Studio, the course authoring system; and the LMS
+(learning management system) used by students. These two systems communicate
+through the MongoDB database, which stores course information.
+
+To run Studio, run:
+
+ $ rake cms
+
+To run the LMS, run:
+
+ $ rake lms[cms.dev]
+
+Studio runs on port 8001, while LMS runs on port 8000, so you can run both of
+these commands simultaneously, using two different terminal windows. To view
+Studio, visit `127.0.0.1:8001` in your web browser; to view the LMS, visit
+`127.0.0.1:8000`.
+
+There's also an older version of the LMS that saves its information in XML files
+in the `data` directory, instead of in Mongo. To run this older version, run:
+
+$ rake lms
+
+Further Documentation
+=====================
+Once you've got your project up and running, you can check out the `docs`
+directory to see more documentation about how edX is structured.
+
+
+
diff --git a/cms/djangoapps/contentstore/module_info_model.py b/cms/djangoapps/contentstore/module_info_model.py
index 91f722a699..f7d1bbd8fe 100644
--- a/cms/djangoapps/contentstore/module_info_model.py
+++ b/cms/djangoapps/contentstore/module_info_model.py
@@ -75,11 +75,7 @@ def set_module_info(store, location, post_data):
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
for metadata_key, value in posted_metadata.items():
- # let's strip out any metadata fields from the postback which have been identified as system metadata
- # and therefore should not be user-editable, so we should accept them back from the client
- if metadata_key in module.system_metadata_fields:
- del posted_metadata[metadata_key]
- elif posted_metadata[metadata_key] is None:
+ if posted_metadata[metadata_key] is None:
# remove both from passed in collection as well as the collection read in from the modulestore
if metadata_key in module._model_data:
del module._model_data[metadata_key]
diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py
index caf3901e03..824d2119f1 100644
--- a/cms/djangoapps/contentstore/views.py
+++ b/cms/djangoapps/contentstore/views.py
@@ -676,11 +676,7 @@ def save_item(request):
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
for metadata_key, value in posted_metadata.items():
- # let's strip out any metadata fields from the postback which have been identified as system metadata
- # and therefore should not be user-editable, so we should accept them back from the client
- if metadata_key in existing_item.system_metadata_fields:
- del posted_metadata[metadata_key]
- elif posted_metadata[metadata_key] is None:
+ if posted_metadata[metadata_key] is None:
# remove both from passed in collection as well as the collection read in from the modulestore
if metadata_key in existing_item._model_data:
del existing_item._model_data[metadata_key]
diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py
index 4429e35692..708e79f0a3 100644
--- a/cms/djangoapps/models/settings/course_metadata.py
+++ b/cms/djangoapps/models/settings/course_metadata.py
@@ -14,13 +14,14 @@ class CourseMetadata(object):
The objects have no predefined attrs but instead are obj encodings of the
editable metadata.
'''
- FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start',
- 'end',
- 'enrollment_start',
- 'enrollment_end',
- 'tabs',
- 'graceperiod',
- 'checklists']
+ FILTERED_LIST = ['xml_attributes',
+ 'start',
+ 'end',
+ 'enrollment_start',
+ 'enrollment_end',
+ 'tabs',
+ 'graceperiod',
+ 'checklists']
@classmethod
def fetch(cls, course_location):
diff --git a/cms/static/coffee/spec/views/module_edit_spec.coffee b/cms/static/coffee/spec/views/module_edit_spec.coffee
index 5e83ecb42d..baf9ee9c20 100644
--- a/cms/static/coffee/spec/views/module_edit_spec.coffee
+++ b/cms/static/coffee/spec/views/module_edit_spec.coffee
@@ -72,3 +72,14 @@ describe "CMS.Views.ModuleEdit", ->
it "loads the .xmodule-display inside the module editor", ->
expect(XModule.loadModule).toHaveBeenCalled()
expect(XModule.loadModule.mostRecentCall.args[0]).toBe($('.xmodule_display'))
+
+ describe "changedMetadata", ->
+ it "returns empty if no metadata loaded", ->
+ expect(@moduleEdit.changedMetadata()).toEqual({})
+
+ it "returns only changed values", ->
+ @moduleEdit.originalMetadata = {'foo', 'bar'}
+ spyOn(@moduleEdit, 'metadata').andReturn({'a': '', 'b': 'before', 'c': ''})
+ @moduleEdit.loadEdit()
+ @moduleEdit.metadata.andReturn({'a': '', 'b': 'after', 'd': 'only_after'})
+ expect(@moduleEdit.changedMetadata()).toEqual({'b' : 'after', 'd' : 'only_after'})
diff --git a/cms/static/coffee/src/views/module_edit.coffee b/cms/static/coffee/src/views/module_edit.coffee
index 3cb3b1703f..bf56807f66 100644
--- a/cms/static/coffee/src/views/module_edit.coffee
+++ b/cms/static/coffee/src/views/module_edit.coffee
@@ -20,6 +20,7 @@ class CMS.Views.ModuleEdit extends Backbone.View
loadEdit: ->
if not @module
@module = XModule.loadModule(@$el.find('.xmodule_edit'))
+ @originalMetadata = @metadata()
metadata: ->
# cdodge: package up metadata which is separated into a number of input fields
@@ -35,6 +36,14 @@ class CMS.Views.ModuleEdit extends Backbone.View
return _metadata
+ changedMetadata: ->
+ currentMetadata = @metadata()
+ changedMetadata = {}
+ for key of currentMetadata
+ if currentMetadata[key] != @originalMetadata[key]
+ changedMetadata[key] = currentMetadata[key]
+ return changedMetadata
+
cloneTemplate: (parent, template) ->
$.post("/clone_item", {
parent_location: parent
@@ -60,7 +69,7 @@ class CMS.Views.ModuleEdit extends Backbone.View
course: course_location_analytics
id: _this.model.id
- data.metadata = _.extend(data.metadata || {}, @metadata())
+ data.metadata = _.extend(data.metadata || {}, @changedMetadata())
@hideModal()
@model.save(data).done( =>
# # showToastMessage("Your changes have been saved.", null, 3)
diff --git a/cms/static/js/base.js b/cms/static/js/base.js
index 3a51d797ec..c8371fcf64 100644
--- a/cms/static/js/base.js
+++ b/cms/static/js/base.js
@@ -10,7 +10,7 @@ var $newComponentTypePicker;
var $newComponentTemplatePickers;
var $newComponentButton;
-$(document).ready(function () {
+$(document).ready(function() {
$body = $('body');
$modal = $('.history-modal');
$modalCover = $('
');
@@ -35,7 +35,7 @@ $(document).ready(function () {
$('.uploads .upload-button').bind('click', showUploadModal);
$('.upload-modal .close-button').bind('click', hideModal);
- $body.on('click', '.embeddable-xml-input', function () {
+ $body.on('click', '.embeddable-xml-input', function() {
$(this).select();
});
@@ -45,8 +45,11 @@ $(document).ready(function () {
$('.new-unit-item').bind('click', createNewUnit);
// lean/simple modal
- $('a[rel*=modal]').leanModal({overlay : 0.80, closeButton: '.action-modal-close' });
- $('a.action-modal-close').click(function(e){
+ $('a[rel*=modal]').leanModal({
+ overlay: 0.80,
+ closeButton: '.action-modal-close'
+ });
+ $('a.action-modal-close').click(function(e) {
(e).preventDefault();
});
@@ -55,12 +58,12 @@ $(document).ready(function () {
$('.action-notification-close').bind('click', hideNotification);
// nav - dropdown related
- $body.click(function (e) {
+ $body.click(function(e) {
$('.nav-dropdown .nav-item .wrapper-nav-sub').removeClass('is-shown');
$('.nav-dropdown .nav-item .title').removeClass('is-selected');
});
- $('.nav-dropdown .nav-item .title').click(function (e) {
+ $('.nav-dropdown .nav-item .title').click(function(e) {
$subnav = $(this).parent().find('.wrapper-nav-sub');
$title = $(this).parent().find('.title');
@@ -70,9 +73,7 @@ $(document).ready(function () {
if ($subnav.hasClass('is-shown')) {
$subnav.removeClass('is-shown');
$title.removeClass('is-selected');
- }
-
- else {
+ } else {
$('.nav-dropdown .nav-item .title').removeClass('is-selected');
$('.nav-dropdown .nav-item .wrapper-nav-sub').removeClass('is-shown');
$title.addClass('is-selected');
@@ -84,8 +85,11 @@ $(document).ready(function () {
$('a[rel="external"]').attr('title', 'This link will open in a new browser window/tab').bind('click', linkNewWindow);
// general link management - lean modal window
- $('a[rel="modal"]').attr('title', 'This link will open in a modal window').leanModal({overlay: 0.50, closeButton: '.action-modal-close' });
- $('.action-modal-close').click(function (e) {
+ $('a[rel="modal"]').attr('title', 'This link will open in a modal window').leanModal({
+ overlay: 0.50,
+ closeButton: '.action-modal-close'
+ });
+ $('.action-modal-close').click(function(e) {
(e).preventDefault();
});
@@ -99,7 +103,7 @@ $(document).ready(function () {
$('.cta-show-sock').bind('click', toggleSock);
// toggling overview section details
- $(function () {
+ $(function() {
if ($('.courseware-section').length > 0) {
$('.toggle-button-sections').addClass('is-shown');
}
@@ -108,7 +112,7 @@ $(document).ready(function () {
// autosave when leaving input field
$body.on('change', '.subsection-display-name-input', saveSubsection);
- $('.subsection-display-name-input').each(function () {
+ $('.subsection-display-name-input').each(function() {
this.val = $(this).val();
});
$("#start_date, #start_time, #due_date, #due_time").bind('change', autosaveInput);
@@ -129,7 +133,7 @@ $(document).ready(function () {
// import form setup
$('.import .file-input').bind('change', showImportSubmit);
- $('.import .choose-file-button, .import .choose-file-button-inline').bind('click', function (e) {
+ $('.import .choose-file-button, .import .choose-file-button-inline').bind('click', function(e) {
e.preventDefault();
$('.import .file-input').click();
});
@@ -152,12 +156,12 @@ $(document).ready(function () {
$body.on('click', '.section-published-date .schedule-button', editSectionPublishDate);
$body.on('click', '.edit-subsection-publish-settings .save-button', saveSetSectionScheduleDate);
$body.on('click', '.edit-subsection-publish-settings .cancel-button', hideModal);
- $body.on('change', '.edit-subsection-publish-settings .start-date', function () {
+ $body.on('change', '.edit-subsection-publish-settings .start-date', function() {
if ($('.edit-subsection-publish-settings').find('.start-time').val() == '') {
$('.edit-subsection-publish-settings').find('.start-time').val('12:00am');
}
});
- $('.edit-subsection-publish-settings').on('change', '.start-date, .start-time', function () {
+ $('.edit-subsection-publish-settings').on('change', '.start-date, .start-time', function() {
$('.edit-subsection-publish-settings').find('.save-button').show();
});
});
@@ -177,7 +181,7 @@ function smoothScrollLink(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) {
+window.CmsUtils.smoothScrollTop = function(e) {
(e).preventDefault();
$.smoothScroll({
@@ -260,8 +264,7 @@ function getEdxTimeFromDateTimeVals(date_val, time_val) {
var edxTimeStr = null;
if (date_val != '') {
- if (time_val == '')
- time_val = '00:00';
+ if (time_val == '') time_val = '00:00';
// Note, we are using date.js utility which has better parsing abilities than the built in JS date parsing
var date = Date.parse(date_val + " " + time_val);
@@ -284,7 +287,7 @@ function autosaveInput(e) {
clearTimeout(this.saveTimer);
}
- this.saveTimer = setTimeout(function () {
+ this.saveTimer = setTimeout(function() {
$changedInput = $(e.target);
saveSubsection();
self.saveTimer = null;
@@ -324,12 +327,15 @@ function saveSubsection() {
type: "POST",
dataType: "json",
contentType: "application/json",
- data: JSON.stringify({ 'id': id, 'metadata': metadata}),
- success: function () {
+ data: JSON.stringify({
+ 'id': id,
+ 'metadata': metadata
+ }),
+ success: function() {
$spinner.delay(500).fadeOut(150);
$changedInput = null;
},
- error: function () {
+ error: function() {
showToastMessage('There has been an error while saving your changes.');
}
});
@@ -348,15 +354,16 @@ function createNewUnit(e) {
});
- $.post('/clone_item',
- {'parent_location': parent,
- 'template': template,
- 'display_name': 'New Unit'
- },
- function (data) {
- // redirect to the edit page
- window.location = "/edit/" + data['id'];
- });
+ $.post('/clone_item', {
+ 'parent_location': parent,
+ 'template': template,
+ 'display_name': 'New Unit'
+ },
+
+ function(data) {
+ // redirect to the edit page
+ window.location = "/edit/" + data['id'];
+ });
}
function deleteUnit(e) {
@@ -375,8 +382,7 @@ function deleteSection(e) {
}
function _deleteItem($el) {
- if (!confirm('Are you sure you wish to delete this item. It cannot be reversed!'))
- return;
+ if (!confirm('Are you sure you wish to delete this item. It cannot be reversed!')) return;
var id = $el.data('id');
@@ -386,11 +392,15 @@ function _deleteItem($el) {
});
- $.post('/delete_item',
- {'id': id, 'delete_children': true, 'delete_all_versions': true},
- function (data) {
- $el.remove();
- });
+ $.post('/delete_item', {
+ 'id': id,
+ 'delete_children': true,
+ 'delete_all_versions': true
+ },
+
+ function(data) {
+ $el.remove();
+ });
}
function showUploadModal(e) {
@@ -492,18 +502,16 @@ function toggleSock(e) {
$sockContent.toggle('fast');
$.smoothScroll({
- offset: -200,
- easing: 'swing',
- speed: 1000,
- scrollElement: null,
- scrollTarget: $sock
+ offset: -200,
+ easing: 'swing',
+ speed: 1000,
+ scrollElement: null,
+ scrollTarget: $sock
});
- if($sock.hasClass('is-shown')) {
+ if ($sock.hasClass('is-shown')) {
$btnLabel.text(gettext('Hide Studio Help'));
- }
-
- else {
+ } else {
$btnLabel.text(gettext('Looking for Help with Studio?'));
}
}
@@ -549,7 +557,7 @@ function removeDateSetter(e) {
function hideNotification(e) {
(e).preventDefault();
- $(this).closest('.wrapper-notification').removeClass('is-shown').addClass('is-hiding').attr('aria-hidden','true');
+ $(this).closest('.wrapper-notification').removeClass('is-shown').addClass('is-hiding').attr('aria-hidden', 'true');
}
function hideAlert(e) {
@@ -580,7 +588,7 @@ function showToastMessage(message, $button, lifespan) {
$toast.fadeIn(200);
if (lifespan) {
- $toast.timer = setTimeout(function () {
+ $toast.timer = setTimeout(function() {
$toast.fadeOut(300);
}, lifespan * 1000);
}
@@ -602,7 +610,9 @@ function addNewSection(e, isTemplate) {
$newSection.find('.new-section-name').focus().select();
$newSection.find('.section-name-form').bind('submit', saveNewSection);
$cancelButton.bind('click', cancelNewSection);
- $body.bind('keyup', { $cancelButton: $cancelButton }, checkForCancel);
+ $body.bind('keyup', {
+ $cancelButton: $cancelButton
+ }, checkForCancel);
}
function checkForCancel(e) {
@@ -627,15 +637,14 @@ function saveNewSection(e) {
});
$.post('/clone_item', {
- 'parent_location': parent,
- 'template': template,
- 'display_name': display_name,
- },
- function (data) {
- if (data.id != undefined)
- location.reload();
- }
- );
+ 'parent_location': parent,
+ 'template': template,
+ 'display_name': display_name,
+ },
+
+ function(data) {
+ if (data.id != undefined) location.reload();
+ });
}
function cancelNewSection(e) {
@@ -654,7 +663,9 @@ function addNewCourse(e) {
$newCourse.find('.new-course-name').focus().select();
$newCourse.find('form').bind('submit', saveNewCourse);
$cancelButton.bind('click', cancelNewCourse);
- $body.bind('keyup', { $cancelButton: $cancelButton }, checkForCancel);
+ $body.bind('keyup', {
+ $cancelButton: $cancelButton
+ }, checkForCancel);
}
function saveNewCourse(e) {
@@ -678,18 +689,19 @@ function saveNewCourse(e) {
});
$.post('/create_new_course', {
- 'template': template,
- 'org': org,
- 'number': number,
- 'display_name': display_name
- },
- function (data) {
- if (data.id != undefined) {
- window.location = '/' + data.id.replace(/.*:\/\//, '');
- } else if (data.ErrMsg != undefined) {
- alert(data.ErrMsg);
- }
- });
+ 'template': template,
+ 'org': org,
+ 'number': number,
+ 'display_name': display_name
+ },
+
+ function(data) {
+ if (data.id != undefined) {
+ window.location = '/' + data.id.replace(/.*:\/\//, '');
+ } else if (data.ErrMsg != undefined) {
+ alert(data.ErrMsg);
+ }
+ });
}
function cancelNewCourse(e) {
@@ -715,7 +727,9 @@ function addNewSubsection(e) {
$newSubsection.find('.new-subsection-form').bind('submit', saveNewSubsection);
$cancelButton.bind('click', cancelNewSubsection);
- $body.bind('keyup', { $cancelButton: $cancelButton }, checkForCancel);
+ $body.bind('keyup', {
+ $cancelButton: $cancelButton
+ }, checkForCancel);
}
function saveNewSubsection(e) {
@@ -732,16 +746,16 @@ function saveNewSubsection(e) {
$.post('/clone_item', {
- 'parent_location': parent,
- 'template': template,
- 'display_name': display_name
- },
- function (data) {
- if (data.id != undefined) {
- location.reload();
- }
+ 'parent_location': parent,
+ 'template': template,
+ 'display_name': display_name
+ },
+
+ function(data) {
+ if (data.id != undefined) {
+ location.reload();
}
- );
+ });
}
function cancelNewSubsection(e) {
@@ -757,7 +771,9 @@ function editSectionName(e) {
$(this).children('.section-name-span').hide();
$(this).find('.section-name-edit').bind('submit', saveEditSectionName);
$(this).find('.edit-section-name-cancel').bind('click', cancelNewSection);
- $body.bind('keyup', { $cancelButton: $(this).find('.edit-section-name-cancel') }, checkForCancel);
+ $body.bind('keyup', {
+ $cancelButton: $(this).find('.edit-section-name-cancel')
+ }, checkForCancel);
}
function cancelEditSectionName(e) {
@@ -798,14 +814,19 @@ function saveEditSectionName(e) {
type: "POST",
dataType: "json",
contentType: "application/json",
- data: JSON.stringify({ 'id': id, 'metadata': {'display_name': display_name}})
- }).success(function () {
- $spinner.delay(250).fadeOut(250);
- $_this.closest('h3').find('.section-name-span').html(display_name).show();
- $_this.hide();
- $_this.closest('.section-name').bind('click', editSectionName);
- e.stopPropagation();
- });
+ data: JSON.stringify({
+ 'id': id,
+ 'metadata': {
+ 'display_name': display_name
+ }
+ })
+ }).success(function() {
+ $spinner.delay(250).fadeOut(250);
+ $_this.closest('h3').find('.section-name-span').html(display_name).show();
+ $_this.hide();
+ $_this.closest('.section-name').bind('click', editSectionName);
+ e.stopPropagation();
+ });
}
function setSectionScheduleDate(e) {
@@ -842,28 +863,36 @@ function saveSetSectionScheduleDate(e) {
type: "POST",
dataType: "json",
contentType: "application/json",
- data: JSON.stringify({ 'id': id, 'metadata': {'start': start}})
- }).success(function () {
- var $thisSection = $('.courseware-section[data-id="' + id + '"]');
- var format = gettext('Will Release: %(date)s at $(time)s UTC');
- var willReleaseAt = interpolate(format, [input_date, input_time], true);
- $thisSection.find('.section-published-date').html(
- '' + willReleaseAt + '' +
- '' +
- gettext('Edit') + '');
- $thisSection.find('.section-published-date').animate({
- 'background-color': 'rgb(182,37,104)'
- }, 300).animate({
- 'background-color': '#edf1f5'
- }, 300).animate({
- 'background-color': 'rgb(182,37,104)'
- }, 300).animate({
- 'background-color': '#edf1f5'
- }, 300);
+ data: JSON.stringify({
+ 'id': id,
+ 'metadata': {
+ 'start': start
+ }
+ })
+ }).success(function() {
+ var $thisSection = $('.courseware-section[data-id="' + id + '"]');
+ var format = gettext('Will Release: %(date)s at %(time)s UTC');
+ var willReleaseAt = interpolate(format, {
+ 'date': input_date,
+ 'time': input_time
+ },
+ true);
+ $thisSection.find('.section-published-date').html(
+ '' + willReleaseAt + '' +
+ '' + gettext('Edit') + '');
+ $thisSection.find('.section-published-date').animate({
+ 'background-color': 'rgb(182,37,104)'
+ }, 300).animate({
+ 'background-color': '#edf1f5'
+ }, 300).animate({
+ 'background-color': 'rgb(182,37,104)'
+ }, 300).animate({
+ 'background-color': '#edf1f5'
+ }, 300);
- hideModal();
- });
+ hideModal();
+ });
}
diff --git a/cms/static/js/hesitate.js b/cms/static/js/hesitate.js
index c5848a6c0c..8609562220 100644
--- a/cms/static/js/hesitate.js
+++ b/cms/static/js/hesitate.js
@@ -2,13 +2,13 @@
* Create a HesitateEvent and assign it as the event to execute:
* $(el).on('mouseEnter', CMS.HesitateEvent( expand, 'mouseLeave').trigger);
* It calls the executeOnTimeOut function with the event.currentTarget after the configurable timeout IFF the cancelSelector event
- * did not occur on the event.currentTarget.
- *
- * More specifically, when trigger is called (triggered by the event you bound it to), it starts a timer
+ * did not occur on the event.currentTarget.
+ *
+ * More specifically, when trigger is called (triggered by the event you bound it to), it starts a timer
* which the cancelSelector event will cancel or if the timer finished, it executes the executeOnTimeOut function
* passing it the original event (whose currentTarget s/b the specific ele). It never accumulates events; however, it doesn't hurt for your
* code to minimize invocations of trigger by binding to mouseEnter v mouseOver and such.
- *
+ *
* NOTE: if something outside of this wants to cancel the event, invoke cachedhesitation.untrigger(null | anything);
*/
@@ -25,7 +25,7 @@ CMS.HesitateEvent.DURATION = 800;
CMS.HesitateEvent.prototype.trigger = function(event) {
if (event.data.timeoutEventId == null) {
event.data.timeoutEventId = window.setTimeout(
- function() { event.data.fireEvent(event); },
+ function() { event.data.fireEvent(event); },
CMS.HesitateEvent.DURATION);
event.data.originalEvent = event;
$(event.data.originalEvent.delegateTarget).on(event.data.cancelSelector, event.data, event.data.untrigger);
@@ -45,4 +45,4 @@ CMS.HesitateEvent.prototype.untrigger = function(event) {
$(event.data.originalEvent.delegateTarget).off(event.data.cancelSelector, event.data.untrigger);
}
event.data.timeoutEventId = null;
-};
\ No newline at end of file
+};
diff --git a/cms/static/js/main.js b/cms/static/js/main.js
index 4a9b5d2374..2d72edc4bf 100644
--- a/cms/static/js/main.js
+++ b/cms/static/js/main.js
@@ -80,6 +80,6 @@ $(document).ready(function(){
$('section.problem-edit').show();
return false;
});
-
+
});
diff --git a/cms/static/js/models/course_info.js b/cms/static/js/models/course_info.js
index 8cb5a654cb..3bd7ffa97a 100644
--- a/cms/static/js/models/course_info.js
+++ b/cms/static/js/models/course_info.js
@@ -1,17 +1,17 @@
-// single per course holds the updates and handouts
+// single per course holds the updates and handouts
CMS.Models.CourseInfo = Backbone.Model.extend({
// This model class is not suited for restful operations and is considered just a server side initialized container
url: '',
-
+
defaults: {
"courseId": "", // the location url
"updates" : null, // UpdateCollection
"handouts": null // HandoutCollection
},
-
+
idAttribute : "courseId"
});
-
+
// course update -- biggest kludge here is the lack of a real id to map updates to originals
CMS.Models.CourseUpdate = Backbone.Model.extend({
defaults: {
@@ -26,11 +26,11 @@ CMS.Models.CourseUpdate = Backbone.Model.extend({
*/
CMS.Models.CourseUpdateCollection = Backbone.Collection.extend({
url : function() {return this.urlbase + "course_info/updates/";},
-
+
model : CMS.Models.CourseUpdate
});
-
-
-
\ No newline at end of file
+
+
+
diff --git a/cms/static/js/models/course_relative.js b/cms/static/js/models/course_relative.js
index 99bb1c6d77..6608f92a4f 100644
--- a/cms/static/js/models/course_relative.js
+++ b/cms/static/js/models/course_relative.js
@@ -16,7 +16,7 @@ CMS.Models.Location = Backbone.Model.extend({
},
_tagPattern : /[^:]+/g,
_fieldPattern : new RegExp('[^/]+','g'),
-
+
parse: function(payload) {
if (_.isArray(payload)) {
return {
@@ -25,7 +25,7 @@ CMS.Models.Location = Backbone.Model.extend({
course: payload[2],
category: payload[3],
name: payload[4]
- }
+ };
}
else if (_.isString(payload)) {
this._tagPattern.lastIndex = 0; // odd regex behavior requires this to be reset sometimes
@@ -65,4 +65,4 @@ CMS.Models.CourseRelative = Backbone.Model.extend({
CMS.Models.CourseRelativeCollection = Backbone.Collection.extend({
model : CMS.Models.CourseRelative
-});
\ No newline at end of file
+});
diff --git a/cms/static/js/models/module_info.js b/cms/static/js/models/module_info.js
index 6a593372c4..529d155aba 100644
--- a/cms/static/js/models/module_info.js
+++ b/cms/static/js/models/module_info.js
@@ -6,5 +6,5 @@ CMS.Models.ModuleInfo = Backbone.Model.extend({
"data": null,
"metadata" : null,
"children" : null
- },
-});
\ No newline at end of file
+ }
+});
diff --git a/cms/static/js/models/settings/advanced.js b/cms/static/js/models/settings/advanced.js
index adc259239d..c074b12067 100644
--- a/cms/static/js/models/settings/advanced.js
+++ b/cms/static/js/models/settings/advanced.js
@@ -11,7 +11,7 @@ CMS.Models.Settings.Advanced = Backbone.Model.extend({
validate: function (attrs) {
// Keys can no longer be edited. We are currently not validating values.
},
-
+
save : function (attrs, options) {
// wraps the save call w/ the deletion of the removed keys after we know the saved ones worked
options = options ? _.clone(options) : {};
@@ -23,7 +23,7 @@ CMS.Models.Settings.Advanced = Backbone.Model.extend({
};
Backbone.Model.prototype.save.call(this, attrs, options);
},
-
+
afterSave : function(self) {
// remove deleted attrs
if (!_.isEmpty(self.deleteKeys)) {
diff --git a/cms/static/js/models/settings/course_details.js b/cms/static/js/models/settings/course_details.js
index d41545cca9..b71b4e2ab2 100644
--- a/cms/static/js/models/settings/course_details.js
+++ b/cms/static/js/models/settings/course_details.js
@@ -66,7 +66,7 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({
save_videosource: function(newsource) {
// newsource either is or just the "speed:key, *" string
// returns the videosource for the preview which iss the key whose speed is closest to 1
- if (_.isEmpty(newsource) && !_.isEmpty(this.get('intro_video'))) this.save({'intro_video': null});
+ if (_.isEmpty(newsource) && !_.isEmpty(this.get('intro_video'))) this.save({'intro_video': null});
// TODO remove all whitespace w/in string
else {
if (this.get('intro_video') !== newsource) this.save('intro_video', newsource);
diff --git a/cms/static/js/models/settings/course_grading_policy.js b/cms/static/js/models/settings/course_grading_policy.js
index 3f8b1bf29a..8162145003 100644
--- a/cms/static/js/models/settings/course_grading_policy.js
+++ b/cms/static/js/models/settings/course_grading_policy.js
@@ -1,9 +1,9 @@
if (!CMS.Models['Settings']) CMS.Models.Settings = new Object();
CMS.Models.Settings.CourseGradingPolicy = Backbone.Model.extend({
- defaults : {
+ defaults : {
course_location : null,
- graders : null, // CourseGraderCollection
+ graders : null, // CourseGraderCollection
grade_cutoffs : null, // CourseGradeCutoff model
grace_period : null // either null or { hours: n, minutes: m, ...}
},
@@ -54,7 +54,7 @@ CMS.Models.Settings.CourseGrader = Backbone.Model.extend({
"type" : "", // must be unique w/in collection (ie. w/in course)
"min_count" : 1,
"drop_count" : 0,
- "short_label" : "", // what to use in place of type if space is an issue
+ "short_label" : "", // what to use in place of type if space is an issue
"weight" : 0 // int 0..100
},
parse : function(attrs) {
@@ -125,4 +125,4 @@ CMS.Models.Settings.CourseGraderCollection = Backbone.Collection.extend({
sumWeights : function() {
return this.reduce(function(subtotal, grader) { return subtotal + grader.get('weight'); }, 0);
}
-});
\ No newline at end of file
+});
diff --git a/cms/static/js/views/checklists_view.js b/cms/static/js/views/checklists_view.js
index 85c0f5242b..1ae29f99d5 100644
--- a/cms/static/js/views/checklists_view.js
+++ b/cms/static/js/views/checklists_view.js
@@ -93,4 +93,4 @@ CMS.Views.Checklists = Backbone.View.extend({
error : CMS.ServerError
});
}
-});
\ No newline at end of file
+});
diff --git a/cms/static/js/views/course_info_edit.js b/cms/static/js/views/course_info_edit.js
index a6d42e1927..50793c5f1e 100644
--- a/cms/static/js/views/course_info_edit.js
+++ b/cms/static/js/views/course_info_edit.js
@@ -32,7 +32,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
"click .post-actions > .edit-button" : "onEdit",
"click .post-actions > .delete-button" : "onDelete"
},
-
+
initialize: function() {
var self = this;
// instantiates an editor template for each update in the collection
@@ -41,13 +41,13 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
"/static/client_templates/course_info_update.html",
function (raw_template) {
self.template = _.template(raw_template);
- self.render();
+ self.render();
}
);
// when the client refetches the updates as a whole, re-render them
this.listenTo(this.collection, 'reset', this.render);
},
-
+
render: function () {
// iterate over updates and create views for each using the template
var updateEle = this.$el.find("#course-update-list");
@@ -66,14 +66,14 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
this.$el.find('.date').datepicker({ 'dateFormat': 'MM d, yy' });
return this;
},
-
+
onNew: function(event) {
event.preventDefault();
var self = this;
// create new obj, insert into collection, and render this one ele overriding the hidden attr
var newModel = new CMS.Models.CourseUpdate();
this.collection.add(newModel, {at : 0});
-
+
var $newForm = $(this.template({ updateModel : newModel }));
var updateEle = this.$el.find("#course-update-list");
@@ -87,7 +87,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
lineWrapping: true,
});
}
-
+
$newForm.addClass('editing');
this.$currentPost = $newForm.closest('li');
@@ -99,21 +99,21 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
$('.date').datepicker('destroy');
$('.date').datepicker({ 'dateFormat': 'MM d, yy' });
},
-
+
onSave: function(event) {
event.preventDefault();
var targetModel = this.eventModel(event);
targetModel.set({ date : this.dateEntry(event).val(), content : this.$codeMirror.getValue() });
- // push change to display, hide the editor, submit the change
+ // push change to display, hide the editor, submit the change
targetModel.save({}, {error : CMS.ServerError});
this.closeEditor(this);
analytics.track('Saved Course Update', {
'course': course_location_analytics,
- 'date': this.dateEntry(event).val()
+ 'date': this.dateEntry(event).val()
});
},
-
+
onCancel: function(event) {
event.preventDefault();
// change editor contents back to model values and hide the editor
@@ -121,13 +121,13 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
var targetModel = this.eventModel(event);
this.closeEditor(this, !targetModel.id);
},
-
+
onEdit: function(event) {
event.preventDefault();
var self = this;
this.$currentPost = $(event.target).closest('li');
this.$currentPost.addClass('editing');
-
+
$(this.editor(event)).show();
var $textArea = this.$currentPost.find(".new-update-content").first();
if (this.$codeMirror == null ) {
@@ -154,13 +154,13 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
analytics.track('Deleted Course Update', {
'course': course_location_analytics,
- 'date': this.dateEntry(event).val()
+ 'date': this.dateEntry(event).val()
});
var targetModel = this.eventModel(event);
this.modelDom(event).remove();
var cacheThis = this;
- targetModel.destroy({success : function (model, response) {
+ targetModel.destroy({success : function (model, response) {
cacheThis.collection.fetch({success : function() {cacheThis.render();},
error : CMS.ServerError});
},
@@ -192,17 +192,17 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
this.$codeMirror = null;
self.$currentPost.find('.CodeMirror').remove();
},
-
- // Dereferencing from events to screen elements
+
+ // Dereferencing from events to screen elements
eventModel: function(event) {
// not sure if it should be currentTarget or delegateTarget
return this.collection.get($(event.currentTarget).attr("name"));
},
-
+
modelDom: function(event) {
return $(event.currentTarget).closest("li");
},
-
+
editor: function(event) {
var li = $(event.currentTarget).closest("li");
if (li) return $(li).find("form").first();
@@ -216,7 +216,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
contentEntry: function(event) {
return $(event.currentTarget).closest("li").find(".new-update-content").first();
},
-
+
dateDisplay: function(event) {
return $(event.currentTarget).closest("li").find("#date-display").first();
},
@@ -224,7 +224,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
contentDisplay: function(event) {
return $(event.currentTarget).closest("li").find(".update-contents").first();
}
-
+
});
// the handouts view is dumb right now; it needs tied to a model and all that jazz
@@ -245,7 +245,7 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
"/static/client_templates/course_info_handouts.html",
function (raw_template) {
self.template = _.template(raw_template);
- self.render();
+ self.render();
}
);
},
@@ -253,8 +253,8 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
}
);
},
-
- render: function () {
+
+ render: function () {
var updateEle = this.$el;
var self = this;
this.$el.html(
@@ -313,4 +313,4 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
self.$form.find('.CodeMirror').remove();
this.$codeMirror = null;
}
-});
\ No newline at end of file
+});
diff --git a/cms/static/js/views/grader-select-view.js b/cms/static/js/views/grader-select-view.js
index 50e32cbbe1..941342f89e 100644
--- a/cms/static/js/views/grader-select-view.js
+++ b/cms/static/js/views/grader-select-view.js
@@ -16,7 +16,7 @@ CMS.Models.AssignmentGrade = Backbone.Model.extend({
urlRoot : function() {
if (this.has('location')) {
var location = this.get('location');
- return '/' + location.get('org') + "/" + location.get('course') + '/' + location.get('category') + '/'
+ return '/' + location.get('org') + "/" + location.get('course') + '/' + location.get('category') + '/'
+ location.get('name') + '/gradeas/';
}
else return "";
@@ -37,14 +37,14 @@ CMS.Views.OverviewAssignmentGrader = Backbone.View.extend({
'' +
'<% if (!hideSymbol) {%>✓<%};%>' +
'' +
- '
');
this.assignmentGrade = new CMS.Models.AssignmentGrade({
- assignmentUrl : this.$el.closest('.id-holder').data('id'),
+ assignmentUrl : this.$el.closest('.id-holder').data('id'),
graderType : this.$el.data('initial-status')});
// TODO throw exception if graders is null
this.graders = this.options['graders'];
@@ -78,13 +78,13 @@ CMS.Views.OverviewAssignmentGrader = Backbone.View.extend({
},
selectGradeType : function(e) {
e.preventDefault();
-
+
this.removeMenu(e);
// TODO I'm not happy with this string fetch via the html for what should be an id. I'd rather use the id attr
// of the CourseGradingPolicy model or null for Not Graded (NOTE, change template's if check for is-selected accordingly)
this.assignmentGrade.save('graderType', $(e.target).text());
-
+
this.render();
}
-})
\ No newline at end of file
+})
diff --git a/cms/static/js/views/overview.js b/cms/static/js/views/overview.js
index 7d92ab69ad..42ed2d6920 100644
--- a/cms/static/js/views/overview.js
+++ b/cms/static/js/views/overview.js
@@ -6,26 +6,26 @@ $(document).ready(function() {
$('.unit').draggable({
axis: 'y',
handle: '.drag-handle',
- zIndex: 999,
+ zIndex: 999,
start: initiateHesitate,
// left 2nd arg in as inert selector b/c i was uncertain whether we'd try to get the shove up/down
// to work in the future
- drag: generateCheckHoverState('.collapsed', ''),
+ drag: generateCheckHoverState('.collapsed', ''),
stop: removeHesitate,
revert: "invalid"
});
-
+
// Subsection reordering
$('.id-holder').draggable({
axis: 'y',
handle: '.section-item .drag-handle',
- zIndex: 999,
+ zIndex: 999,
start: initiateHesitate,
drag: generateCheckHoverState('.courseware-section.collapsed', ''),
stop: removeHesitate,
revert: "invalid"
});
-
+
// Section reordering
$('.courseware-section').draggable({
axis: 'y',
@@ -33,8 +33,8 @@ $(document).ready(function() {
stack: '.courseware-section',
revert: "invalid"
});
-
-
+
+
$('.sortable-unit-list').droppable({
accept : '.unit',
greedy: true,
@@ -50,7 +50,7 @@ $(document).ready(function() {
drop: onSubsectionReordered,
greedy: true
});
-
+
// Section reordering
$('.courseware-overview').droppable({
accept : '.courseware-section',
@@ -58,7 +58,7 @@ $(document).ready(function() {
drop: onSectionReordered,
greedy: true
});
-
+
// stop clicks on drag bars from doing their thing w/o stopping drag
$('.drag-handle').click(function(e) {e.preventDefault(); });
@@ -87,7 +87,7 @@ function computeIntersection(droppable, uiHelper, y) {
$.extend(droppable, {offset : $(droppable).offset()});
- var t = droppable.offset.top,
+ var t = droppable.offset.top,
b = t + droppable.proportions.height;
if (t === b) {
@@ -118,10 +118,10 @@ function generateCheckHoverState(selectorsToOpen, selectorsToShove) {
this[c === "isout" ? "isover" : "isout"] = false;
$(this).trigger(c === "isover" ? "dragEnter" : "dragLeave");
});
-
+
$(selectorsToShove).each(function() {
var intersectsBottom = computeIntersection(this, ui.helper, (draggable.positionAbs || draggable.position.absolute).top);
-
+
if ($(this).hasClass('ui-dragging-pushup')) {
if (!intersectsBottom) {
console.log('not up', $(this).data('id'));
@@ -132,10 +132,10 @@ function generateCheckHoverState(selectorsToOpen, selectorsToShove) {
console.log('up', $(this).data('id'));
$(this).addClass('ui-dragging-pushup');
}
-
- var intersectsTop = computeIntersection(this, ui.helper,
+
+ var intersectsTop = computeIntersection(this, ui.helper,
(draggable.positionAbs || draggable.position.absolute).top + draggable.helperProportions.height);
-
+
if ($(this).hasClass('ui-dragging-pushdown')) {
if (!intersectsTop) {
console.log('not down', $(this).data('id'));
@@ -146,7 +146,7 @@ function generateCheckHoverState(selectorsToOpen, selectorsToShove) {
console.log('down', $(this).data('id'));
$(this).addClass('ui-dragging-pushdown');
}
-
+
});
}
}
@@ -159,20 +159,20 @@ function removeHesitate(event, ui) {
}
function expandSection(event) {
- $(event.delegateTarget).removeClass('collapsed', 400);
+ $(event.delegateTarget).removeClass('collapsed', 400);
// don't descend to icon's on children (which aren't under first child) only to this element's icon
$(event.delegateTarget).children().first().find('.expand-collapse-icon').removeClass('expand', 400).addClass('collapse');
}
function onUnitReordered(event, ui) {
// a unit's been dropped on this subsection,
- // figure out where it came from and where it slots in.
+ // figure out where it came from and where it slots in.
_handleReorder(event, ui, 'subsection-id', 'li:.leaf');
}
function onSubsectionReordered(event, ui) {
// a subsection has been dropped on this section,
- // figure out where it came from and where it slots in.
+ // figure out where it came from and where it slots in.
_handleReorder(event, ui, 'section-id', 'li:.branch');
}
@@ -182,7 +182,7 @@ function onSectionReordered(event, ui) {
}
function _handleReorder(event, ui, parentIdField, childrenSelector) {
- // figure out where it came from and where it slots in.
+ // figure out where it came from and where it slots in.
var subsection_id = $(event.target).data(parentIdField);
var _els = $(event.target).children(childrenSelector);
var children = _els.map(function(idx, el) { return $(el).data('id'); }).get();
diff --git a/cms/static/js/views/server_error.js b/cms/static/js/views/server_error.js
index 11478b5712..c97a812358 100644
--- a/cms/static/js/views/server_error.js
+++ b/cms/static/js/views/server_error.js
@@ -1,4 +1,4 @@
CMS.ServerError = function(model, error) {
// this handler is for the client:server communication not the validation errors which handleValidationError catches
window.alert("Server Error: " + error.responseText);
-};
\ No newline at end of file
+};
diff --git a/cms/static/js/views/validating_view.js b/cms/static/js/views/validating_view.js
index 3376e5fe9b..5635d1e357 100644
--- a/cms/static/js/views/validating_view.js
+++ b/cms/static/js/views/validating_view.js
@@ -1,6 +1,6 @@
CMS.Views.ValidatingView = Backbone.View.extend({
- // Intended as an abstract class which catches validation errors on the model and
- // decorates the fields. Needs wiring per class, but this initialization shows how
+ // Intended as an abstract class which catches validation errors on the model and
+ // decorates the fields. Needs wiring per class, but this initialization shows how
// either have your init call this one or copy the contents
initialize : function() {
this.listenTo(this.model, 'error', CMS.ServerError);
@@ -15,7 +15,7 @@ CMS.Views.ValidatingView = Backbone.View.extend({
"change textarea" : "clearValidationErrors"
},
fieldToSelectorMap : {
- // Your subclass must populate this w/ all of the model keys and dom selectors
+ // Your subclass must populate this w/ all of the model keys and dom selectors
// which may be the subjects of validation errors
},
_cacheValidationErrors : [],
diff --git a/cms/static/sass/elements/_alerts.scss b/cms/static/sass/elements/_alerts.scss
index 49aa015313..1e8b79b3fe 100644
--- a/cms/static/sass/elements/_alerts.scss
+++ b/cms/static/sass/elements/_alerts.scss
@@ -6,7 +6,7 @@
@include box-sizing(border-box);
.copy {
- @include font-size(13);
+ @extend .t-copy-sub2;
}
}
@@ -184,12 +184,12 @@
}
.action-primary {
- @include font-size(13);
+ @extend .t-action3;
font-weight: 600;
}
.action-secondary {
- @include font-size(13);
+ @extend .t-action3;
}
}
}
@@ -367,12 +367,12 @@
}
.copy {
- @include font-size(13);
+ @extend .t-copy-sub2;
width: flex-grid(10, 12);
color: $gray-l2;
.title {
- @include font-size(14);
+ @extend .t-title-4;
margin-bottom: 0;
color: $white;
}
@@ -409,13 +409,13 @@
.action-primary {
@include blue-button();
- @include font-size(13);
+ @extend .t-action3;
border-color: $blue-d2;
font-weight: 600;
}
.action-secondary {
- @include font-size(13);
+ @extend .t-action3;
}
}
@@ -504,7 +504,7 @@
// adopted alerts
.alert {
- @include font-size(14);
+ @extend .t-copy-sub2;
@include box-sizing(border-box);
@include clearfix();
margin: 0 auto;
@@ -530,7 +530,7 @@
}
.copy {
- @include font-size(13);
+ @extend .t-copy-sub2;
width: flex-grid(10, 12);
color: $gray-l2;
@@ -568,12 +568,12 @@
}
.action-primary {
- @include font-size(13);
+ @extend .t-action3;
font-weight: 600;
}
.action-secondary {
- @include font-size(13);
+ @extend .t-action3;
}
}
}
@@ -730,7 +730,7 @@ body.uxdesign.alerts {
border-radius: 3px;
background: #fbf6e1;
// background: #edbd3c;
- font-size: 14px;
+ @extend .t-copy-sub1;
@include clearfix;
.alert-message {
diff --git a/cms/static/sass/elements/_typography.scss b/cms/static/sass/elements/_typography.scss
index 32c4b3928b..a58fe27eb8 100644
--- a/cms/static/sass/elements/_typography.scss
+++ b/cms/static/sass/elements/_typography.scss
@@ -2,7 +2,7 @@
// ====================
// headings/titles
-.t-title-1, .t-title-2, .t-title-3, .t-title-4, .t-title-5, .t-title-5 {
+.t-title-1, .t-title-2, .t-title-3, .t-title-4, .t-title-5 {
color: $gray-d3;
}
@@ -21,7 +21,7 @@
}
.t-title-4 {
-
+ @include font-size(14);
}
.t-title-5 {
@@ -82,4 +82,4 @@
// misc
.t-icon {
line-height: 0;
-}
\ No newline at end of file
+}
diff --git a/cms/templates/ux-alerts.html b/cms/templates/ux-alerts.html
index de062e471e..b9c5fd6053 100644
--- a/cms/templates/ux-alerts.html
+++ b/cms/templates/ux-alerts.html
@@ -114,6 +114,7 @@
diff --git a/cms/templates/widgets/metadata-edit.html b/cms/templates/widgets/metadata-edit.html
index 51fe400f88..aada438f38 100644
--- a/cms/templates/widgets/metadata-edit.html
+++ b/cms/templates/widgets/metadata-edit.html
@@ -1,5 +1,6 @@
<%
import hashlib
+ from xmodule.fields import StringyInteger, StringyFloat
hlskey = hashlib.md5(module.location.url()).hexdigest()
%>
@@ -7,17 +8,42 @@
% for field_name, field_value in editable_metadata_fields.items():
% if field_name == 'source_code':
- Edit High Level Source
+ % if field_value['explicitly_set'] is True:
+ Edit High Level Source
+ % endif
% else:
-
-
+
+
+ ## Change to True to see all the information being passed through.
+ % if False:
+
+
+
+
+
+
+ % if field_value['field'].values:
+
+ % for value in field_value['field'].values:
+
+ % endfor
+ % endif
+ % endif
% endif
% endfor
- % if 'source_code' in editable_metadata_fields:
- <%include file="source-edit.html" />
+ % if 'source_code' in editable_metadata_fields and editable_metadata_fields['source_code']['explicitly_set']:
+ <%include file="source-edit.html" />
% endif
diff --git a/cms/templates/widgets/source-edit.html b/cms/templates/widgets/source-edit.html
index 883190d6b3..b7ee6c9db9 100644
--- a/cms/templates/widgets/source-edit.html
+++ b/cms/templates/widgets/source-edit.html
@@ -12,7 +12,7 @@
+
+% if not user.is_authenticated():
+
+
+
+
+% endif
+
+
+
+
+
+
+
+
+
+
+
+
+
✕
+
+
+
+
+
+
+
Thank You!
+
+
+
+ <%
+ dst = datetime.now(pytz.utc).astimezone(pytz.timezone("America/New_York")).dst()
+ business_hours = "13:00 UTC to 21:00 UTC" if dst else "14:00 UTC to 22:00 UTC"
+ %>
+
+ Thank you for your inquiry or feedback. We typically respond to a
+ request within one business day (Monday to Friday,
+ ${business_hours}.) In the meantime, please review our
+ detailed FAQs
+ where most questions have already been answered.
+
The Devop Engineers at edX help develop and maintain the infrastructure in AWS for all services and systems required to run edX. We're seeking a capable systems administrator who is unafraid of scripting languages and development to build out tools in order to improve the functionality of edX. The devops team primarily focuses on the provisioning, configuration, and deployment of services at edX. If you have a passion for automation and constant improvement then we want to hear from you. Our production environment is primarily built on Ubuntu (in AWS) and we use Puppet and Fabric to manage most of the environment.
-
In addition to the primary task of building infrastructure the Devops team supports the developers in a variety of other contexts, including helping with desktop development environments if required. We participate in on-call and emergency support and there will be occasional out of normal hours work required.
-
Responsibilities:
-
-
Work with developers and staff to maintain and improve the infrastructure of edX.
-
Assist where needed with other technical support tasks to support the fast moving pace of edX.
-
Rapidly diagnose and resolve faults with organization-wide servers and services, and communicate to users as appropriate.
-
-
Requirements:
-
-
Bachelor's degree in engineering or computer science. 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.
-
Three or more years of systems administration.
-
Must have an excellent working knowledge of Linux both as an end-user and as an administrator.
-
Must be adept in programming/scripting languages such as Python, Ruby, Bash.
-
Must be familiar with a configuration management system such as Puppet, Chef, Ansible.
-
Must have experience running web applications in a production environment.
-
Must have excellent personal interaction skills as the position requires interfacing with a wide range of people up to board level.
-
Ideally possesses experience with some of the following technologies: nginx, mysql, mongodb, django environments, splunk, git.
-
-
-
If you are interested in this position, please send an email to jobs@edx.org.
-
-
-
-
LEARNING SCIENCES ENGINEER
@@ -483,39 +454,7 @@ development and program management teams.
If you are interested in this position, please send an email to jobs@edx.org.
-
-
-
-
WEB DESIGNER, PRODUCT TEAM
-
-
EdX is looking for a Web Designer to join our Product Team and shape the experience of edX's online learning tools. With thousands and thousands of students and hundreds of professors using our software every day, our online learning tools have to sing. Our ideal candidates are passionate and picky about what makes a good user experience; sweat the mechanical, visual, and transactional details when designing; know how to bring an idea or project from a sketch on paper to being alive in a browser; can instinctually bring organization to a design meeting, deliverable, or project; and thrive on collaboration with colleagues and constant iteration/refinement.
-
-
As an edX Designer, you:
-
-
Have an innate sense of – and strong opinion about – good usability when it comes to web applications, and an ability to clearly articulate both.
-
Understand established interactive technologies and possess an undying thirst to learn about new ones.
-
Define and work within visual themes based on your excellent understanding of grids, typography, color, and design principles.
-
Marry design aesthetics to user experiences while keeping in mind accessibility, usability, and web standards.
-
Can use HTML5, CSS3, and DOM-manipulating JavaScript to represent your designs in the browser.
-
Conceptualize and articulate complex ideas to drive decisions, facilitate understanding, and reach consensus.
-
Document your thinking using appropriately chosen, informed deliverables such as sketches, wireframes, prototypes, site maps/flows, personas, style tiles, and design comps.
-
Have a perfectionist mindset, but won’t lose momentum in projects because of it.
-
Expertly present user experience and design recommendations to team members.
-
-
-
Requirements:
-
-
Have at least 2 years of professional, post-collegiate experience.
-
Have a BA, BS, BFA, or equivalent work experience in areas such as human-computer interaction, information science, graphic or industrial design, computer science, fine arts, social sciences such as psychology, or another related field. 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.
-
-
About the Product Design Team:
-
We are a small team with a startup, lean culture, committed to building tools that help our users learn and teach online. Working alongside developers, course staff, product owners, and project stakeholders, our Designers shepherd the experience of an idea or tool through research and strategy phases and lead the Information Architecture, Interaction Design, Visual Design, and Front End Development efforts in bringing that experience to life. We enjoy holding Design Studio exercises, finding the right design tool to do the job efficiently, and our CSS preprocessors.
-
-
If you wish to apply, please send your resume (PDF, text, or Word Doc), a thoughtful email that includes specifics about how your previous experience matches the Designer role at edX, and online samples of your work to jobs@edx.org. Candidates who do not provide these will not be considered. EdX is open to considering candidates outside of the Boston/Cambridge, MA area who are willing to relocate.
-
-
-
-
+
FRONT END DEVELOPER
edX is looking for a Front End Developer to join our Product and Engineering Teams to shape the experience of all of edX's online learning tools. Thousands of students learn with us every day – the way they connect with their courses, their professors and edX is through our ever more powerful front end. Our ideal candidates not only know modern front end development best practices, but make organization standards and teach others with them; sweat the mechanical, visual, and transactional details when bring a design to life in the browser; can instinctually bring organization to their HTML/CSS/JavaScript, documentation, or project; and thrive on collaborating with both designers and developers throughout a project's lifecycle.
@@ -545,6 +484,72 @@ development and program management teams.
If you wish to apply, please send your resume (PDF, text, or Word Doc), a thoughtful email that includes specifics about how your previous experience matches the Front End Developer role at edX, and online samples of your work to jobs@edx.org. Candidates who do not provide these will not be considered. EdX is open to considering candidates outside of the Boston/Cambridge, MA area who are willing to relocate.
+
+
+
+
+
TEST ENGINEER
+
EdX is looking for a Software Engineer in Test to help architect and implement improvements to our testing infrastructure and write code to validate and verify development and deployment of our MOOC platform.
+
You are an experienced professional who is passionate about and current with cutting edge methodologies and practices for delivering high quality software. For example, you understand and can articulate the difference between BDD and TDD. You champion for developers to be confident in the quality of their code by giving them the tools they need to create and execute their own tests. You write unit tests that follow best practices for each layer of an MVC architecture. You work side by side with the DevOps team to define environments and automate their buildouts.
+
Responsibilities:
+
+
Review software designs with a focus on code quality, risk, and testability
+
Build tools and frameworks that enable fellow engineers be more productive, write better code and test it themselves
+
Code test automation at all levels including class library, web application framework, javascript, and end-to-end
+
Enable metrics collection to measure adoption and expand the reach of the delivered tools
+
Fix framework bugs and improve test architecture, including adding required unit tests
+
Train and mentor other team members
+
+
Qualifications:
+
+
Excellent coding skills across a number of languages: Python or other high level programming languages, Javascript, bash, etc.
+
Experience in building test automation frameworks
+
Comfortable with source code in various languages (Python/Django, Ruby/Rails, Javascript/Backbone/JQuery, etc.)
+
Highly proficient in a Unix/Linux environment
+
Experience with database technologies from SQLite to MongoDB
+
Familiar with deployment automation (Puppet, Jenkins, AWS)
+
Open Source development experience preferred, extra points for sharing your GitHub / StackOverflow / etc. profile
+
+
+
If you are interested in this position, please send an email to jobs@edx.org.
+
+
+
+
+
COORDINATOR OF UNIVERSITY AND BUSINESS AFFAIRS
+
EdX is looking for a Coordinator of External Affairs, to streamline, organize and maintain our efforts in Business Development and University Relations.
+
There are 4 primary areas of responsibility:
+
+
To ensure all visits to and from the edX offices by any partners and affiliates are managed, coordinated, and documented. This involves developing itineraries, booking flights and schedules, and managing meetings and events in concert with members of our executive team in University Relations and Business Development and our consortium of partners.
+
To maintain a database of partners and prospects and manage any data flows/reporting required.
+
To manage the information flow, recording activity on the edX Wiki page by synthesizing data and analysis from all visits and meetings and create updates on the edX Wiki page.
+
To act as a central point of contact for all relationship and event activity within this scope.
+
+
Detailed Responsibilities:
+
+
Provide support and coordinate activities for these 3 executives
+
Acquire strong user knowledge of related systems, processes and tools
+
Participate in the new partner on-boarding process
+
Provide an escalation point for Sales personnel for systems, procedures and policies
+
Maintain Salesforce database for client/partner set up and support information, generating reports as needed
+
Document proofreading, editing as directed for proposals, contracts, contact and call reports
+
Coordinate and manage travel, events and meetings, including invitations, RSVP’s, hotel/meeting space contracts, and providing event materials to attendees
+
+
Qualifications:
+
+
5-7 years of experience in a similar project/coordinator type position with progressively responsible administrative experience
+
Self-starter, possessing tenacity and a desire for challenges, not afraid to take risks, and the initiative to get things done with little direction
+
Superior interpersonal and communications skills, including concise writing and editing skills
+
Strong organizational skills to manage multiple competing priorities and projects with attention to detail
+
Exceptional ability to effectively interact with multiple external and internal stakeholders
+
Adept at analyzing complex issues with the ability to synthesize data and perform gap analyses
+
Performs well with a variety of disciplines while remaining effective in a high-volume, fast-pace start-up environment with high workload
+
Must be proficient in: MS PowerPoint, Word and Excel, Salesforce.com, and online tools such as Google docs and Wiki, and knowledge of Kanban is also helpful
+
+
+
If you are interested in this position, please send an email to jobs@edx.org.
EPFL is one of the two Swiss Federal Institutes of Technology. With the status of a national school since 1969, the young engineering school has grown in many dimensions, to the extent of becoming one of the most famous European institutions of science and technology. It has three core missions: training, research and technology transfer.
-
EPFL is located in Lausanne in Switzerland, on the shores of the largest lake in Europe, Lake Geneva and at the foot of the Alps and Mont-Blanc. Its main campus brings together over 11,000 persons, students, researchers and staff in the same magical place. Because of its dynamism and rich student community, EPFL has been able to create a special spirit imbued with curiosity and simplicity. Daily interactions amongst students, researchers and entrepreneurs on campus give rise to new scientific, technological and architectural projects.
-
+
EPFL is the Swiss Federal Institute of Technology in Lausanne. The past decade has seen EPFL ascend to the very top of European institutions of science and technology: it is ranked #1 in Europe in the field of engineering by the Times Higher Education (based on publications and citations), Leiden Rankings, and the Academic Ranking of World Universities.
+
+
EPFL's main campus brings together 12,600 students, faculty, researchers, and staff in a high-energy, dynamic learning and research environment. It directs the Human Brain Project, an undertaking to simulate the entire human brain using supercomputers, in order to gain new insights into how it operates and to better diagnose brain disorders. The university is building Solar Impulse, a long-range solar-powered plane that aims to be the first piloted fixed-wing aircraft to circumnavigate the Earth using only solar power. EPFL was part of the Alinghi project, developing advanced racing boats that won the America's Cup multiple times. The university operates, for education and research purposes, a Tokamak nuclear fusion reactor. EPFL also houses the Musée Bolo museum and hosts several music festivals, including Balelec, that draws over 15,000 guests every year.
+
+
EPFL is a major force in entrepreneurship, with 2012 bringing in $100M in funding for ten EPFL startups. Both young spin-offs (like Typesafe and Pix4D) and companies that have long grown past the startup stage (like Logitech) actively transfer the results of EPFL's scientific innovation to industry.
+
%block>
${parent.body()}
diff --git a/lms/urls.py b/lms/urls.py
index 2573719332..5a568d689b 100644
--- a/lms/urls.py
+++ b/lms/urls.py
@@ -2,7 +2,6 @@ from django.conf import settings
from django.conf.urls import patterns, include, url
from django.contrib import admin
from django.conf.urls.static import static
-from django.views.generic import RedirectView
from . import one_time_startup
@@ -10,10 +9,9 @@ import django.contrib.auth.views
# Uncomment the next two lines to enable the admin:
if settings.DEBUG:
- from django.contrib import admin
admin.autodiscover()
-urlpatterns = ('',
+urlpatterns = ('', # nopep8
# certificate view
url(r'^update_certificate$', 'certificates.views.update_certificate'),
@@ -118,8 +116,9 @@ urlpatterns = ('',
# Favicon
(r'^favicon\.ico$', 'django.views.generic.simple.redirect_to', {'url': '/static/images/favicon.ico'}),
+ url(r'^submit_feedback$', 'util.views.submit_feedback_via_zendesk'),
+
# TODO: These urls no longer work. They need to be updated before they are re-enabled
- # url(r'^send_feedback$', 'util.views.send_feedback'),
# url(r'^reactivate/(?P[^/]*)$', 'student.views.reactivation_email'),
)
@@ -309,12 +308,12 @@ if settings.COURSEWARE_ENABLED:
'courseware.views.news', name="news"),
url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/discussion/',
include('django_comment_client.urls'))
- )
+ )
urlpatterns += (
# This MUST be the last view in the courseware--it's a catch-all for custom tabs.
url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/(?P[^/]+)/$',
'courseware.views.static_tab', name="static_tab"),
- )
+ )
if settings.MITX_FEATURES.get('ENABLE_STUDENT_HISTORY_VIEW'):
urlpatterns += (
@@ -356,13 +355,13 @@ if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'):
url(r'^migrate/reload/(?P[^/]+)/(?P[^/]+)$', 'lms_migration.migrate.manage_modulestores'),
url(r'^gitreload$', 'lms_migration.migrate.gitreload'),
url(r'^gitreload/(?P[^/]+)$', 'lms_migration.migrate.gitreload'),
- )
+ )
if settings.MITX_FEATURES.get('ENABLE_SQL_TRACKING_LOGS'):
urlpatterns += (
url(r'^event_logs$', 'track.views.view_tracking_log'),
url(r'^event_logs/(?P.+)$', 'track.views.view_tracking_log'),
- )
+ )
# FoldIt views
urlpatterns += (
diff --git a/pre-requirements.txt b/pre-requirements.txt
index 7ecead0ce7..d39199a741 100644
--- a/pre-requirements.txt
+++ b/pre-requirements.txt
@@ -1,2 +1,10 @@
+# We use `scipy` in our project, which relies on `numpy`. `pip` apparently
+# installs packages in a two-step process, where it will first try to build
+# all packages, and then try to install all packages. As a result, if we simply
+# added these packages to the top of `requirements.txt`, `pip` would try to
+# build `scipy` before `numpy` has been installed, and it would fail. By
+# separating this out into a `pre-requirements.txt` file, we can make sure
+# that `numpy` is built *and* installed before we try to build `scipy`.
+
numpy==1.6.2
distribute>=0.6.28